package auth_test

import (
	"net/http"
	"testing"
	"time"

	"github.com/owncord/server/auth"
	"github.com/owncord/server/db"
)

// ─── ExtractBearerToken ───────────────────────────────────────────────────────

func TestExtractBearerToken_ValidHeader(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Bearer mytoken123")

	token, ok := auth.ExtractBearerToken(r)

	if !ok {
		t.Fatal("ExtractBearerToken() ok = false, want true")
	}
	if token != "mytoken123" {
		t.Errorf("ExtractBearerToken() token = %q, want %q", token, "mytoken123")
	}
}

func TestExtractBearerToken_MissingHeader(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)

	token, ok := auth.ExtractBearerToken(r)

	if ok {
		t.Error("ExtractBearerToken() ok = true with no Authorization header, want false")
	}
	if token != "" {
		t.Errorf("ExtractBearerToken() token = %q, want empty string", token)
	}
}

func TestExtractBearerToken_EmptyHeaderValue(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "")

	_, ok := auth.ExtractBearerToken(r)

	if ok {
		t.Error("ExtractBearerToken() ok = true for empty header value, want false")
	}
}

func TestExtractBearerToken_WrongScheme(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Basic dXNlcjpwYXNz")

	_, ok := auth.ExtractBearerToken(r)

	if ok {
		t.Error("ExtractBearerToken() ok = true for Basic scheme, want false")
	}
}

func TestExtractBearerToken_BearerCaseInsensitive(t *testing.T) {
	cases := []string{
		"BEARER mytoken",
		"bearer mytoken",
		"Bearer mytoken",
		"bEaReR mytoken",
	}
	for _, authHeader := range cases {
		r, _ := http.NewRequest(http.MethodGet, "/", nil)
		r.Header.Set("Authorization", authHeader)

		token, ok := auth.ExtractBearerToken(r)

		if !ok {
			t.Errorf("ExtractBearerToken() ok = false for header %q, want true", authHeader)
		}
		if token != "mytoken" {
			t.Errorf("ExtractBearerToken() token = %q for header %q, want %q", token, authHeader, "mytoken")
		}
	}
}

func TestExtractBearerToken_BearerWithNoToken(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Bearer ")

	_, ok := auth.ExtractBearerToken(r)

	if ok {
		t.Error("ExtractBearerToken() ok = true for 'Bearer ' with empty token, want false")
	}
}

func TestExtractBearerToken_OnlySchemeNoSpace(t *testing.T) {
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Bearer")

	_, ok := auth.ExtractBearerToken(r)

	if ok {
		t.Error("ExtractBearerToken() ok = true for 'Bearer' with no space or token, want false")
	}
}

func TestExtractBearerToken_TokenPreservesValue(t *testing.T) {
	// Tokens can contain mixed-case, digits, hyphens, underscores, dots.
	rawToken := "aB3-xY9_zZ0.qQ7"
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Bearer "+rawToken)

	token, ok := auth.ExtractBearerToken(r)

	if !ok {
		t.Fatal("ExtractBearerToken() ok = false, want true")
	}
	if token != rawToken {
		t.Errorf("ExtractBearerToken() token = %q, want %q", token, rawToken)
	}
}

func TestExtractBearerToken_MultipleSpaces(t *testing.T) {
	// SplitN with n=2 means "Bearer  tok" splits into ["Bearer", " tok"].
	// The implementation trims whitespace, so " mytoken" becomes "mytoken".
	r, _ := http.NewRequest(http.MethodGet, "/", nil)
	r.Header.Set("Authorization", "Bearer  mytoken")

	token, ok := auth.ExtractBearerToken(r)

	// The implementation applies TrimSpace to the extracted token,
	// so the leading space from the double-space header is stripped.
	if !ok {
		t.Fatal("ExtractBearerToken() ok = false for double-space header, want true")
	}
	if token != "mytoken" {
		t.Errorf("ExtractBearerToken() token = %q, want %q", token, "mytoken")
	}
}

// ─── IsSessionExpired ─────────────────────────────────────────────────────────

func TestIsSessionExpired_FutureTimeNotExpired(t *testing.T) {
	future := time.Now().UTC().Add(time.Hour)
	expiresAt := future.Format("2006-01-02 15:04:05")

	if auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = true for future time, want false", expiresAt)
	}
}

func TestIsSessionExpired_PastTimeExpired(t *testing.T) {
	past := time.Now().UTC().Add(-time.Hour)
	expiresAt := past.Format("2006-01-02 15:04:05")

	if !auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = false for past time, want true", expiresAt)
	}
}

func TestIsSessionExpired_FutureTimeSQLiteFormat(t *testing.T) {
	future := time.Now().UTC().Add(24 * time.Hour)
	expiresAt := future.Format("2006-01-02 15:04:05")

	if auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = true for future SQLite-format time, want false", expiresAt)
	}
}

