//go:build test

package auth

// Package auth contains tests for authentication and verification endpoints.
//
// Test Coverage:
//   - RegisterHandler: POST /api/register - User registration
//     * Validates: required fields, email format, UK phone, age >= 16, duplicate email
//   - LoginHandler: POST /api/login - User login
//     * Validates credentials, returns JWT token
//   - RefreshTokenHandler: POST /api/refresh-token - Refresh JWT token
//   - GenerateVerificationCodeHandler: POST /api/verify/generate - Send verification code
//     * Returns success even for non-existent emails (security)
//   - VerifyCodeHandler: POST /api/verify/check - Verify code and update user role
//     * Validates: correct code, not expired, not already used
//     * Updates user role from unverified_email to verified_email on success
//
// Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.)

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"crussell/auth"
	"crussell/clock"
	"crussell/db"
	"crussell/mw"
	"crussell/testutils"
	"crussell/testutils/fixtures"
	"crussell/testutils/jwt"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

func resetTestData(t *testing.T) (context.Context, db.Querier) {
	t.Helper()
	ctx, tx := testutils.SetupTestTx(t)
	return ctx, tx
}

// =============================================================================
// Register Handler Tests
// =============================================================================

// TestRegister_Success verifies that a new user can successfully register with
// valid credentials. It tests the happy path: valid name, email, password,
// UK phone number, date of birth, and policy agreement. The test confirms
// the user is created in the database with status 201.
func TestRegister_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	body := RegisterRequest{
		FirstName:      "John",
		LastName:       "Doe",
		Email:          "john.doe@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify user was created in DB
	var userID string
	err := tx.QueryRow(ctx,
		"SELECT id FROM users WHERE email = $1", "john.doe@test.com").Scan(&userID)
	if err != nil {
		t.Errorf("failed to find user in DB: %v", err)
	}

	// Clean up
	tx.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
}

