package httpserver_test

import (
	"net/http"
	"net/url"
	"testing"

	"update_server/internal/apikeys"
	"update_server/internal/config"
	"update_server/internal/db"
)

func TestAdminPOSTRejectsMissingOrInvalidCSRFToken(t *testing.T) {
	t.Parallel()

	router, _, _ := newTestRouterWithStore(t)
	sessionCookie := loginAsAdmin(t, router)

	missingRecorder := performRequest(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Desktop App"},
		"slug": {"desktop-app"},
	}, sessionCookie)
	if missingRecorder.Code != http.StatusForbidden {
		t.Fatalf("expected missing csrf token to return 403, got %d with body %s", missingRecorder.Code, missingRecorder.Body.String())
	}

	csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
	invalidRecorder := performRequest(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name":       {"Desktop App"},
		"slug":       {"desktop-app"},
		"csrf_token": {"definitely-wrong"},
	}, sessionCookie, csrfCookie)
	if invalidRecorder.Code != http.StatusForbidden {
		t.Fatalf("expected invalid csrf token to return 403, got %d with body %s", invalidRecorder.Code, invalidRecorder.Body.String())
	}

	location := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Desktop App"},
		"slug": {"desktop-app"},
	}, http.StatusSeeOther, sessionCookie)
	if location == "" {
		t.Fatal("expected valid csrf-protected form to redirect")
	}
}

func TestSecurityHeadersAndCookieDefaults(t *testing.T) {
	t.Parallel()

	router, cfg, store := newTestRouterWithConfig(t, func(cfg *config.Config) {
		cfg.BaseURL = "https://updates.example.com"
		cfg.SecureCookies = true
	})

	loginPageRecorder := performRequest(t, router, http.MethodGet, "/admin/login", nil)
	if loginPageRecorder.Code != http.StatusOK {
		t.Fatalf("expected login page to load, got %d", loginPageRecorder.Code)
	}

	assertHeaderContains(t, loginPageRecorder, "Cache-Control", "no-store")
	assertHeaderEquals(t, loginPageRecorder, "X-Frame-Options", "DENY")
	assertHeaderEquals(t, loginPageRecorder, "X-Content-Type-Options", "nosniff")
	assertHeaderEquals(t, loginPageRecorder, "Referrer-Policy", "no-referrer")
	assertHeaderContains(t, loginPageRecorder, "Content-Security-Policy", "frame-ancestors 'none'")
	assertHeaderContains(t, loginPageRecorder, "Strict-Transport-Security", "max-age=31536000")

	csrfCookie := ensureCSRFCookie(t, router)
	if csrfCookie.Path != "/admin" {
		t.Fatalf("expected csrf cookie path /admin, got %q", csrfCookie.Path)
	}
	if !csrfCookie.HttpOnly {
		t.Fatal("expected csrf cookie to be HttpOnly")
	}
	if !csrfCookie.Secure {
		t.Fatal("expected csrf cookie to be Secure when APP_BASE_URL is https")
	}
	if csrfCookie.SameSite != http.SameSiteStrictMode {
		t.Fatalf("expected csrf cookie SameSite=Strict, got %v", csrfCookie.SameSite)
	}

	loginRecorder := performRequest(t, router, http.MethodPost, "/admin/login", url.Values{
		"email":      {"admin@example.com"},
		"password":   {"correct horse battery staple"},
		"next":       {"/admin"},
		"csrf_token": {csrfCookie.Value},
	}, csrfCookie)
	if loginRecorder.Code != http.StatusSeeOther {
		t.Fatalf("expected login to redirect, got %d with body %s", loginRecorder.Code, loginRecorder.Body.String())
	}

	var sessionCookie *http.Cookie
	for _, cookie := range loginRecorder.Result().Cookies() {
		if cookie.Name == cfg.SessionCookieName {
			sessionCookie = cookie
			break
		}
	}

	if sessionCookie == nil {
		t.Fatal("expected session cookie after login")
	}
	if sessionCookie.Path != "/admin" {
		t.Fatalf("expected session cookie path /admin, got %q", sessionCookie.Path)
	}
	if !sessionCookie.HttpOnly {
		t.Fatal("expected session cookie to be HttpOnly")
	}
	if !sessionCookie.Secure {
		t.Fatal("expected session cookie to be Secure")
	}
	if sessionCookie.SameSite != http.SameSiteLaxMode {
		t.Fatalf("expected session cookie SameSite=Lax, got %v", sessionCookie.SameSite)
	}

	keyResult, err := apikeys.NewService(store).Create(t.Context(), apikeys.CreateParams{
		Name:        "Clients",
		ScopeMode:   db.ScopeModeAllProjects,
		CanDownload: true,
	})
	if err != nil {
		t.Fatalf("create api key: %v", err)
	}

	apiRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
	if apiRecorder.Code != http.StatusOK {
		t.Fatalf("expected api project listing to succeed, got %d with body %s", apiRecorder.Code, apiRecorder.Body.String())
	}

	assertHeaderContains(t, apiRecorder, "Cache-Control", "no-store")
	assertHeaderContains(t, apiRecorder, "Vary", "Authorization")
	assertHeaderEquals(t, apiRecorder, "X-Content-Type-Options", "nosniff")
	assertHeaderEquals(t, apiRecorder, "X-Frame-Options", "DENY")
	assertHeaderContains(t, apiRecorder, "X-Robots-Tag", "noindex")
}