func TestIsSessionExpired_PastTimeSQLiteFormat(t *testing.T) {
	past := time.Now().UTC().Add(-24 * time.Hour)
	expiresAt := past.Format("2006-01-02 15:04:05")

	if !auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = false for past SQLite-format time, want true", expiresAt)
	}
}

func TestIsSessionExpired_FutureTimeISO8601Format(t *testing.T) {
	future := time.Now().UTC().Add(time.Hour)
	expiresAt := future.Format("2006-01-02T15:04:05Z")

	if auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = true for future ISO-8601 time, want false", expiresAt)
	}
}

func TestIsSessionExpired_PastTimeISO8601Format(t *testing.T) {
	past := time.Now().UTC().Add(-time.Hour)
	expiresAt := past.Format("2006-01-02T15:04:05Z")

	if !auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = false for past ISO-8601 time, want true", expiresAt)
	}
}

func TestIsSessionExpired_EmptyString(t *testing.T) {
	// Unparseable — must treat as expired for safety.
	if !auth.IsSessionExpired("") {
		t.Error("IsSessionExpired(\"\") = false for empty string, want true (fail-safe)")
	}
}

func TestIsSessionExpired_InvalidFormat(t *testing.T) {
	cases := []string{
		"not-a-date",
		"2025/03/15 12:00:00",
		"15-03-2025",
		"2025-13-45T99:99:99Z", // out-of-range values
	}
	for _, s := range cases {
		if !auth.IsSessionExpired(s) {
			t.Errorf("IsSessionExpired(%q) = false for invalid format, want true (fail-safe)", s)
		}
	}
}

func TestIsSessionExpired_ExactlyNow(t *testing.T) {
	// A timestamp one second in the past must always be expired.
	justPast := time.Now().UTC().Add(-time.Second)
	expiresAt := justPast.Format("2006-01-02 15:04:05")

	if !auth.IsSessionExpired(expiresAt) {
		t.Errorf("IsSessionExpired(%q) = false for just-past time, want true", expiresAt)
	}
}

// ─── IsEffectivelyBanned ──────────────────────────────────────────────────────

func TestIsEffectivelyBanned_NotBanned(t *testing.T) {
	u := &db.User{Banned: false}
	if auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=false) = true, want false")
	}
}

func TestIsEffectivelyBanned_BannedNilExpiry(t *testing.T) {
	// Banned with no expiry — permanently banned.
	u := &db.User{Banned: true, BanExpires: nil}
	if !auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, BanExpires=nil) = false, want true")
	}
}

func TestIsEffectivelyBanned_BannedFutureExpiry(t *testing.T) {
	// Banned with an expiry in the future — still banned.
	future := time.Now().UTC().Add(time.Hour).Format("2006-01-02 15:04:05")
	u := &db.User{Banned: true, BanExpires: new(future)}
	if !auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, future expiry) = false, want true")
	}
}

func TestIsEffectivelyBanned_BannedPastExpiry(t *testing.T) {
	// Banned but the ban expired in the past — should be treated as NOT banned.
	past := time.Now().UTC().Add(-time.Hour).Format("2006-01-02 15:04:05")
	u := &db.User{Banned: true, BanExpires: new(past)}
	if auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, past expiry) = true, want false")
	}
}

func TestIsEffectivelyBanned_BannedExpiredISO8601(t *testing.T) {
	// ISO-8601 format for BanExpires past — should be treated as NOT banned.
	past := time.Now().UTC().Add(-time.Minute).Format("2006-01-02T15:04:05Z")
	u := &db.User{Banned: true, BanExpires: new(past)}
	if auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, ISO-8601 past expiry) = true, want false")
	}
}

func TestIsEffectivelyBanned_BannedFutureISO8601(t *testing.T) {
	// ISO-8601 format for BanExpires in future — still banned.
	future := time.Now().UTC().Add(time.Hour).Format("2006-01-02T15:04:05Z")
	u := &db.User{Banned: true, BanExpires: new(future)}
	if !auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, ISO-8601 future expiry) = false, want true")
	}
}

func TestIsEffectivelyBanned_BannedUnparsableExpiry(t *testing.T) {
	// Unparseable expiry string — fail-safe: treat as still banned.
	u := &db.User{Banned: true, BanExpires: new("not-a-date")}
	if !auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=true, unparseable expiry) = false, want true (fail-safe)")
	}
}

func TestIsEffectivelyBanned_NotBannedIgnoresExpiry(t *testing.T) {
	// Banned=false even with a future expiry field — should be false.
	future := time.Now().UTC().Add(time.Hour).Format("2006-01-02 15:04:05")
	u := &db.User{Banned: false, BanExpires: new(future)}
	if auth.IsEffectivelyBanned(u) {
		t.Error("IsEffectivelyBanned(Banned=false, future expiry) = true, want false")
	}
}