// TestRegister_InvalidInput_MissingFields tests that registration fails with
// HTTP 400 when required fields are missing. It covers missing firstName,
// lastName, email, phone, dateOfBirth, and when policy agreement is not given.
func TestRegister_InvalidInput_MissingFields(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	tests := []struct {
		name string
		body RegisterRequest
	}{
		{
			name: "missing firstName",
			body: RegisterRequest{LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07123456789", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
		},
		{
			name: "missing lastName",
			body: RegisterRequest{FirstName: "John", Email: "test@test.com", Password: "pass", Phone: "07123456789", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
		},
		{
			name: "missing email",
			body: RegisterRequest{FirstName: "John", LastName: "Doe", Password: "pass", Phone: "07123456789", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
		},
		{
			name: "missing phone",
			body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
		},
		{
			name: "missing dateOfBirth",
			body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07123456789", AgreedToPolicy: true},
		},
		{
			name: "did not agree to policy",
			body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07123456789", DateOfBirth: "1990-01-15", AgreedToPolicy: false},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", tt.body, ctx)
			if w.Code != http.StatusBadRequest {
				t.Errorf("expected status 400, got %d", w.Code)
			}
		})
	}
}

// TestRegister_InvalidInput_InvalidEmail verifies that registration fails
// with HTTP 400 when an invalid email format is provided (e.g., "not-an-email").
func TestRegister_InvalidInput_InvalidEmail(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	body := RegisterRequest{
		FirstName:      "John",
		LastName:       "Doe",
		Email:          "not-an-email",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestRegister_InvalidInput_InvalidPhone tests that registration fails
// with HTTP 400 when an invalid UK phone number is provided (e.g., too short).
func TestRegister_InvalidInput_InvalidPhone(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	body := RegisterRequest{
		FirstName:      "John",
		LastName:       "Doe",
		Email:          "john@test.com",
		Password:       "password123",
		Phone:          "12345", // Not a valid UK phone
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestRegister_ValidUKPhoneNumbers verifies that registration accepts all
// valid UK mobile phone formats including 07x numbers and E.164 format (+447...).
func TestRegister_ValidUKPhoneNumbers(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Valid UK mobile numbers (07x numbers)
	validPhones := []struct {
		name  string
		phone string
	}{
		{"07123456789", "07123456789"},     // Standard mobile
		{"07234567890", "07234567890"},     // 072
		{"07345678901", "07345678901"},     // 073
		{"07456789012", "07456789012"},     // 074
		{"07567890123", "07567890123"},     // 075
		{"07712345678", "07712345678"},     // 077
		{"07812345678", "07812345678"},     // 078
		{"07912345678", "07912345678"},     // 079
		{"+447123456789", "+447123456789"}, // E.164 format
	}

	for _, tc := range validPhones {
		t.Run(tc.name, func(t *testing.T) {
			body := RegisterRequest{
				FirstName:      "John",
				LastName:       "Doe",
				Email:          fmt.Sprintf("john.%s@test.com", tc.phone),
				Password:       "password123",
				Phone:          tc.phone,
				DateOfBirth:    "1990-01-15",
				AgreedToPolicy: true,
			}

			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

			if w.Code != http.StatusCreated {
				t.Errorf("expected status 201 for %s, got %d. body: %s", tc.phone, w.Code, w.Body.String())
			}
		})
	}
}

// TestRegister_InvalidPhoneNumbers verifies that registration rejects
// invalid phone numbers including too short, invalid formats, US numbers, and
// numbers with special characters.
func TestRegister_InvalidPhoneNumbers(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Invalid phone numbers - should all be rejected
	invalidPhones := []struct {
		name  string
		phone string
	}{
		{"too_short", "12345"},
		{"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber
		{"us_number", "+12025551234"},          // US number - not UK
		{"letters", "ABCDEFGHIJK"},
		{"empty", ""},
		{"special_chars", "+44!@#$%^&*()"},
	}

	for _, tc := range invalidPhones {
		t.Run(tc.name, func(t *testing.T) {
			body := RegisterRequest{
				FirstName:      "John",
				LastName:       "Doe",
				Email:          fmt.Sprintf("john.%s@test.com", tc.phone),
				Password:       "password123",
				Phone:          tc.phone,
				DateOfBirth:    "1990-01-15",
				AgreedToPolicy: true,
			}

			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

			if w.Code != http.StatusBadRequest {
				t.Errorf("expected status 400 for invalid phone %s, got %d. body: %s", tc.phone, w.Code, w.Body.String())
			}
		})
	}
}

// TestRegister_InvalidInput_Under16 tests that users under 16 years old cannot
// register. The system enforces a minimum age of 16 for account creation.
func TestRegister_InvalidInput_Under16(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Calculate a date that makes them under 16
	under16DOB := clock.Now().AddDate(-15, 0, 0).Format("2006-01-02")

	body := RegisterRequest{
		FirstName:      "Young",
		LastName:       "User",
		Email:          "young@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    under16DOB,
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestRegister_DuplicateEmail verifies that attempting to register with
// an email that already exists returns HTTP 409 Conflict.
func TestRegister_DuplicateEmail(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// First create a user with specific email
	_, err := tx.Exec(ctx, `
		INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
		VALUES ('Test', 'User', 'user@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
	`)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	// Now try to register with same email
	body := RegisterRequest{
		FirstName:      "John",
		LastName:       "Doe",
		Email:          "user@test.com", // Same as fixture
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Login Handler Tests
// =============================================================================

// TestLogin_Success tests that an existing user can successfully log in
// with correct email and password, receiving a JWT token in the response.
func TestLogin_Success(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	// Create a test user with known email
	userID, err := fixtures.CreateTestUserWithEmail(tx, "user@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	body := LoginRequest{
		Email:    "user@test.com",
		Password: "testpassword123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp struct {
		Token string `json:"token"`
	}
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Token == "" {
		t.Error("expected token in response, got empty string")
	}
}

// TestLogin_InvalidCredentials_WrongPassword verifies that login fails with
// HTTP 401 when the correct email exists but the password is incorrect.
func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	body := LoginRequest{
		Email:    "user@test.com",
		Password: "wrongpassword",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails
// with HTTP 401 when the email does not exist in the database.
func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) {
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	body := LoginRequest{
		Email:    "nonexistent@test.com",
		Password: "password123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Refresh Token Handler Tests
// =============================================================================

// TestRefreshToken_Success tests that a valid JWT token can be refreshed
// to obtain a new token with extended expiry.
func TestRefreshToken_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(RefreshTokenHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Generate a valid token
	token := jwt.GenerateTestToken(userID, "verified_email")

	req := httptest.NewRequest("POST", "/api/refresh-token", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()

	// Use the middleware keys to set up context (matching what mw.RequireAuth does)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, userID)
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email")
	req = req.WithContext(reqCtx)

	handler.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp struct {
		Token string `json:"token"`
	}
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Token == "" {
		t.Error("expected new token in response, got empty string")
	}
}

// TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh
// a token without providing one results in HTTP 401 Unauthorized.
func TestRefreshToken_Unauthorized_NoToken(t *testing.T) {
	t.Parallel()
	_, _ = resetTestData(t)

	handler := http.HandlerFunc(RefreshTokenHandler)

	req := httptest.NewRequest("POST", "/api/refresh-token", nil)
	w := httptest.NewRecorder()

	handler.ServeHTTP(w, req)

	// Without proper auth middleware, userID/role won't be in context
	// The handler tries to query DB with empty userID, which should fail
	if w.Code != http.StatusUnauthorized && w.Code != http.StatusInternalServerError {
		t.Errorf("expected status 401 or 500, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Verify Generate Handler Tests
// =============================================================================

// TestVerifyGenerate_ValidEmail tests that a verification code can be
// generated for an existing user email. The code is stored in the database
// for subsequent verification.
func TestVerifyGenerate_ValidEmail(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(GenerateVerificationCodeHandler)

	// Create a test user with known email
	userID, err := fixtures.CreateTestUserWithEmail(tx, "user@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	body := VerificationCodeRequest{
		Email: "user@test.com",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/generate", body, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp VerificationResponse
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if !resp.Success {
		t.Error("expected success=true in response")
	}

	// Verify a code was created in DB
	var codeID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM verification_codes WHERE user_id = $1", userID).Scan(&codeID)
	if err != nil {
		t.Errorf("failed to find verification code in DB: %v", err)
	}

	// Clean up
	tx.Exec(ctx, "DELETE FROM verification_codes WHERE user_id = $1", userID)
}

// TestVerifyGenerate_NonExistentEmail verifies that the verification code
// generation endpoint returns HTTP 200 even for non-existent emails. This is
// a security measure to prevent email enumeration attacks.
func TestVerifyGenerate_NonExistentEmail(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(GenerateVerificationCodeHandler)

	// Security: should return success even if email doesn't exist
	body := VerificationCodeRequest{
		Email: "nonexistent@test.com",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/generate", body, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp VerificationResponse
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	// Should return success for security (don't reveal if email exists)
	if !resp.Success {
		t.Error("expected success=true in response for non-existent email")
	}
}

// =============================================================================
// Verify Check Handler Tests
// =============================================================================

// TestVerifyCheck_ValidCode tests that a valid, non-expired, unused
// verification code successfully verifies a user's email and updates their
// account role from unverified_email to verified_email.
func TestVerifyCheck_ValidCode(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(VerifyCodeHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Create a verification code
	var code string
	expiresAt := clock.Now().Add(24 * time.Hour)
	err = tx.QueryRow(ctx,
		`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
		userID, expiresAt).Scan(&code)
	if err != nil {
		t.Fatalf("failed to create verification code: %v", err)
	}
	defer tx.Exec(ctx, "DELETE FROM verification_codes WHERE user_id = $1", userID)

	body := VerifyCodeRequest{
		Code: code,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp VerificationResponse
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if !resp.Success {
		t.Error("expected success=true in response")
	}

	// Verify code is marked as used
	var usedAt *time.Time
	err = tx.QueryRow(ctx,
		"SELECT used_at FROM verification_codes WHERE code = $1", code).Scan(&usedAt)
	if err != nil || usedAt == nil {
		t.Error("expected verification code to be marked as used")
	}
}

// TestVerifyCheck_InvalidCode verifies that attempting to verify with
// a non-existent code returns HTTP 400 Bad Request.
func TestVerifyCheck_InvalidCode(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(VerifyCodeHandler)

	body := VerifyCodeRequest{
		Code: "nonexistent-code-12345",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400
// when the code has expired (past its expires_at timestamp).
func TestVerifyCheck_ExpiredCode(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(VerifyCodeHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Create an expired verification code
	var code string
	expiresAt := clock.Now().Add(-1 * time.Hour) // Expired 1 hour ago
	err = tx.QueryRow(ctx,
		`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
		userID, expiresAt).Scan(&code)
	if err != nil {
		t.Fatalf("failed to create verification code: %v", err)
	}
	defer tx.Exec(ctx, "DELETE FROM verification_codes WHERE user_id = $1", userID)

	body := VerifyCodeRequest{
		Code: code,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Additional Edge Case Tests
// =============================================================================

// TestLogin_InvalidRequest verifies that sending malformed JSON to the login
// endpoint returns HTTP 400 Bad Request.
func TestLogin_InvalidRequest(t *testing.T) {
	_, _ = resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	// Send invalid JSON
	req := httptest.NewRequest("POST", "/api/login", bytes.NewReader([]byte("not json")))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d", w.Code)
	}
}

// TestRegister_NameTooLong tests that registration fails when the first name
// exceeds 50 characters (the maximum allowed length).
func TestRegister_NameTooLong(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// First name > 50 chars
	longName := string(bytes.Repeat([]byte("a"), 51))
	body := RegisterRequest{
		FirstName:      longName,
		LastName:       "Doe",
		Email:          "john@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestRegister_InvalidNameCharacters verifies that registration fails when
// names contain invalid characters (e.g., numbers).
func TestRegister_InvalidNameCharacters(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Name with numbers (invalid)
	body := RegisterRequest{
		FirstName:      "John123",
		LastName:       "Doe",
		Email:          "john@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code
// that has already been used returns HTTP 403 Forbidden.
func TestVerifyCheck_AlreadyUsed(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(VerifyCodeHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Create a verification code
	var code string
	expiresAt := clock.Now().Add(24 * time.Hour)
	err = tx.QueryRow(ctx,
		`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
		userID, expiresAt).Scan(&code)
	if err != nil {
		t.Fatalf("failed to create verification code: %v", err)
	}
	defer tx.Exec(ctx, "DELETE FROM verification_codes WHERE user_id = $1", userID)

	// First verification should succeed
	body := VerifyCodeRequest{
		Code: code,
	}
	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)
	if w.Code != http.StatusOK {
		t.Errorf("first verification: expected status 200, got %d", w.Code)
	}

	// Second verification with same code should return 403 (already used)
	w = testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)
	if w.Code != http.StatusForbidden {
		t.Errorf("second verification: expected status 403, got %d", w.Code)
	}
}

// TestVerifyCheck_RoleChangeToVerified confirms that after a successful
// verification, the user's account_role changes from unverified_email to
// verified_email, granting them full account access.
func TestVerifyCheck_RoleChangeToVerified(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(VerifyCodeHandler)

	// Create an unverified user
	userID, err := fixtures.CreateTestUnverifiedUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Verify initial role is unverified_email
	var initialRole string
	err = tx.QueryRow(ctx,
		"SELECT account_role FROM users WHERE id = $1", userID).Scan(&initialRole)
	if err != nil {
		t.Fatalf("failed to check initial role: %v", err)
	}
	if initialRole != "unverified_email" {
		t.Errorf("expected initial role 'unverified_email', got %s", initialRole)
	}

	// Create a verification code
	var code string
	expiresAt := clock.Now().Add(24 * time.Hour)
	err = tx.QueryRow(ctx,
		`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
		userID, expiresAt).Scan(&code)
	if err != nil {
		t.Fatalf("failed to create verification code: %v", err)
	}
	defer tx.Exec(ctx, "DELETE FROM verification_codes WHERE user_id = $1", userID)

	// Verify the code
	body := VerifyCodeRequest{
		Code: code,
	}
	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body, ctx)
	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	// Check that user's role changed to verified_email
	var newRole string
	err = tx.QueryRow(ctx,
		"SELECT account_role FROM users WHERE id = $1", userID).Scan(&newRole)
	if err != nil {
		t.Errorf("failed to check new role: %v", err)
	}
	if newRole != "verified_email" {
		t.Errorf("expected role to change to 'verified_email', got %s", newRole)
	}
}

// =============================================================================
// Password Length Tests (Registration)
// =============================================================================

// TestRegister_PasswordLength_Minimum verifies that registration enforces
// a minimum password length of 6 characters (new requirement from security pass).
// bcrypt handles passwords up to 72 chars internally (truncates longer ones).
func TestRegister_PasswordLength_Minimum(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	tests := []struct {
		name        string
		password    string
		expectError bool
	}{
		{
			name:        "1_char_password_too_short",
			password:    "x",
			expectError: true, // Below 6 char minimum
		},
		{
			name:        "5_char_password_too_short",
			password:    "short",
			expectError: true, // Below 6 char minimum
		},
		{
			name:        "6_char_password_minimum",
			password:    "pass12",
			expectError: false, // Meets minimum
		},
		{
			name:        "72_char_password_exact_bcrypt_limit",
			password:    strings.Repeat("a", 72),
			expectError: false,
		},
		{
			name:        "73_char_password_exceeds_limit",
			password:    strings.Repeat("a", 73),
			expectError: true, // Exceeds bcrypt limit
		},
		{
			name:        "100_char_password_exceeds_limit",
			password:    strings.Repeat("a", 100),
			expectError: true, // Exceeds bcrypt limit
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := RegisterRequest{
				FirstName:      "Test",
				LastName:       "User",
				Email:          fmt.Sprintf("test-%s@test.com", tt.name),
				Password:       tt.password,
				Phone:          "07123456789",
				DateOfBirth:    "1990-01-15",
				AgreedToPolicy: true,
			}

			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

			if tt.expectError {
				if w.Code == http.StatusCreated {
					t.Errorf("expected non-201 status for password '%s', got 201", tt.password)
				}
			} else {
				if w.Code != http.StatusCreated {
					t.Errorf("expected status 201 for password len=%d, got %d. body: %s", len(tt.password), w.Code, w.Body.String())
				}
			}
		})
	}
}

// TestRegister_EmptyPassword verifies that an empty password is rejected
// because it's a required field (not because of minimum length).
func TestRegister_EmptyPassword(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	body := RegisterRequest{
		FirstName:      "Test",
		LastName:       "User",
		Email:          "empty@test.com",
		Password:       "", // Empty - should fail as required field
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	// Empty password fails because it's a required field
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for empty password, got %d", w.Code)
	}
}

// TestRegister_WithValidReferralCode verifies that registration succeeds when
// a valid existing referral code is provided, and the referral relationship
// is recorded in the user_referrals table.
func TestRegister_WithValidReferralCode(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Create a referrer user with a known referral code
	referrerID, err := fixtures.CreateTestUserWithEmail(tx, "referrer@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create referrer user: %v", err)
	}
	defer fixtures.DeleteUser(tx, referrerID)

	// Set a known referral code for the referrer
	knownCode := "abc123def456"
	_, err = tx.Exec(ctx,
		"UPDATE users SET referral_code = $1 WHERE id = $2", knownCode, referrerID)
	if err != nil {
		t.Fatalf("failed to set referral code: %v", err)
	}

	body := RegisterRequest{
		FirstName:      "Referred",
		LastName:       "User",
		Email:          "referred@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
		ReferralCode:   knownCode,
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify referral relationship was created
	var referredID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM users WHERE email = $1", "referred@test.com").Scan(&referredID)
	if err != nil {
		t.Fatalf("failed to find referred user: %v", err)
	}
	defer fixtures.DeleteUser(tx, referredID)

	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM user_referrals WHERE referrer_id = $1 AND referred_id = $2",
		referrerID, referredID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to query user_referrals: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 referral record, got %d", count)
	}
}

// TestRegister_WithInvalidReferralCode verifies that registration fails with
// 400 when a non-existent referral code is provided.
func TestRegister_WithInvalidReferralCode(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	body := RegisterRequest{
		FirstName:      "Test",
		LastName:       "User",
		Email:          "invalid-referral@test.com",
		Password:       "password123",
		Phone:          "07123456789",
		DateOfBirth:    "1990-01-15",
		AgreedToPolicy: true,
		ReferralCode:   "nonexistent1234", // 12 chars but doesn't exist
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestRegister_WithInvalidReferralCodeFormat verifies that registration fails
// when the referral code is not exactly 12 characters.
func TestRegister_WithInvalidReferralCodeFormat(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	tests := []struct {
		name string
		code string
		desc string
	}{
		{"too_short", "abc123", "less than 12 chars"},
		{"too_long", "abc123def456ghi", "more than 12 chars"},
		{"special_chars", "abc123def4!!", "contains special chars"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := RegisterRequest{
				FirstName:      "Test",
				LastName:       "User",
				Email:          fmt.Sprintf("format-test-%s@test.com", tt.name),
				Password:       "password123",
				Phone:          "07123456789",
				DateOfBirth:    "1990-01-15",
				AgreedToPolicy: true,
				ReferralCode:   tt.code,
			}

			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

			if w.Code != http.StatusBadRequest {
				t.Errorf("expected status 400 for %s (%s), got %d. body: %s", tt.name, tt.desc, w.Code, w.Body.String())
			}
		})
	}
}

// TestRegister_ReferralCodeCaseInsensitive verifies that referral codes with
// uppercase letters are accepted and correctly matched against lowercase stored codes.
func TestRegister_ReferralCodeCaseInsensitive(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(RegisterHandler)

	// Create a referrer user with a known referral code (lowercase hex)
	referrerID, err := fixtures.CreateTestUserWithEmail(tx, "referrer-case@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create referrer user: %v", err)
	}
	defer fixtures.DeleteUser(tx, referrerID)

	knownCode := "abc123def456"
	_, err = tx.Exec(ctx,
		"UPDATE users SET referral_code = $1 WHERE id = $2", knownCode, referrerID)
	if err != nil {
		t.Fatalf("failed to set referral code: %v", err)
	}

	tests := []struct {
		name       string
		inputCode  string
		wantStatus int
	}{
		{"lowercase", "abc123def456", http.StatusCreated},
		{"uppercase", "ABC123DEF456", http.StatusCreated},
		{"mixed_case", "AbC123DeF456", http.StatusCreated},
		{"all_caps", "ABC123DEF456", http.StatusCreated},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := RegisterRequest{
				FirstName:      "Case",
				LastName:       "Test",
				Email:          fmt.Sprintf("case-test-%s@test.com", tt.name),
				Password:       "password123",
				Phone:          "07123456789",
				DateOfBirth:    "1990-01-15",
				AgreedToPolicy: true,
				ReferralCode:   tt.inputCode,
			}

			w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body, ctx)

			if w.Code != tt.wantStatus {
				t.Errorf("%s: expected status %d, got %d. body: %s", tt.name, tt.wantStatus, w.Code, w.Body.String())
			}

			if tt.wantStatus == http.StatusCreated {
				// Verify referral relationship was created
				var referredID string
				err = tx.QueryRow(ctx,
					"SELECT id FROM users WHERE email = $1", fmt.Sprintf("case-test-%s@test.com", tt.name)).Scan(&referredID)
				if err != nil {
					t.Fatalf("%s: failed to find referred user: %v", tt.name, err)
				}
				defer fixtures.DeleteUser(tx, referredID)

				var count int
				err = tx.QueryRow(ctx,
					"SELECT COUNT(*) FROM user_referrals WHERE referrer_id = $1 AND referred_id = $2",
					referrerID, referredID).Scan(&count)
				if err != nil {
					t.Fatalf("%s: failed to query user_referrals: %v", tt.name, err)
				}
				if count != 1 {
					t.Errorf("%s: expected 1 referral record, got %d", tt.name, count)
				}
			}
		})
	}
}

// =============================================================================
// Logout Handler Tests
// =============================================================================

// TestLogoutHandler_Success verifies that a valid logout request returns
// 200 OK with {"success": true} and revokes the JTI.
func TestLogoutHandler_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Generate a token and get its JTI
	token, jti, err := auth.GenerateToken(userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate token: %v", err)
	}

	// Create request with JTI in context (simulating RequireAuth middleware)
	req := httptest.NewRequest("POST", "/api/logout", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	reqCtx := context.WithValue(ctx, mw.JTIKey, jti)
	req = req.WithContext(reqCtx)
	w := httptest.NewRecorder()

	LogoutHandler(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp struct {
		Success bool `json:"success"`
	}
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}
	if !resp.Success {
		t.Error("expected success=true in response")
	}

	// Verify JTI was revoked
	if !auth.IsJTIRevoked(ctx, jti) {
		t.Error("expected JTI to be revoked after logout")
	}
}

// TestLogoutHandler_RevokesJTI verifies that after logout, the token's JTI is
// revoked and the token can no longer be used with authenticated endpoints.
func TestLogoutHandler_RevokesJTI(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Generate a token and get its JTI
	token, jti, err := auth.GenerateToken(userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate token: %v", err)
	}

	// Call logout with JTI in context
	req := httptest.NewRequest("POST", "/api/logout", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	reqCtx := context.WithValue(ctx, mw.JTIKey, jti)
	req = req.WithContext(reqCtx)
	w := httptest.NewRecorder()
	LogoutHandler(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("logout failed: %d. body: %s", w.Code, w.Body.String())
	}

	// Try to use the same token through RequireAuth middleware
	router := chi.NewRouter()
	router.With(mw.RequireAuth).Get("/api/protected", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	req2 := httptest.NewRequest("GET", "/api/protected", nil)
	req2 = req2.WithContext(ctx)
	req2.Header.Set("Authorization", "Bearer "+token)
	w2 := httptest.NewRecorder()
	router.ServeHTTP(w2, req2)

	if w2.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for revoked token, got %d", w2.Code)
	}
}

// TestLogoutHandler_NoToken verifies that calling logout without an
// Authorization header returns 401.
func TestLogoutHandler_NoToken(t *testing.T) {
	t.Parallel()
	_, _ = resetTestData(t)

	req := httptest.NewRequest("POST", "/api/logout", nil)
	w := httptest.NewRecorder()

	// Without JTI in context, handler returns 401
	LogoutHandler(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestLogoutHandler_InvalidToken verifies that calling logout with an empty
// JTI returns 401.
func TestLogoutHandler_InvalidToken(t *testing.T) {
	t.Parallel()
	_, _ = resetTestData(t)

	req := httptest.NewRequest("POST", "/api/logout", nil)
	reqCtx := context.WithValue(req.Context(), mw.JTIKey, "")
	req = req.WithContext(reqCtx)
	w := httptest.NewRecorder()

	LogoutHandler(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Refresh Token JTI Tests
// =============================================================================

// TestRefreshToken_RevokesOldJTI verifies that refreshing a token revokes the
// old JTI and issues a new one. The old token becomes invalid after refresh.
func TestRefreshToken_RevokesOldJTI(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a test user
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Generate initial token and JTI
	oldToken, oldJTI, err := auth.GenerateToken(userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate old token: %v", err)
	}

	// Verify old JTI is not yet revoked
	if auth.IsJTIRevoked(ctx, oldJTI) {
		t.Fatal("old JTI should not be revoked before refresh")
	}

	// Call refresh handler with old JTI in context
	req := httptest.NewRequest("POST", "/api/refresh-token", nil)
	req.Header.Set("Authorization", "Bearer "+oldToken)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, userID)
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email")
	reqCtx = context.WithValue(reqCtx, mw.JTIKey, oldJTI)
	req = req.WithContext(reqCtx)
	w := httptest.NewRecorder()

	RefreshTokenHandler(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("refresh failed: %d. body: %s", w.Code, w.Body.String())
	}

	// Verify old JTI was revoked
	if !auth.IsJTIRevoked(ctx, oldJTI) {
		t.Error("expected old JTI to be revoked after refresh")
	}

	// Verify old token is rejected by middleware
	router := chi.NewRouter()
	router.With(mw.RequireAuth).Get("/api/protected", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	req2 := httptest.NewRequest("GET", "/api/protected", nil)
	req2 = req2.WithContext(ctx)
	req2.Header.Set("Authorization", "Bearer "+oldToken)
	w2 := httptest.NewRecorder()
	router.ServeHTTP(w2, req2)

	if w2.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for revoked token after refresh, got %d", w2.Code)
	}
}

// =============================================================================
// Login Response JTI Tests
// =============================================================================

// TestLoginResponse_IncludesJTI verifies that the login response includes both
// "token" and "jti" fields, both non-empty.
func TestLoginResponse_IncludesJTI(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	// Create a test user with known email
	userID, err := fixtures.CreateTestUserWithEmail(tx, "jti-test@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	body := LoginRequest{
		Email:    "jti-test@test.com",
		Password: "testpassword123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp struct {
		Token string `json:"token"`
		JTI   string `json:"jti"`
	}
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Token == "" {
		t.Error("expected non-empty token in login response")
	}
	if resp.JTI == "" {
		t.Error("expected non-empty jti in login response")
	}
}

// =============================================================================
// LoginInProgress Rate Limiting Tests
// =============================================================================

// TestLoginInProgress_Cap verifies that when the loginInProgress map is full
// (20 concurrent logins), the 21st attempt returns HTTP 429 Too Many Requests.
func TestLoginInProgress_Cap(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	// Create a test user
	userID, err := fixtures.CreateTestUserWithEmail(tx, "ratelimit@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Fill the loginInProgress map with 20 entries
	loginStateMu.Lock()
	for i := 0; i < maxLoginInProgress; i++ {
		loginInProgress[fmt.Sprintf("stale-user-%d", i)] = clock.Now()
	}
	loginStateMu.Unlock()

	defer func() {
		// Clean up
		loginStateMu.Lock()
		for i := 0; i < maxLoginInProgress; i++ {
			delete(loginInProgress, fmt.Sprintf("stale-user-%d", i))
		}
		loginStateMu.Unlock()
	}()

	// Attempt login - should get 429
	body := LoginRequest{
		Email:    "ratelimit@test.com",
		Password: "testpassword123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusTooManyRequests {
		t.Errorf("expected status 429, got %d. body: %s", w.Code, w.Body.String())
	}
}

// ValidateUKPhoneNumber Security Tests
//
// These tests verify that ValidateUKPhoneNumber rejects or sanitises
// injection payloads (SQLi, XSS, command injection, control characters).

func TestValidateUKPhoneNumber_RejectsPureInjectionPayloads(t *testing.T) {
	t.Parallel()
	payloads := []string{
		// SQL injection
		"' OR '1'='1",
		"admin'--",
		`" OR 1=1 --`,
		"'; DROP TABLE users;--",
		// XSS
		"<script>alert(1)</script>",
		"<img src=x onerror=alert(1)>",
		"\"><script>alert(1)</script>",
		"javascript:alert(1)",
		// Command injection
		"; rm -rf /",
		"| cat /etc/passwd",
		"`id`",
		"$(cat /etc/passwd)",
		// Control characters
		"\n",
		"\r\n",
		"\x00",
	}
	for _, p := range payloads {
		result, err := ValidateUKPhoneNumber(p)
		if err == nil {
			t.Errorf("expected injection payload %q to be rejected, got result %q", p, result)
		}
	}
}

func TestValidateUKPhoneNumber_RejectsMixedInjectionPayloads(t *testing.T) {
	t.Parallel()
	// When injection characters are interleaved with a valid UK phone number,
	// libphonenumber rejects the entire input — it does NOT try to extract
	// digits from non-numeric characters. This is MORE secure than naive
	// digit-stripping approaches.
	payloads := []string{
		"' OR '1'='1 OR '+447700900000",
		"><script>+447700900000</script>",
		"'; rm -rf /; +447700900000",
		"\x00\n+447700900000",
	}
	for _, p := range payloads {
		result, err := ValidateUKPhoneNumber(p)
		if err == nil {
			t.Errorf("expected mixed payload %q to be rejected, got result %q", p, result)
		}
	}
}

// =============================================================================
// Account Lockout Tests (new from security pass)
// =============================================================================

// TestLogin_AccountLockout_After5Failures verifies that after 5 failed login
// attempts, the account is locked and the next login attempt returns HTTP 429.
func TestLogin_AccountLockout_After5Failures(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	userID, err := fixtures.CreateTestUserWithEmail(tx, "user@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)
	defer tx.Exec(ctx, "UPDATE users SET failed_attempts = 0, locked_until = NULL WHERE id = $1", userID)

	for i := 0; i < 5; i++ {
		body := LoginRequest{
			Email:    "user@test.com",
			Password: "wrongpassword",
		}
		w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)
		if w.Code != http.StatusUnauthorized {
			t.Fatalf("attempt %d: expected 401, got %d", i+1, w.Code)
		}
	}

	body := LoginRequest{
		Email:    "user@test.com",
		Password: "wrongpassword",
	}
	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)
	if w.Code != http.StatusTooManyRequests {
		t.Errorf("expected 429 after 5 failures, got %d. body: %s", w.Code, w.Body.String())
	}

	var failedAttempts int
	var lockedUntil *time.Time
	err = tx.QueryRow(ctx,
		"SELECT failed_attempts, locked_until FROM users WHERE id = $1", userID).Scan(&failedAttempts, &lockedUntil)
	if err != nil {
		t.Fatalf("failed to query lockout state: %v", err)
	}
	if failedAttempts < 5 {
		t.Errorf("expected >=5 failed attempts, got %d", failedAttempts)
	}
	if lockedUntil == nil {
		t.Error("expected locked_until to be set")
	}
}

// TestLogin_AccountLockout_ResetsOnSuccess verifies that a successful login
// resets the failed_attempts counter and clears the locked_until.
func TestLogin_AccountLockout_ResetsOnSuccess(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	userID, err := fixtures.CreateTestUserWithEmail(tx, "lockout-reset@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	_, err = tx.Exec(ctx,
		"UPDATE users SET failed_attempts = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set failed_attempts: %v", err)
	}

	body := LoginRequest{
		Email:    "lockout-reset@test.com",
		Password: "testpassword123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var failedAttempts int
	var lockedUntil *time.Time
	err = tx.QueryRow(ctx,
		"SELECT failed_attempts, locked_until FROM users WHERE id = $1", userID).Scan(&failedAttempts, &lockedUntil)
	if err != nil {
		t.Fatalf("failed to query lockout state: %v", err)
	}
	if failedAttempts != 0 {
		t.Errorf("expected 0 failed_attempts after success, got %d", failedAttempts)
	}
	if lockedUntil != nil {
		t.Error("expected locked_until to be cleared after success")
	}
}

// =============================================================================
// JWT Auth Unit Tests (new from security pass)
// =============================================================================

// TestJWT_ExpiryIsOneHour verifies that generated JWTs have a 1-hour expiry
// (changed from 30 days during security pass).
func TestJWT_ExpiryIsOneHour(t *testing.T) {
	t.Parallel()
	userID := "test-user-id"
	role := "verified_email"

	token, jti, err := auth.GenerateToken(userID, role)
	if err != nil {
		t.Fatalf("failed to generate token: %v", err)
	}
	if token == "" {
		t.Fatal("expected non-empty token")
	}
	if jti == "" {
		t.Fatal("expected non-empty jti")
	}

	retrievedUserID, retrievedRole, retrievedJTI, err := auth.VerifyToken(token, context.Background())
	if err != nil {
		t.Fatalf("failed to verify token: %v", err)
	}
	if retrievedUserID != userID {
		t.Errorf("expected userID %q, got %q", userID, retrievedUserID)
	}
	if retrievedRole != role {
		t.Errorf("expected role %q, got %q", role, retrievedRole)
	}
	if retrievedJTI != jti {
		t.Errorf("expected jti %q, got %q", jti, retrievedJTI)
	}
}

// TestRefreshToken_Generation verifies that a refresh token can be generated
// and stored in the database. This requires DB access.
func TestRefreshToken_Generation(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	refreshToken, err := auth.GenerateRefreshToken(ctx, userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate refresh token: %v", err)
	}
	if refreshToken == "" {
		t.Fatal("expected non-empty refresh token")
	}

	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refresh_tokens WHERE user_id = $1", userID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to query refresh_tokens: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 refresh_token, got %d", count)
	}

	retrievedUserID, retrievedRole, err := auth.VerifyRefreshToken(ctx, refreshToken)
	if err != nil {
		t.Fatalf("failed to verify refresh token: %v", err)
	}
	if retrievedUserID != userID {
		t.Errorf("expected userID %q, got %q", userID, retrievedUserID)
	}
	if retrievedRole != "verified_email" {
		t.Errorf("expected role 'verified_email', got %q", retrievedRole)
	}

	_, _, err = auth.VerifyRefreshToken(ctx, refreshToken)
	if err == nil {
		t.Error("expected error on second refresh token verification (rotated)")
	}
}

// TestJTI_Revocation_PostgreSQL verifies that JTI revocation uses the
// PostgreSQL revoked_jtis table and persists across operations.
func TestJTI_Revocation_PostgreSQL(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	token, jti, err := auth.GenerateToken(userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate token: %v", err)
	}

	if auth.IsJTIRevoked(ctx, jti) {
		t.Fatal("JTI should not be revoked before we revoke it")
	}

	_, _, _, err = auth.VerifyToken(token, ctx)
	if err != nil {
		t.Fatalf("token should be valid before revocation: %v", err)
	}

	auth.RevokeJTI(ctx, jti, clock.Now().Add(1*time.Hour))

	if !auth.IsJTIRevoked(ctx, jti) {
		t.Error("JTI should be revoked after RevokeJTI call")
	}

	_, _, _, err = auth.VerifyToken(token, ctx)
	if err == nil {
		t.Error("VerifyToken should fail for revoked JTI")
	}
}

// =============================================================================
// Login Response Field Tests (new from security pass)
// =============================================================================

// TestLogin_ResponseIncludesRefreshToken verifies the login response
// includes a refreshToken field alongside the JWT.
func TestLogin_ResponseIncludesRefreshToken(t *testing.T) {
	ctx, tx := resetTestData(t)

	handler := http.HandlerFunc(LoginHandler)

	userID, err := fixtures.CreateTestUserWithEmail(tx, "refresh-check@test.com", "verified_email")
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	body := LoginRequest{
		Email:    "refresh-check@test.com",
		Password: "testpassword123",
	}

	w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, ctx)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp struct {
		Token        string `json:"token"`
		JTI          string `json:"jti"`
		RefreshToken string `json:"refreshToken"`
	}
	if err := testutils.ParseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	if resp.Token == "" {
		t.Error("expected non-empty token")
	}
	if resp.JTI == "" {
		t.Error("expected non-empty jti")
	}
	if resp.RefreshToken == "" {
		t.Error("expected non-empty refreshToken in login response")
	}
}

// TestRefreshToken_RevokesOldJTI_DBBacked verifies refresh still revokes old
// JTI and the revocation is persisted in the revoked_jtis table.
func TestRefreshToken_RevokesOldJTI_DBBacked(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	oldToken, oldJTI, err := auth.GenerateToken(userID, "verified_email")
	if err != nil {
		t.Fatalf("failed to generate old token: %v", err)
	}

	if auth.IsJTIRevoked(ctx, oldJTI) {
		t.Fatal("old JTI should not be revoked before refresh")
	}

	req := httptest.NewRequest("POST", "/api/refresh-token", nil)
	req.Header.Set("Authorization", "Bearer "+oldToken)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, userID)
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email")
	reqCtx = context.WithValue(reqCtx, mw.JTIKey, oldJTI)
	req = req.WithContext(reqCtx)
	w := httptest.NewRecorder()

	RefreshTokenHandler(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("refresh failed: %d. body: %s", w.Code, w.Body.String())
	}

	if !auth.IsJTIRevoked(ctx, oldJTI) {
		t.Error("expected old JTI to be revoked after refresh (DB-backed)")
	}

	var dbCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM revoked_jtis WHERE jti = $1 AND expires_at > NOW()", oldJTI).Scan(&dbCount)
	if err != nil {
		t.Fatalf("failed to query revoked_jtis: %v", err)
	}
	if dbCount != 1 {
		t.Errorf("expected 1 revoked_jtis row, got %d", dbCount)
	}
}

// ============================================================
// CleanupStaleLoginEntries Tests
// ============================================================

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

	loginStateMu.Lock()
	saved := loginInProgress
	loginInProgress = make(map[string]time.Time)
	loginStateMu.Unlock()
	defer func() {
		loginStateMu.Lock()
		loginInProgress = saved
		loginStateMu.Unlock()
	}()

	_, err := CleanupStaleLoginEntries(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}
}

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

	loginStateMu.Lock()
	saved := loginInProgress
	loginInProgress = map[string]time.Time{
		"stale-user": clock.Now().Add(-60 * time.Second),
	}
	loginStateMu.Unlock()
	defer func() {
		loginStateMu.Lock()
		loginInProgress = saved
		loginStateMu.Unlock()
	}()

	_, err := CleanupStaleLoginEntries(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	loginStateMu.Lock()
	_, exists := loginInProgress["stale-user"]
	deleted := loginInProgress["stale-user"]
	loginStateMu.Unlock()
	if exists {
		t.Errorf("expected stale entry (60s old) to be removed, got %v", deleted)
	}
}

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

	loginStateMu.Lock()
	saved := loginInProgress
	loginInProgress = map[string]time.Time{
		"recent-user": clock.Now().Add(-5 * time.Second),
	}
	loginStateMu.Unlock()
	defer func() {
		loginStateMu.Lock()
		loginInProgress = saved
		loginStateMu.Unlock()
	}()

	_, err := CleanupStaleLoginEntries(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	loginStateMu.Lock()
	_, exists := loginInProgress["recent-user"]
	loginStateMu.Unlock()
	if !exists {
		t.Error("expected recent entry (5s old) to be preserved")
	}
}

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

	loginStateMu.Lock()
	saved := loginInProgress
	loginInProgress = map[string]time.Time{
		"stale-user":  clock.Now().Add(-60 * time.Second),
		"recent-user": clock.Now().Add(-5 * time.Second),
		"borderline":  clock.Now().Add(-29 * time.Second), // Just under 30s threshold
	}
	loginStateMu.Unlock()
	defer func() {
		loginStateMu.Lock()
		loginInProgress = saved
		loginStateMu.Unlock()
	}()

	_, err := CleanupStaleLoginEntries(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	loginStateMu.Lock()
	_, staleExists := loginInProgress["stale-user"]
	_, recentExists := loginInProgress["recent-user"]
	_, borderlineExists := loginInProgress["borderline"]
	loginStateMu.Unlock()

	if staleExists {
		t.Error("expected stale-user (60s old) to be removed")
	}
	if !recentExists {
		t.Error("expected recent-user (5s old) to be preserved")
	}
	if !borderlineExists {
		t.Error("expected borderline entry (29s old) to be preserved")
	}
}

// Ensure test compilation - import pgxpool to avoid unused import
var _ = func() *pgxpool.Pool { return nil }
