package submit

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"testing/fstest"
	"time"

	"n2usenet/internal/config"
	"n2usenet/internal/storage"
)

func newTestApp(t *testing.T) *App {
	t.Helper()
	app, err := NewApp(config.Config{}, nil, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "<!doctype html>")
	if err != nil {
		t.Fatalf("NewApp returned error: %v", err)
	}
	return app
}

func TestCSRFCookieHasNoElapsedTimeLimit(t *testing.T) {
	app := newTestApp(t)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()

	app.handleIndex(rec, req)

	cookies := rec.Result().Cookies()
	if len(cookies) != 1 {
		t.Fatalf("unexpected cookie count: got %d want 1", len(cookies))
	}
	cookie := cookies[0]
	if cookie.Name != "m2u_csrf" {
		t.Fatalf("unexpected cookie name: %q", cookie.Name)
	}
	if cookie.MaxAge != 0 || !cookie.Expires.IsZero() {
		t.Fatalf("CSRF cookie has an elapsed-time expiry: MaxAge=%d Expires=%v", cookie.MaxAge, cookie.Expires)
	}
	if !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode {
		t.Fatal("CSRF cookie security attributes were weakened")
	}
}

func TestCSRFRefreshIssuesValidToken(t *testing.T) {
	app := newTestApp(t)
	req := httptest.NewRequest(http.MethodGet, "/csrf", nil)
	rec := httptest.NewRecorder()

	app.handleCSRF(rec, req)

	if got := rec.Header().Get("Cache-Control"); got != "no-store" {
		t.Fatalf("unexpected Cache-Control: %q", got)
	}
	var body map[string]string
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode CSRF response: %v", err)
	}
	token := body["token"]
	if token == "" {
		t.Fatal("CSRF refresh returned an empty token")
	}
	cookies := rec.Result().Cookies()
	if len(cookies) != 1 {
		t.Fatalf("unexpected cookie count: got %d want 1", len(cookies))
	}

	form := url.Values{"csrf_token": {token}}
	post := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(form.Encode()))
	post.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	post.AddCookie(cookies[0])
	if err := post.ParseForm(); err != nil {
		t.Fatalf("parse form: %v", err)
	}
	if !app.verifyCSRF(post) {
		t.Fatal("refreshed CSRF token did not verify")
	}
}

func TestCSRFCookieSurvivesRestartWithPersistentKey(t *testing.T) {
	cfg := config.Config{Security: config.SecurityConfig{CSRFKey: []byte(strings.Repeat("k", 32))}}
	newApp := func() *App {
		app, err := NewApp(cfg, nil, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "<!doctype html>")
		if err != nil {
			t.Fatalf("NewApp returned error: %v", err)
		}
		return app
	}

	first := httptest.NewRecorder()
	newApp().handleCSRF(first, httptest.NewRequest(http.MethodGet, "/csrf", nil))
	var firstBody map[string]string
	if err := json.Unmarshal(first.Body.Bytes(), &firstBody); err != nil {
		t.Fatalf("decode first CSRF response: %v", err)
	}
	cookies := first.Result().Cookies()
	if len(cookies) != 1 {
		t.Fatalf("unexpected cookie count: %d", len(cookies))
	}

	requestAfterRestart := httptest.NewRequest(http.MethodGet, "/csrf", nil)
	requestAfterRestart.AddCookie(cookies[0])
	second := httptest.NewRecorder()
	newApp().handleCSRF(second, requestAfterRestart)
	var secondBody map[string]string
	if err := json.Unmarshal(second.Body.Bytes(), &secondBody); err != nil {
		t.Fatalf("decode second CSRF response: %v", err)
	}
	if secondBody["token"] != firstBody["token"] {
		t.Fatal("restart rotated a still-valid session token")
	}
	if got := len(second.Result().Cookies()); got != 0 {
		t.Fatalf("valid session unexpectedly rotated %d cookies", got)
	}
}

type transportStateStub struct {
	ready bool
}

func (s *transportStateStub) Ready() bool       { return s.ready }
func (s *transportStateStub) MarkSuccess()      { s.ready = true }
func (s *transportStateStub) MarkFailure(error) { s.ready = false }

func TestReadinessUsesCachedTransportState(t *testing.T) {
	app := newTestApp(t)
	state := &transportStateStub{}
	app.transport = state

	recorder := httptest.NewRecorder()
	app.handleReadiness(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil))
	if recorder.Code != http.StatusServiceUnavailable {
		t.Fatalf("unready status = %d, want %d", recorder.Code, http.StatusServiceUnavailable)
	}

	state.ready = true
	recorder = httptest.NewRecorder()
	app.handleReadiness(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil))
	if recorder.Code != http.StatusOK {
		t.Fatalf("ready status = %d, want %d", recorder.Code, http.StatusOK)
	}
}

func TestSuccessResponseJSONNoStore(t *testing.T) {
	app := newTestApp(t)
	req := httptest.NewRequest(http.MethodPost, "/submit", nil)
	req.Header.Set("Accept", "application/json")
	rec := httptest.NewRecorder()

	app.successResponse(rec, req, "<test@example.net>")

	if rec.Code != http.StatusOK {
		t.Fatalf("unexpected status: got %d want %d", rec.Code, http.StatusOK)
	}
	if got := rec.Header().Get("Cache-Control"); got != "no-store" {
		t.Fatalf("unexpected Cache-Control: %q", got)
	}
	if got := rec.Header().Get("Clear-Site-Data"); got != "\"storage\"" {
		t.Fatalf("unexpected Clear-Site-Data: %q", got)
	}
	if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
		t.Fatalf("unexpected Content-Type: %q", got)
	}
	var body map[string]string
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	if body["messageId"] != "<test@example.net>" {
		t.Fatalf("unexpected messageId: %q", body["messageId"])
	}
}

func TestErrorResponseJSONNoStore(t *testing.T) {
	app := newTestApp(t)
	req := httptest.NewRequest(http.MethodPost, "/submit", nil)
	req.Header.Set("Accept", "application/json")
	rec := httptest.NewRecorder()

	app.errorResponse(rec, req, http.StatusBadRequest, "Validation failed.")

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("unexpected status: got %d want %d", rec.Code, http.StatusBadRequest)
	}
	if got := rec.Header().Get("Cache-Control"); got != "no-store" {
		t.Fatalf("unexpected Cache-Control: %q", got)
	}
	if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
		t.Fatalf("unexpected Content-Type: %q", got)
	}
	var body map[string]string
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	if body["error"] != "Validation failed." {
		t.Fatalf("unexpected error body: %q", body["error"])
	}
}