func TestIsEffectivelyBanned_NilUser(t *testing.T) {
	// A nil user pointer must not panic and must return false.
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("IsEffectivelyBanned(nil) panicked: %v", r)
		}
	}()
	if auth.IsEffectivelyBanned(nil) {
		t.Error("IsEffectivelyBanned(nil) = true, want false")
	}
}

// ─── ValidateUsername ────────────────────────────────────────────────────────

func TestValidateUsername_ValidNames(t *testing.T) {
	cases := []string{
		"ab",                               // minimum length (2 runes)
		"alice",                            // normal ASCII
		"user_name",                        // with underscore
		"日本語ユーザー",                          // CJK (multi-byte runes)
		"abcdefghijklmnopqrstuvwxyz123456", // exactly 32 chars
	}
	for _, name := range cases {
		if err := auth.ValidateUsername(name); err != nil {
			t.Errorf("ValidateUsername(%q) = %v, want nil", name, err)
		}
	}
}

// TestValidateUsername_ReservedDeletedNamespace locks the anonymisation
// namespace shut. db.DeleteAccount renames a deleted account to
// "[deleted-<id>]" in a UNIQUE COLLATE NOCASE column, so while the name was
// registrable a member could take a chosen victim's and make every subsequent
// account-deletion attempt fail on the unique index.
func TestValidateUsername_ReservedDeletedNamespace(t *testing.T) {
	reserved := []string{
		"[deleted-42]",        // the exact anonymised form
		"[DELETED-42]",        // UNIQUE is COLLATE NOCASE, so case must not help
		"[Deleted-1]",         // mixed case
		"[deleted-42-a3f19c]", // the collision-fallback form
		"[deleted-]",          // the namespace, not just the numeric form
	}
	for _, name := range reserved {
		if err := auth.ValidateUsername(name); err == nil {
			t.Errorf("ValidateUsername(%q) = nil, want error for reserved namespace", name)
		}
	}

	// Names that merely resemble it stay available.
	allowed := []string{
		"[deleted",        // no closing bracket
		"deleted-42]",     // no opening bracket
		"[not-deleted-1]", // different namespace
		"[cool]",          // ordinary bracketed name
	}
	for _, name := range allowed {
		if err := auth.ValidateUsername(name); err != nil {
			t.Errorf("ValidateUsername(%q) = %v, want nil", name, err)
		}
	}
}

func TestValidateUsername_TooShort(t *testing.T) {
	cases := []string{
		"",  // empty
		"a", // single char
	}
	for _, name := range cases {
		if err := auth.ValidateUsername(name); err == nil {
			t.Errorf("ValidateUsername(%q) = nil, want error for too short", name)
		}
	}
}

func TestValidateUsername_TooLong(t *testing.T) {
	// 33 runes exceeds the 32-rune limit.
	long := "abcdefghijklmnopqrstuvwxyz1234567"
	if err := auth.ValidateUsername(long); err == nil {
		t.Errorf("ValidateUsername(%q) = nil, want error for too long", long)
	}
}

func TestValidateUsername_ControlCharactersRejected(t *testing.T) {
	cases := []string{
		"user\x00name", // null byte
		"user\nname",   // newline
		"user\tname",   // tab
		"abc\x07def",   // bell
	}
	for _, name := range cases {
		if err := auth.ValidateUsername(name); err == nil {
			t.Errorf("ValidateUsername(%q) = nil, want error for control char", name)
		}
	}
}

func TestValidateUsername_InvisibleCharactersRejected(t *testing.T) {
	// Zero-width joiner (U+200D) is in unicode.Cf category.
	name := "user\u200Dname"
	if err := auth.ValidateUsername(name); err == nil {
		t.Errorf("ValidateUsername(%q) = nil, want error for invisible character", name)
	}

	// Zero-width space (U+200B).
	name2 := "user\u200Bname"
	if err := auth.ValidateUsername(name2); err == nil {
		t.Errorf("ValidateUsername(%q) = nil, want error for zero-width space", name2)
	}
}

func TestValidateUsername_WhitespaceTrimmed(t *testing.T) {
	// Leading/trailing whitespace is trimmed, so "  a  " becomes "a" (1 rune = too short).
	if err := auth.ValidateUsername("  a  "); err == nil {
		t.Error("ValidateUsername(\"  a  \") = nil, want error (trimmed to 1 rune)")
	}

	// After trimming, "  ab  " becomes "ab" (2 runes = valid).
	if err := auth.ValidateUsername("  ab  "); err != nil {
		t.Errorf("ValidateUsername(\"  ab  \") = %v, want nil (trimmed to 2 runes)", err)
	}
}

func TestValidateUsername_UnicodeLength(t *testing.T) {
	// Each emoji is 1 rune but multiple bytes. 2 emoji should be valid (min length).
	twoEmoji := "😀😀"
	if err := auth.ValidateUsername(twoEmoji); err != nil {
		t.Errorf("ValidateUsername(%q) = %v, want nil for 2-rune emoji name", twoEmoji, err)
	}
}
