package middleware

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

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

func init() {
	gin.SetMode(gin.TestMode)
}

func TestCSRF_SafeMethodsAllowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.GET("/test", func(c *gin.Context) { c.Status(200) })
	router.HEAD("/test", func(c *gin.Context) { c.Status(200) })
	router.OPTIONS("/test", func(c *gin.Context) { c.Status(200) })

	for _, method := range []string{"GET", "HEAD", "OPTIONS"} {
		req := httptest.NewRequest(method, "/test", nil)
		req.Header.Set("Origin", "http://evil.com")
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)
		if w.Code != 200 {
			t.Errorf("%s should be allowed regardless of origin, got %d", method, w.Code)
		}
	}
}

func TestCSRF_AllowedOrigin(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Origin", "http://127.0.0.1:4173")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("expected 200 for allowed origin, got %d", w.Code)
	}
}

func TestCSRF_RejectedOrigin(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Origin", "http://evil.com")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for rejected origin, got %d", w.Code)
	}
}

func TestCSRF_RefererFallback(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Referer", "http://127.0.0.1:4173/some/page")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("expected 200 for allowed referer, got %d", w.Code)
	}
}

func TestCSRF_RejectedReferer(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Referer", "http://evil.com/attack")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for bad referer, got %d", w.Code)
	}
}

func TestCSRF_NoOriginNoReferer_Rejected_WithoutToken(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

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

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 when no origin/referer and no CSRF token, got %d", w.Code)
	}
}

func TestCSRF_NoOriginNoReferer_Allowed_WithMatchingToken(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	csrfToken := "test-csrf-token-value"
	req.Header.Set(csrfHeaderName, csrfToken)
	req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: csrfToken})
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("expected 200 with matching CSRF token, got %d", w.Code)
	}
}

func TestCSRF_NoOriginNoReferer_Rejected_MismatchToken(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set(csrfHeaderName, "header-value")
	req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "cookie-value"})
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for mismatched CSRF token, got %d", w.Code)
	}
}

func TestCSRF_NoOriginNoReferer_Rejected_HeaderOnlyNoCookie(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set(csrfHeaderName, "some-token")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 when CSRF header present but no cookie, got %d", w.Code)
	}
}

// ── Bearer token exemption tests (#506) ──────────────────────

func TestCSRF_BearerToken_POST_Allowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/services/data/v63.0/sobjects/Account", func(c *gin.Context) {
		c.JSON(200, gin.H{"id": "001xxx", "success": true})
	})

	// Simulates: curl -X POST -H "Authorization: Bearer TOKEN" (no Origin, no CSRF token)
	req := httptest.NewRequest("POST", "/services/data/v63.0/sobjects/Account", nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.fake-token")
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("Bearer token POST should bypass CSRF, got %d: %s", w.Code, w.Body.String())
	}
}

func TestCSRF_BearerToken_PATCH_Allowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.PATCH("/services/data/v63.0/sobjects/Account/:id", func(c *gin.Context) {
		c.Status(204)
	})

	req := httptest.NewRequest("PATCH", "/services/data/v63.0/sobjects/Account/001xxx", nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.fake-token")
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("Bearer token PATCH should bypass CSRF, got %d: %s", w.Code, w.Body.String())
	}
}

func TestCSRF_BearerToken_DELETE_Allowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.DELETE("/services/data/v63.0/sobjects/Account/:id", func(c *gin.Context) {
		c.Status(204)
	})

	req := httptest.NewRequest("DELETE", "/services/data/v63.0/sobjects/Account/001xxx", nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.fake-token")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("Bearer token DELETE should bypass CSRF, got %d: %s", w.Code, w.Body.String())
	}
}

