/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
:                                                                               :
:   █▀ █ █▀▀   ·   Blazing-fast pentesting suite                                :
:   ▄█ █ █▀    ·   BSD 3-Clause License                                         :
:                                                                               :
:   (c) 2022-2026 vmfunc, xyzeva,                                               :
:                 lunchcat alumni & contributors                                :
:                                                                               :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

package scan

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"
)

func buildHeader(kv map[string]string) http.Header {
	h := http.Header{}
	for k, v := range kv {
		h.Set(k, v)
	}
	return h
}

func findFinding(results SecurityHeaderResults, name string) (SecurityHeaderResult, bool) {
	for _, r := range results {
		if r.Header == name {
			return r, true
		}
	}
	return SecurityHeaderResult{}, false
}

func TestGradeSecurityHeaders_MissingOverHTTPS(t *testing.T) {
	results := gradeSecurityHeaders(http.Header{}, true)

	for _, h := range recommendedHeaders {
		f, ok := findFinding(results, h.name)
		if !ok {
			t.Errorf("expected %s to be flagged", h.name)
			continue
		}
		if f.Present {
			t.Errorf("%s should not be marked present", h.name)
		}
		if f.Severity != h.severity {
			t.Errorf("%s severity = %q, want %q", h.name, f.Severity, h.severity)
		}
	}
}

func TestGradeSecurityHeaders_HSTSSkippedOverHTTP(t *testing.T) {
	results := gradeSecurityHeaders(http.Header{}, false)
	if _, ok := findFinding(results, "Strict-Transport-Security"); ok {
		t.Error("HSTS should only be graded for https targets")
	}
}

func TestGradeSecurityHeaders_AllPresent(t *testing.T) {
	h := buildHeader(map[string]string{
		"Strict-Transport-Security":  "max-age=63072000; includeSubDomains",
		"Content-Security-Policy":    "default-src 'self'",
		"X-Frame-Options":            "DENY",
		"X-Content-Type-Options":     "nosniff",
		"Referrer-Policy":            "no-referrer",
		"Permissions-Policy":         "geolocation=()",
		"Cross-Origin-Opener-Policy": "same-origin",
	})

	if results := gradeSecurityHeaders(h, true); len(results) != 0 {
		t.Errorf("expected no findings, got %d: %+v", len(results), results)
	}
}

func TestGradeSecurityHeaders_ContentTypeNotNosniff(t *testing.T) {
	h := buildHeader(map[string]string{
		"Strict-Transport-Security":  "max-age=63072000",
		"Content-Security-Policy":    "default-src 'self'",
		"X-Frame-Options":            "DENY",
		"X-Content-Type-Options":     "sniff",
		"Referrer-Policy":            "no-referrer",
		"Permissions-Policy":         "geolocation=()",
		"Cross-Origin-Opener-Policy": "same-origin",
	})

	f, ok := findFinding(gradeSecurityHeaders(h, true), "X-Content-Type-Options")
	if !ok {
		t.Fatal("expected X-Content-Type-Options to be flagged when not nosniff")
	}
	if !f.Present || f.Value != "sniff" {
		t.Errorf("finding = %+v, want present with value sniff", f)
	}
}

func TestGradeSecurityHeaders_WeakHSTS(t *testing.T) {
	// max-age=0 actively disables hsts, so a present header still has to be flagged
	h := buildHeader(map[string]string{"Strict-Transport-Security": "max-age=0"})

	f, ok := findFinding(gradeSecurityHeaders(h, true), "Strict-Transport-Security")
	if !ok {
		t.Fatal("expected a short-lived hsts header to be flagged")
	}
	if !f.Present || f.Severity != "high" {
		t.Errorf("finding = %+v, want present high", f)
	}
}

func TestGradeSecurityHeaders_QuotedHSTS(t *testing.T) {
	// rfc 6797 allows a quoted value; strip the quotes before grading
	tests := []struct {
		name    string
		value   string
		flagged bool
	}{
		{"quoted strong", `max-age="63072000"; includeSubDomains`, false},
		{"quoted weak still flagged", `max-age="0"`, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := buildHeader(map[string]string{"Strict-Transport-Security": tt.value})
			_, flagged := findFinding(gradeSecurityHeaders(h, true), "Strict-Transport-Security")
			if flagged != tt.flagged {
				t.Errorf("value %q flagged=%v, want %v", tt.value, flagged, tt.flagged)
			}
		})
	}
}

func TestGradeSecurityHeaders_Disclosure(t *testing.T) {
	h := buildHeader(map[string]string{
		"Server":       "Apache/2.4.1 (Ubuntu)",
		"X-Powered-By": "PHP/8.1.2",
	})

	results := gradeSecurityHeaders(h, false)
	for _, name := range []string{"Server", "X-Powered-By"} {
		f, ok := findFinding(results, name)
		if !ok {
			t.Errorf("expected disclosure finding for %s", name)
			continue
		}
		if !f.Present || f.Severity != "low" {
			t.Errorf("%s finding = %+v, want present low", name, f)
		}
	}
}

func TestResponseIsHTTPS_UsesFinalRequestScheme(t *testing.T) {
	httpURL, err := url.Parse("http://example.com")
	if err != nil {
		t.Fatal(err)
	}
	httpsURL, err := url.Parse("https://example.com")
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name         string
		resp         *http.Response
		requestedURL string
		want         bool
	}{
		{
			// the requested url was http, but the client followed a redirect
			// to https; hsts must be graded against the actual scheme.
			name:         "redirected from http to https",
			resp:         &http.Response{Request: &http.Request{URL: httpsURL}},
			requestedURL: "http://example.com",
			want:         true,
		},
		{
			name:         "redirected from https to http",
			resp:         &http.Response{Request: &http.Request{URL: httpURL}},
			requestedURL: "https://example.com",
			want:         false,
		},
		{
			name:         "no redirect, stays http",
			resp:         &http.Response{Request: &http.Request{URL: httpURL}},
			requestedURL: "http://example.com",
			want:         false,
		},
		{
			name:         "no request on response falls back to requested url",
			resp:         &http.Response{},
			requestedURL: "https://example.com",
			want:         true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := responseIsHTTPS(tt.resp, tt.requestedURL); got != tt.want {
				t.Errorf("responseIsHTTPS() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSecurityHeaders_LiveResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Frame-Options", "SAMEORIGIN")
		w.Header().Set("Server", "nginx/1.25.3")
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	results, err := SecurityHeaders(server.URL, 5*time.Second, "")
	if err != nil {
		t.Fatalf("SecurityHeaders returned error: %v", err)
	}

	if _, ok := findFinding(results, "X-Frame-Options"); ok {
		t.Error("X-Frame-Options was set, should not be flagged")
	}
	if _, ok := findFinding(results, "Content-Security-Policy"); !ok {
		t.Error("expected missing Content-Security-Policy to be flagged")
	}
	if _, ok := findFinding(results, "Server"); !ok {
		t.Error("expected Server disclosure to be flagged")
	}
}