func TestLoginRateLimitReturnsTooManyRequests(t *testing.T) {
	t.Parallel()

	router, _, _ := newTestRouterWithConfig(t, func(cfg *config.Config) {
		cfg.LoginRateLimitPerMinute = 60
		cfg.LoginRateLimitBurst = 2
	})

	csrfCookie := ensureCSRFCookie(t, router)
	form := url.Values{
		"email":      {"admin@example.com"},
		"password":   {"wrong-password"},
		"next":       {"/admin"},
		"csrf_token": {csrfCookie.Value},
	}

	for attempt := 0; attempt < 2; attempt++ {
		recorder := performRequest(t, router, http.MethodPost, "/admin/login", form, csrfCookie)
		if recorder.Code != http.StatusUnauthorized {
			t.Fatalf("expected attempt %d to return 401, got %d with body %s", attempt+1, recorder.Code, recorder.Body.String())
		}
	}

	limitedRecorder := performRequest(t, router, http.MethodPost, "/admin/login", form, csrfCookie)
	if limitedRecorder.Code != http.StatusTooManyRequests {
		t.Fatalf("expected limited login to return 429, got %d with body %s", limitedRecorder.Code, limitedRecorder.Body.String())
	}
	assertHeaderContains(t, limitedRecorder, "Retry-After", "1")
}

func TestClientAPIRateLimitReturnsTooManyRequests(t *testing.T) {
	t.Parallel()

	router, _, store := newTestRouterWithConfig(t, func(cfg *config.Config) {
		cfg.ClientRateLimitPerMinute = 60
		cfg.ClientRateLimitBurst = 2
	})

	keyResult, err := apikeys.NewService(store).Create(t.Context(), apikeys.CreateParams{
		Name:        "Clients",
		ScopeMode:   db.ScopeModeAllProjects,
		CanDownload: true,
	})
	if err != nil {
		t.Fatalf("create api key: %v", err)
	}

	for attempt := 0; attempt < 2; attempt++ {
		recorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
		if recorder.Code != http.StatusOK {
			t.Fatalf("expected api attempt %d to return 200, got %d with body %s", attempt+1, recorder.Code, recorder.Body.String())
		}
	}

	limitedRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
	if limitedRecorder.Code != http.StatusTooManyRequests {
		t.Fatalf("expected api rate limit to return 429, got %d with body %s", limitedRecorder.Code, limitedRecorder.Body.String())
	}
	assertHeaderContains(t, limitedRecorder, "Retry-After", "1")
}
