package api

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"sync"
	"testing"

	"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
)

// resetUsers wipes the entire users table (including any non-prefixed admin
// row). Used only by tests that need the empty-users bootstrap path; normal
// tests call seedUser which uses the TestUserPrefix prefix and is cleaned up
// by dbtest.ResetDB inside testHandlers.
func resetUsers(t *testing.T, h *handlers) {
	t.Helper()
	if _, err := h.pool.Exec(context.Background(), "DELETE FROM users"); err != nil {
		t.Fatalf("resetUsers: %v", err)
	}
}

// resetInvites wipes the user_invites table. Called by tests that seed their
// own invites so subsequent runs start clean.
func resetInvites(t *testing.T, h *handlers) {
	t.Helper()
	if _, err := h.pool.Exec(context.Background(), "DELETE FROM user_invites"); err != nil {
		t.Fatalf("resetInvites: %v", err)
	}
}

func TestRegister_FirstUserBecomesAdmin(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, _ := testHandlers(t)
	resetUsers(t, h)

	body := `{"username":"firstuser","password":"abcd1234"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	// Verify is_admin = true on the inserted row.
	var isAdmin bool
	if err := h.pool.QueryRow(context.Background(),
		"SELECT is_admin FROM users WHERE username = 'firstuser'").Scan(&isAdmin); err != nil {
		t.Fatalf("verify: %v", err)
	}
	if !isAdmin {
		t.Errorf("is_admin = false; first registered user must be admin")
	}
	// Cookie set?
	gotCookie := false
	for _, c := range rec.Result().Cookies() {
		if c.Name == auth.SessionCookieName {
			gotCookie = true
		}
	}
	if !gotCookie {
		t.Errorf("session cookie not set")
	}
}

func TestRegister_InviteOnlyMode_RequiresToken(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	// Seed an admin user so we're past the empty-users state.
	seedUser(t, pool, "existingadmin", "pw", true)
	// Mode is default invite_only.

	body := `{"username":"newuser","password":"abcd1234"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400 (invite_required)", rec.Code)
	}
}

func TestRegister_InviteOnlyMode_ValidTokenSucceeds(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	resetInvites(t, h)
	admin := seedUser(t, pool, "admin1", "pw", true)
	// Insert a fresh invite directly.
	token := "test-invite-abc123"
	if _, err := pool.Exec(context.Background(),
		`INSERT INTO user_invites (token, invited_by, expires_at)
		 VALUES ($1, $2, now() + interval '24 hours')`,
		token, admin.ID,
	); err != nil {
		t.Fatalf("seed invite: %v", err)
	}

	body := `{"username":"invitee1","password":"abcd1234","invite_token":"` + token + `"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	// Invite redeemed?
	var redeemedAt *string
	if err := pool.QueryRow(context.Background(),
		`SELECT redeemed_at::text FROM user_invites WHERE token = $1`, token,
	).Scan(&redeemedAt); err != nil {
		t.Fatalf("verify redeem: %v", err)
	}
	if redeemedAt == nil {
		t.Errorf("invite not redeemed after successful registration")
	}
	// New user is_admin = false?
	var isAdmin bool
	if err := pool.QueryRow(context.Background(),
		`SELECT is_admin FROM users WHERE username = 'invitee1'`).Scan(&isAdmin); err != nil {
		t.Fatalf("verify user: %v", err)
	}
	if isAdmin {
		t.Errorf("new invitee marked as admin; only first user should be admin")
	}
}

func TestRegister_InviteOnlyMode_InvalidTokenRejects(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	seedUser(t, pool, "adm", "pw", true)

	body := `{"username":"baduser","password":"abcd1234","invite_token":"nonexistent-token"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400 (invite_invalid)", rec.Code)
	}
}

func TestRegister_OpenMode_NoTokenNeeded(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	seedUser(t, pool, "preexisting", "pw", true)
	// Switch to open mode; restore invite_only afterwards so other tests are
	// not affected (registration_settings is a singleton not truncated by ResetDB).
	if _, err := pool.Exec(context.Background(),
		"UPDATE registration_settings SET mode = 'open' WHERE id = true"); err != nil {
		t.Fatalf("set mode: %v", err)
	}
	t.Cleanup(func() {
		if _, err := pool.Exec(context.Background(),
			"UPDATE registration_settings SET mode = 'invite_only' WHERE id = true"); err != nil {
			t.Logf("cleanup: restore mode failed: %v", err)
		}
	})

	body := `{"username":"openreg","password":"abcd1234"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
}

func TestRegister_DuplicateUsername_409(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	resetUsers(t, h)
	// Switch to open mode so the second attempt is not rejected by invite
	// check before it can reach the INSERT and hit the unique violation.
	if _, err := pool.Exec(context.Background(),
		"UPDATE registration_settings SET mode = 'open' WHERE id = true"); err != nil {
		t.Fatalf("set mode: %v", err)
	}
	t.Cleanup(func() {
		if _, err := pool.Exec(context.Background(),
			"UPDATE registration_settings SET mode = 'invite_only' WHERE id = true"); err != nil {
			t.Logf("cleanup: restore mode failed: %v", err)
		}
	})

	// First register succeeds (empty-users state → first-admin path).
	body := `{"username":"taken","password":"abcd1234"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	h.handleRegister(httptest.NewRecorder(), req)

	// Second register with same username in open mode → 409 (unique violation).
	req2 := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec2 := httptest.NewRecorder()
	h.handleRegister(rec2, req2)

	if rec2.Code != http.StatusConflict {
		t.Errorf("status = %d, want 409 (username_taken)", rec2.Code)
	}
}

func TestRegister_PasswordTooShort_400(t *testing.T) {
	h, _ := testHandlers(t)
	body := `{"username":"shortpw","password":"abc"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400", rec.Code)
	}
}

func TestRegister_UsernameInvalid_400(t *testing.T) {
	h, _ := testHandlers(t)
	body := `{"username":"has spaces","password":"abcd1234"}`
	req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
	rec := httptest.NewRecorder()
	h.handleRegister(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400", rec.Code)
	}
}

func TestRegister_FirstAdminRace(t *testing.T) {
	// Concurrent registrations from empty-users state. Both
	// usernames are different, so users.username uniqueness doesn't
	// arbitrate. Both will see "no users yet" and both insert as
	// admin. Test asserts the at-least-one-admin invariant.
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, _ := testHandlers(t)
	resetUsers(t, h)

	const N = 5
	var wg sync.WaitGroup
	for i := 0; i < N; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			body := json.RawMessage(`{"username":"raceuser` + string(rune('A'+idx)) + `","password":"abcd1234"}`)
			req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
			rec := httptest.NewRecorder()
			h.handleRegister(rec, req)
		}(i)
	}
	wg.Wait()

	var adminCount int
	if err := h.pool.QueryRow(context.Background(),
		`SELECT count(*) FROM users WHERE is_admin = true`).Scan(&adminCount); err != nil {
		t.Fatalf("count: %v", err)
	}
	if adminCount < 1 {
		t.Errorf("admin count = 0; expected at least 1 (race-safety invariant violated)")
	}
	// adminCount can be > 1 — that's fine. The invariant is "at least one,"
	// not "exactly one" (which would require serializable isolation).
	t.Logf("admin count after race = %d (>=1 is the invariant)", adminCount)
}