func TestCSRF_BearerToken_NonBearerSchemes_StillChecked(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	// None of these should bypass CSRF
	cases := []struct {
		name  string
		value string
	}{
		{"Basic auth", "Basic dXNlcjpwYXNz"},
		{"Bearer without space", "Bearer"},          // Missing space → not "Bearer " prefix
		{"Lowercase bearer", "bearer some-token"},   // RFC 6750 uses capital B; we match strictly
		{"Empty header", ""},
	}

	for _, tc := range cases {
		req := httptest.NewRequest("POST", "/test", nil)
		if tc.value != "" {
			req.Header.Set("Authorization", tc.value)
		}
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		if w.Code != http.StatusForbidden {
			t.Errorf("%s: should NOT bypass CSRF, got %d", tc.name, w.Code)
		}
	}
}

func TestCSRF_BearerToken_WithEvilOrigin_StillAllowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	// Bearer token + evil Origin: Bearer exemption takes priority.
	// This is safe because CORS preflight blocks cross-origin requests
	// with Authorization header from disallowed origins, and requireAuth
	// validates the token downstream.
	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Authorization", "Bearer valid-token")
	req.Header.Set("Origin", "http://evil.com")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("Bearer token should bypass CSRF even with evil Origin, got %d", w.Code)
	}
}

func TestCSRF_SecFetchSiteCrossSiteRejected(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Origin", "http://127.0.0.1:4173")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for Sec-Fetch-Site=cross-site, got %d", w.Code)
	}
}

func TestCSRF_SecFetchSiteSameOriginAllowed(t *testing.T) {
	t.Setenv("FRONTEND_URL", "http://127.0.0.1:4173")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Origin", "http://127.0.0.1:4173")
	req.Header.Set("Sec-Fetch-Site", "same-origin")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("expected 200 for Sec-Fetch-Site=same-origin with valid origin, got %d", w.Code)
	}
}

func TestCSRF_UsesConfiguredOriginsAlias(t *testing.T) {
	t.Setenv("FRONTEND_URL", "")
	t.Setenv("CORS_ALLOWED_ORIGINS", "")
	t.Setenv("ALLOWED_ORIGINS", "http://127.0.0.1:4173")

	router := gin.New()
	router.Use(CSRF())
	router.POST("/test", func(c *gin.Context) { c.Status(200) })

	req := httptest.NewRequest("POST", "/test", nil)
	req.Header.Set("Origin", "http://127.0.0.1:4173")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Fatalf("expected deprecated alias to remain supported, got %d", w.Code)
	}
}

func TestSetCSRFCookie(t *testing.T) {
	router := gin.New()
	router.GET("/login", func(c *gin.Context) {
		SetCSRFCookie(c)
		c.Status(200)
	})

	req := httptest.NewRequest("GET", "/login", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	cookies := w.Result().Cookies()
	found := false
	for _, cookie := range cookies {
		if cookie.Name == csrfCookieName {
			found = true
			if len(cookie.Value) != csrfTokenBytes*2 {
				t.Errorf("expected %d hex chars, got %d", csrfTokenBytes*2, len(cookie.Value))
			}
			if cookie.HttpOnly {
				t.Error("CSRF cookie must NOT be HttpOnly so JavaScript can read it")
			}
			if cookie.SameSite != http.SameSiteLaxMode {
				t.Errorf("expected SameSite=Lax, got %v", cookie.SameSite)
			}
		}
	}
	if !found {
		t.Error("expected _csrf cookie to be set")
	}
}

func TestGenerateCSRFToken_ReturnsValidHex(t *testing.T) {
	token := generateCSRFToken()
	if len(token) != csrfTokenBytes*2 {
		t.Errorf("expected %d hex chars, got %d", csrfTokenBytes*2, len(token))
	}
	// Verify it's valid hex
	for _, c := range token {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
			t.Errorf("token contains non-hex character: %c", c)
			break
		}
	}
}

func TestIsLocalhost(t *testing.T) {
	tests := []struct {
		host string
		want bool
	}{
		{"localhost:3001", true},
		{"127.0.0.1:3001", true},
		{"example.com", false},
		{"example.com:443", false},
	}
	for _, tt := range tests {
		if got := isLocalhost(tt.host); got != tt.want {
			t.Errorf("isLocalhost(%q) = %v, want %v", tt.host, got, tt.want)
		}
	}
}
