package middleware

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

	"github.com/gin-gonic/gin"
)

func TestSecurityHeaders_AddsBasicHeaders(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = true // Skip HSTS and cross-origin headers
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d", w.Code)
	}

	// Check basic security headers
	tests := []struct {
		header   string
		expected string
	}{
		{"X-Content-Type-Options", "nosniff"},
		{"X-Frame-Options", "DENY"},
		{"X-XSS-Protection", "1; mode=block"},
		{"Referrer-Policy", "strict-origin-when-cross-origin"},
	}

	for _, tt := range tests {
		value := w.Header().Get(tt.header)
		if value != tt.expected {
			t.Errorf("Header %s: expected %q, got %q", tt.header, tt.expected, value)
		}
	}
}

func TestSecurityHeaders_HSTSNotAddedInDevelopment(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = true
	config.HSTSEnabled = true
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	hstsHeader := w.Header().Get("Strict-Transport-Security")
	if hstsHeader != "" {
		t.Errorf("HSTS should not be set in development mode, got: %s", hstsHeader)
	}
}

func TestSecurityHeaders_HSTSAddedInProduction(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = false
	config.HSTSEnabled = true
	config.HSTSMaxAge = 31536000
	config.HSTSIncludeSubdomains = true
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	hstsHeader := w.Header().Get("Strict-Transport-Security")
	if hstsHeader == "" {
		t.Error("HSTS should be set in production mode")
	}

	// Check that it contains max-age
	if hstsHeader != "max-age=31536000; includeSubDomains" {
		t.Errorf("Unexpected HSTS value: %s", hstsHeader)
	}
}

func TestSecurityHeaders_HSTSWithPreload(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = false
	config.HSTSEnabled = true
	config.HSTSMaxAge = 31536000
	config.HSTSIncludeSubdomains = true
	config.HSTSPreload = true
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	hstsHeader := w.Header().Get("Strict-Transport-Security")
	expected := "max-age=31536000; includeSubDomains; preload"
	if hstsHeader != expected {
		t.Errorf("Expected HSTS %q, got %q", expected, hstsHeader)
	}
}

func TestSecurityHeaders_CSPHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.CSPEnabled = true
	config.CSPPolicy = "default-src 'self'"
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	cspHeader := w.Header().Get("Content-Security-Policy")
	if cspHeader != "default-src 'self'" {
		t.Errorf("Expected CSP %q, got %q", "default-src 'self'", cspHeader)
	}
}

func TestSecurityHeaders_NoCSPWhenDisabled(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.CSPEnabled = false
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	cspHeader := w.Header().Get("Content-Security-Policy")
	if cspHeader != "" {
		t.Errorf("CSP should not be set when disabled, got: %s", cspHeader)
	}
}

func TestSecurityHeaders_ExcludedPaths(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.ExcludedPaths = []string{"/health", "/metrics"}
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "healthy"})
	})

	router.GET("/api", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	// Test excluded path
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Header().Get("X-Content-Type-Options") != "" {
		t.Error("Security headers should not be set for excluded paths")
	}

	// Test non-excluded path
	req = httptest.NewRequest(http.MethodGet, "/api", nil)
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Header().Get("X-Content-Type-Options") != "nosniff" {
		t.Error("Security headers should be set for non-excluded paths")
	}
}

func TestSecurityHeaders_CrossOriginInProduction(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = false
	config.CrossOriginOpenerPolicy = "same-origin"
	config.CrossOriginResourcePolicy = "same-origin"
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	coopHeader := w.Header().Get("Cross-Origin-Opener-Policy")
	if coopHeader != "same-origin" {
		t.Errorf("Expected COOP %q, got %q", "same-origin", coopHeader)
	}

	corpHeader := w.Header().Get("Cross-Origin-Resource-Policy")
	if corpHeader != "same-origin" {
		t.Errorf("Expected CORP %q, got %q", "same-origin", corpHeader)
	}
}

func TestSecurityHeaders_NoCrossOriginInDevelopment(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.DevelopmentMode = true
	config.CrossOriginOpenerPolicy = "same-origin"
	config.CrossOriginResourcePolicy = "same-origin"
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Header().Get("Cross-Origin-Opener-Policy") != "" {
		t.Error("COOP should not be set in development mode")
	}

	if w.Header().Get("Cross-Origin-Resource-Policy") != "" {
		t.Error("CORP should not be set in development mode")
	}
}

func TestSecurityHeaders_PermissionsPolicy(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	config := DefaultSecurityHeadersConfig()
	config.PermissionsPolicy = "geolocation=(), microphone=()"
	router.Use(SecurityHeadersWithConfig(config))

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	ppHeader := w.Header().Get("Permissions-Policy")
	if ppHeader != "geolocation=(), microphone=()" {
		t.Errorf("Expected Permissions-Policy %q, got %q", "geolocation=(), microphone=()", ppHeader)
	}
}

func TestSecurityHeaders_DefaultMiddleware(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	// Use the default middleware function
	router.Use(SecurityHeaders())

	router.GET("/test", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Should at least have the basic headers
	if w.Header().Get("X-Content-Type-Options") != "nosniff" {
		t.Error("Default middleware should set X-Content-Type-Options")
	}
}

func TestBuildHSTSHeader(t *testing.T) {
	tests := []struct {
		name     string
		config   SecurityHeadersConfig
		expected string
	}{
		{
			name: "basic HSTS",
			config: SecurityHeadersConfig{
				HSTSMaxAge:            31536000,
				HSTSIncludeSubdomains: false,
				HSTSPreload:           false,
			},
			expected: "max-age=31536000",
		},
		{
			name: "HSTS with subdomains",
			config: SecurityHeadersConfig{
				HSTSMaxAge:            31536000,
				HSTSIncludeSubdomains: true,
				HSTSPreload:           false,
			},
			expected: "max-age=31536000; includeSubDomains",
		},
		{
			name: "HSTS with preload",
			config: SecurityHeadersConfig{
				HSTSMaxAge:            31536000,
				HSTSIncludeSubdomains: true,
				HSTSPreload:           true,
			},
			expected: "max-age=31536000; includeSubDomains; preload",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := tt.config.buildHSTSHeader()
			if result != tt.expected {
				t.Errorf("Expected %q, got %q", tt.expected, result)
			}
		})
	}
}

func TestIsExcludedPath(t *testing.T) {
	config := SecurityHeadersConfig{
		ExcludedPaths: []string{"/health", "/metrics", "/api/v1/health"},
	}

	tests := []struct {
		path     string
		excluded bool
	}{
		{"/health", true},
		{"/metrics", true},
		{"/api/v1/health", true},
		{"/api", false},
		{"/health/check", false},
		{"/", false},
	}

	for _, tt := range tests {
		result := config.isExcludedPath(tt.path)
		if result != tt.excluded {
			t.Errorf("Path %s: expected excluded=%v, got %v", tt.path, tt.excluded, result)
		}
	}
}
