package middleware

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"

	"github.com/user-management-system/internal/config"
)

func TestCORS_UsesConfiguredOrigins(t *testing.T) {
	gin.SetMode(gin.TestMode)
	if err := SetCORSConfig(config.CORSConfig{
		AllowedOrigins:   []string{"https://app.example.com"},
		AllowCredentials: true,
	}); err != nil {
		t.Fatalf("SetCORSConfig should accept explicit origin with credentials: %v", err)
	}
	t.Cleanup(func() {
		if err := SetCORSConfig(config.CORSConfig{}); err != nil {
			t.Fatalf("reset cors config failed: %v", err)
		}
	})

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodOptions, "/api/v1/users", nil)
	c.Request.Header.Set("Origin", "https://app.example.com")
	c.Request.Header.Set("Access-Control-Request-Headers", "Authorization")

	CORS()(c)

	if recorder.Code != http.StatusNoContent {
		t.Fatalf("expected 204, got %d", recorder.Code)
	}
	if got := recorder.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
		t.Fatalf("unexpected allow origin: %s", got)
	}
	if got := recorder.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
		t.Fatalf("expected credentials header to be 'true', got %q", got)
	}
}

func TestSetCORSConfig_RejectsWildcardWithCredentials(t *testing.T) {
	gin.SetMode(gin.TestMode)
	if err := SetCORSConfig(config.CORSConfig{}); err != nil {
		t.Fatalf("failed to initialize baseline cors config: %v", err)
	}

	err := SetCORSConfig(config.CORSConfig{
		AllowedOrigins:   []string{"*"},
		AllowCredentials: true,
	})
	if err == nil {
		t.Fatal("expected wildcard+credentials cors config to be rejected")
	}

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodOptions, "/api/v1/users", nil)
	c.Request.Header.Set("Origin", "https://evil.example.com")
	c.Request.Header.Set("Access-Control-Request-Headers", "Authorization")

	CORS()(c)

	if recorder.Code != http.StatusForbidden {
		t.Fatalf("expected previous safe config to remain active and reject origin, got %d", recorder.Code)
	}
}

func TestSanitizeQuery_MasksSensitiveValues(t *testing.T) {
	raw := "token=abc123&foo=bar&access_token=xyz&secret=s1"
	sanitized := sanitizeQuery(raw)

	if sanitized == "" {
		t.Fatal("expected sanitized query")
	}
	if sanitized == raw {
		t.Fatal("expected query to be sanitized")
	}
	for _, value := range []string{"abc123", "xyz", "s1"} {
		if strings.Contains(sanitized, value) {
			t.Fatalf("expected sensitive value %q to be masked in %q", value, sanitized)
		}
	}
	if sanitizeQuery("") != "" {
		t.Fatal("expected empty query to stay empty")
	}
}

func TestSecurityHeaders_AttachesExpectedHeaders(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)

	SecurityHeaders()(c)

	if got := recorder.Header().Get("X-Content-Type-Options"); got != "nosniff" {
		t.Fatalf("unexpected nosniff header: %q", got)
	}
	if got := recorder.Header().Get("X-Frame-Options"); got != "DENY" {
		t.Fatalf("unexpected frame options: %q", got)
	}
	if got := recorder.Header().Get("Content-Security-Policy"); got == "" {
		t.Fatal("expected content security policy header")
	}
	if got := recorder.Header().Get("Strict-Transport-Security"); got != "" {
		t.Fatalf("did not expect hsts header for http request, got %q", got)
	}
}

func TestSecurityHeaders_AttachesHSTSForForwardedHTTPS(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)
	c.Request.Header.Set("X-Forwarded-Proto", "https")

	SecurityHeaders()(c)

	if got := recorder.Header().Get("Strict-Transport-Security"); !strings.Contains(got, "max-age=31536000") {
		t.Fatalf("expected hsts header, got %q", got)
	}
}

func TestNoStoreSensitiveResponses_AttachesExpectedHeadersToAuthRoutes(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/capabilities", nil)

	NoStoreSensitiveResponses()(c)

	if got := recorder.Header().Get("Cache-Control"); got != sensitiveNoStoreCacheControl {
		t.Fatalf("unexpected cache-control header: %q", got)
	}
	if got := recorder.Header().Get("Pragma"); got != "no-cache" {
		t.Fatalf("unexpected pragma header: %q", got)
	}
	if got := recorder.Header().Get("Expires"); got != "0" {
		t.Fatalf("unexpected expires header: %q", got)
	}
	if got := recorder.Header().Get("Surrogate-Control"); got != "no-store" {
		t.Fatalf("unexpected surrogate-control header: %q", got)
	}
}

func TestNoStoreSensitiveResponses_DoesNotAttachHeadersToNonAuthRoutes(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)

	NoStoreSensitiveResponses()(c)

	if got := recorder.Header().Get("Cache-Control"); got != "" {
		t.Fatalf("did not expect cache-control header, got %q", got)
	}
}

// ---------- TraceID middleware ----------

func TestTraceID_GeneratesAndAttachesTraceID(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)

	TraceID()(c)

	traceID := c.GetString("trace_id")
	if traceID == "" {
		t.Fatal("expected trace_id to be set")
	}
	if len(traceID) < 8 {
		t.Fatalf("trace_id should be reasonably long, got %q", traceID)
	}

	if got := recorder.Header().Get("X-Trace-ID"); got != traceID {
		t.Fatalf("expected X-Trace-ID header to match trace_id, got %q", got)
	}
}

func TestTraceID_ExtractsExistingTraceID(t *testing.T) {
	gin.SetMode(gin.TestMode)

	existingTraceID := "existing-trace-id-12345"
	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)
	c.Request.Header.Set("X-Trace-ID", existingTraceID)

	TraceID()(c)

	traceID := c.GetString("trace_id")
	if traceID != existingTraceID {
		t.Fatalf("expected trace_id to be extracted from header, got %q", traceID)
	}
}

// ---------- Error handling middleware ----------

func TestErrorHandler_HandlesErrors(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)

	c.Error(errors.New("test error"))

	ErrorHandler()(c)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("expected status 500, got %d", recorder.Code)
	}
}

func TestRecover_HandlesPanic(t *testing.T) {
	gin.SetMode(gin.TestMode)

	recorder := httptest.NewRecorder()
	c, router := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodGet, "/panic", nil)

	router.Use(Recover())
	router.GET("/panic", func(c *gin.Context) {
		panic("test panic")
	})

	router.ServeHTTP(recorder, c.Request)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("expected status 500 after panic, got %d", recorder.Code)
	}
}
