package auth

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"kjol/security"

	"github.com/google/uuid"
)

// ---- fakes -----------------------------------------------------------------

type fakeStore struct {
	mu       sync.Mutex
	sessions map[string]Session
	inserted []Session
	limits   []int // limit passed to each EnforceSessionLimit call
	revoked  []string
}

func newFakeStore() *fakeStore {
	return &fakeStore{sessions: map[string]Session{}}
}

func (s *fakeStore) FetchByKey(_ context.Context, key string) (Session, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	session, ok := s.sessions[key]
	if !ok {
		return Session{}, ErrSessionNotFound
	}
	return session, nil
}

func (s *fakeStore) Insert(_ context.Context, session Session) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.inserted = append(s.inserted, session) // exactly as the engine handed it over

	stored := session
	stored.ID = uuid.New() // stand in for the database default
	s.sessions[session.Key] = stored
	return nil
}

func (s *fakeStore) Revoke(_ context.Context, key string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	session, ok := s.sessions[key]
	if !ok {
		return ErrSessionNotFound
	}
	session.Revoked = true
	s.sessions[key] = session
	s.revoked = append(s.revoked, key)
	return nil
}

func (s *fakeStore) EnforceSessionLimit(_ context.Context, _ uuid.UUID, limit int) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.limits = append(s.limits, limit)
	return nil
}

// put installs a session directly, bypassing login.
func (s *fakeStore) put(session Session) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.sessions[session.Key] = session
}

type fakeDirectory struct {
	mu       sync.Mutex
	users    map[string]User
	outcomes []bool // one per RecordLoginResult call
}

func (d *fakeDirectory) FindByIdentifier(_ context.Context, identifier string) (User, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	u, ok := d.users[identifier]
	if !ok {
		return User{}, ErrUserNotFound
	}
	return u, nil
}

func (d *fakeDirectory) RecordLoginResult(_ context.Context, _ User, success bool) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.outcomes = append(d.outcomes, success)
	return nil
}

// fakeResolver hands back a fixed permission set, cloned per call so the engine
// can't mutate the fixture out from under a later test.
type fakeResolver struct{ perms map[string]bool }

func (r fakeResolver) Resolve(_ context.Context, _ Session) map[string]bool {
	out := make(map[string]bool, len(r.perms))
	for k, v := range r.perms {
		out[k] = v
	}
	return out
}

// ---- harness ---------------------------------------------------------------

const (
	testPassword = "correct-horse"
	testUsername = "mta"
)

type harness struct {
	auth *Authenticator
	str  *fakeStore
	dir  *fakeDirectory
	user User
}

func newHarness(t *testing.T, mutate ...func(*Config)) *harness {
	t.Helper()

	hash, err := security.HashPassword(testPassword)
	if err != nil {
		t.Fatalf("hash password: %v", err)
	}

	orgID := uuid.New()
	user := User{
		ID:           uuid.New(),
		PasswordHash: hash,
		Timezone:     "UTC",
		OrgID:        &orgID,
	}

	cfg := Config{
		CookieName:        "test_identity",
		LoginPath:         "/auth/login",
		LogoutPath:        "/auth/logout",
		DefaultPath:       "/app/dashboard",
		Redirect:          true,
		SessionTTL:        24 * time.Hour,
		KeyBytes:          32,
		MaxActiveSessions: 5,
		MaxLoginAttempts:  25,
		SuperPermission:   "*",
	}
	for _, m := range mutate {
		m(&cfg)
	}

	str := newFakeStore()
	dir := &fakeDirectory{users: map[string]User{testUsername: user}}

	return &harness{
		auth: New(cfg, str, dir, fakeResolver{perms: map[string]bool{"read": true}}),
		str:  str,
		dir:  dir,
		user: user,
	}
}

// okHandler records that the request made it through the middleware.
func okHandler(served *bool) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		*served = true
		w.WriteHeader(http.StatusOK)
	}
}

// ---- login -----------------------------------------------------------------

func TestLoginSuccess(t *testing.T) {
	h := newHarness(t)

	principal, key, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{
		IPAddr: "10.0.0.1", UserAgent: "test-agent",
	})
	if err != nil {
		t.Fatalf("Login: %v", err)
	}

	if !principal.Authenticated {
		t.Error("principal is not authenticated")
	}
	if key == "" || principal.Session.Key != key {
		t.Errorf("session key not returned: key=%q session.Key=%q", key, principal.Session.Key)
	}
	if principal.Session.UserID != h.user.ID {
		t.Errorf("UserID = %v, want %v", principal.Session.UserID, h.user.ID)
	}
	if principal.Session.OrgID == nil || *principal.Session.OrgID != *h.user.OrgID {
		t.Error("session did not inherit the user's org")
	}
	if principal.Session.IPAddr != "10.0.0.1" || principal.Session.UserAgent != "test-agent" {
		t.Error("session did not record the request metadata")
	}
	if !principal.Permissions["read"] {
		t.Error("permissions were not resolved onto the principal")
	}
	if got, want := principal.Session.Expiration, time.Now().Add(24*time.Hour); got.Sub(want).Abs() > time.Minute {
		t.Errorf("Expiration = %v, want ~%v", got, want)
	}

	if len(h.str.inserted) != 1 {
		t.Fatalf("inserted %d sessions, want 1", len(h.str.inserted))
	}
	// The store (or its database default) owns ID assignment: cdrateline's
	// InsertIdentity omits the id column entirely, so an engine-invented ID
	// would be silently dropped and disagree with the stored row.
	if h.str.inserted[0].ID != uuid.Nil {
		t.Errorf("engine assigned session ID %v; that is the store's job", h.str.inserted[0].ID)
	}
	if len(h.dir.outcomes) != 1 || !h.dir.outcomes[0] {
		t.Errorf("login outcomes = %v, want [true]", h.dir.outcomes)
	}
	if len(h.str.limits) != 1 || h.str.limits[0] != 5 {
		t.Errorf("session limits enforced = %v, want [5]", h.str.limits)
	}
}

func TestLoginRejectsBadPasswordAndRecordsFailure(t *testing.T) {
	h := newHarness(t)

	_, _, err := h.auth.Login(t.Context(), testUsername, "wrong", SessionMeta{})
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v, want ErrInvalidCredentials", err)
	}
	if len(h.str.inserted) != 0 {
		t.Error("a session was created for a bad password")
	}
	if len(h.dir.outcomes) != 1 || h.dir.outcomes[0] {
		t.Errorf("login outcomes = %v, want [false]", h.dir.outcomes)
	}
}

func TestLoginRejectsUnknownUserIndistinguishably(t *testing.T) {
	h := newHarness(t)

	_, _, err := h.auth.Login(t.Context(), "nobody", testPassword, SessionMeta{})
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v, want ErrInvalidCredentials", err)
	}
	// Nothing to record against: the user does not exist.
	if len(h.dir.outcomes) != 0 {
		t.Errorf("recorded a login outcome for a nonexistent user: %v", h.dir.outcomes)
	}
}

func TestLoginLocksOutAfterMaxAttempts(t *testing.T) {
	h := newHarness(t, func(c *Config) { c.MaxLoginAttempts = 3 })

	locked := h.user
	locked.FailedLoginAttempts = 4 // past the cap
	h.dir.users[testUsername] = locked

	// The correct password must not open a locked-out account.
	_, _, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{})
	if err != ErrInvalidCredentials {
		t.Fatalf("err = %v, want ErrInvalidCredentials for a locked-out account", err)
	}
	if len(h.str.inserted) != 0 {
		t.Error("a locked-out account was issued a session")
	}
}

func TestLoginLockoutDisabledWhenMaxAttemptsUnset(t *testing.T) {
	h := newHarness(t, func(c *Config) { c.MaxLoginAttempts = 0 })

	noisy := h.user
	noisy.FailedLoginAttempts = 999
	h.dir.users[testUsername] = noisy

	if _, _, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{}); err != nil {
		t.Fatalf("Login: %v, want success when lockout is disabled", err)
	}
}

func TestLoginSkipsSessionLimitWhenDisabled(t *testing.T) {
	h := newHarness(t, func(c *Config) { c.MaxActiveSessions = 0 })

	if _, _, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{}); err != nil {
		t.Fatalf("Login: %v", err)
	}
	if len(h.str.limits) != 0 {
		t.Errorf("enforced a session limit though the cap is disabled: %v", h.str.limits)
	}
}

func TestLogoutRevokesSession(t *testing.T) {
	h := newHarness(t)

	_, key, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{})
	if err != nil {
		t.Fatalf("Login: %v", err)
	}
	if err := h.auth.Logout(t.Context(), key); err != nil {
		t.Fatalf("Logout: %v", err)
	}

	session, _ := h.str.FetchByKey(t.Context(), key)
	if !session.Revoked {
		t.Error("session was not revoked")
	}
	// Revoking an unknown or already-revoked session is a no-op, not an error.
	if err := h.auth.Logout(t.Context(), "no-such-key"); err != nil {
		t.Errorf("Logout of unknown key: %v, want nil", err)
	}
}

// ---- LoadContext -----------------------------------------------------------

// login logs in and returns the raw session key.
func (h *harness) login(t *testing.T) string {
	t.Helper()
	_, key, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{})
	if err != nil {
		t.Fatalf("Login: %v", err)
	}
	return key
}

func TestLoadContextAuthenticatesViaCookie(t *testing.T) {
	h := newHarness(t)
	key := h.login(t)

	var got Principal
	handler := h.auth.LoadContext(func(w http.ResponseWriter, r *http.Request) {
		got = PrincipalFrom(r)
	}, true)

	r := httptest.NewRequest(http.MethodGet, "/app/dashboard", nil)
	r.AddCookie(&http.Cookie{Name: "test_identity", Value: key})
	w := httptest.NewRecorder()
	handler(w, r)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", w.Code)
	}
	if !got.Authenticated || got.Session.UserID != h.user.ID {
		t.Error("cookie session did not authenticate")
	}
	if !got.Permissions["read"] {
		t.Error("permissions were not attached to the request")
	}
}

func TestLoadContextAuthenticatesViaBearerToken(t *testing.T) {
	h := newHarness(t)
	key := h.login(t)

	var got Principal
	handler := h.auth.LoadContext(func(w http.ResponseWriter, r *http.Request) {
		got = PrincipalFrom(r)
	}, true)

	r := httptest.NewRequest(http.MethodGet, "/api/user/profile", nil)
	r.Header.Set("Authorization", "Bearer "+key)
	w := httptest.NewRecorder()
	handler(w, r)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", w.Code)
	}
	if !got.Authenticated {
		t.Error("bearer token did not authenticate")
	}
}

func TestLoadContextOptionalAuthServesAnonymously(t *testing.T) {
	h := newHarness(t)

	var got Principal
	served := false
	handler := h.auth.LoadContext(func(w http.ResponseWriter, r *http.Request) {
		served, got = true, PrincipalFrom(r)
	}, false)

	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/", nil))

	if !served {
		t.Fatal("optional-auth route did not serve an anonymous request")
	}
	if got.Authenticated || got.Permissions != nil {
		t.Error("anonymous request did not get the zero Principal")
	}
}

func TestLoadContextRedirectsAnonymousBrowserToLogin(t *testing.T) {
	h := newHarness(t)

	served := false
	handler := h.auth.LoadContext(okHandler(&served), true)

	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/app/secret?x=1", nil))

	if served {
		t.Fatal("an anonymous request reached a protected handler")
	}
	if w.Code != http.StatusFound {
		t.Fatalf("status = %d, want 302", w.Code)
	}
	if got, want := w.Header().Get("Location"), "/auth/login?redirect=%2Fapp%2Fsecret%3Fx%3D1"; got != want {
		t.Errorf("Location = %q, want %q", got, want)
	}
}

func TestLoadContextAnswersAPIWithJSON401(t *testing.T) {
	h := newHarness(t)

	served := false
	handler := h.auth.LoadContext(okHandler(&served), true)

	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/api/user/profile", nil))

	if served {
		t.Fatal("an anonymous request reached a protected API handler")
	}
	if w.Code != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401", w.Code)
	}
	if ct := w.Header().Get("Content-Type"); ct != "application/json" {
		t.Errorf("Content-Type = %q, want application/json", ct)
	}
	if !strings.Contains(w.Body.String(), "valid authentication required") {
		t.Errorf("body = %q", w.Body.String())
	}
}

func TestLoadContextRejectsExpiredSession(t *testing.T) {
	h := newHarness(t)
	h.str.put(Session{
		Key:        "expired-key",
		UserID:     h.user.ID,
		Created:    time.Now().Add(-48 * time.Hour),
		Expiration: time.Now().Add(-time.Hour),
	})

	t.Run("api gets a JSON 401", func(t *testing.T) {
		served := false
		handler := h.auth.LoadContext(okHandler(&served), true)

		r := httptest.NewRequest(http.MethodGet, "/api/user/profile", nil)
		r.AddCookie(&http.Cookie{Name: "test_identity", Value: "expired-key"})
		w := httptest.NewRecorder()
		handler(w, r)

		if served || w.Code != http.StatusUnauthorized {
			t.Fatalf("served=%v status=%d, want false/401", served, w.Code)
		}
		if !strings.Contains(w.Body.String(), "session expired") {
			t.Errorf("body = %q, want it to name the expiry", w.Body.String())
		}
	})

	t.Run("browser is redirected and has its dead cookie cleared", func(t *testing.T) {
		served := false
		handler := h.auth.LoadContext(okHandler(&served), true)

		r := httptest.NewRequest(http.MethodGet, "/app/dashboard", nil)
		r.AddCookie(&http.Cookie{Name: "test_identity", Value: "expired-key"})
		w := httptest.NewRecorder()
		handler(w, r)

		if served || w.Code != http.StatusFound {
			t.Fatalf("served=%v status=%d, want false/302", served, w.Code)
		}
		cookie := w.Result().Cookies()[0]
		if cookie.Name != "test_identity" || cookie.MaxAge != -1 {
			t.Errorf("expired session did not clear the cookie: %+v", cookie)
		}
	})
}

func TestLoadContextRejectsRevokedSession(t *testing.T) {
	h := newHarness(t)
	h.str.put(Session{
		Key:        "revoked-key",
		UserID:     h.user.ID,
		Expiration: time.Now().Add(time.Hour),
		Revoked:    true,
	})

	served := false
	handler := h.auth.LoadContext(okHandler(&served), true)

	r := httptest.NewRequest(http.MethodGet, "/api/user/profile", nil)
	r.AddCookie(&http.Cookie{Name: "test_identity", Value: "revoked-key"})
	w := httptest.NewRecorder()
	handler(w, r)

	if served {
		t.Fatal("a revoked session authenticated a request")
	}
	if w.Code != http.StatusUnauthorized {
		t.Errorf("status = %d, want 401", w.Code)
	}
}

func TestLoadContextSendsAuthenticatedUserAwayFromLoginPage(t *testing.T) {
	h := newHarness(t)
	key := h.login(t)

	served := false
	handler := h.auth.LoadContext(okHandler(&served), false)

	r := httptest.NewRequest(http.MethodGet, "/auth/login", nil)
	r.AddCookie(&http.Cookie{Name: "test_identity", Value: key})
	w := httptest.NewRecorder()
	handler(w, r)

	if w.Code != http.StatusFound || w.Header().Get("Location") != "/app/dashboard" {
		t.Errorf("status=%d Location=%q, want 302 to /app/dashboard", w.Code, w.Header().Get("Location"))
	}
}

func TestLoadContextServesLoginPageToAnonymousUser(t *testing.T) {
	h := newHarness(t)

	served := false
	handler := h.auth.LoadContext(okHandler(&served), true)

	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/auth/login", nil))

	if !served {
		t.Fatal("the login page must be reachable while signed out, or login is impossible")
	}
}

// A bearer-token request must not disable redirects for the browser requests
// that follow it. The middleware previously kept `redirect` in the enclosing
// closure and cleared it on seeing a bearer token, so one API call permanently
// converted this route's redirect-to-login into a bare 401 for every user.
func TestLoadContextBearerRequestDoesNotLeakIntoLaterRequests(t *testing.T) {
	h := newHarness(t)

	served := false
	handler := h.auth.LoadContext(okHandler(&served), true)

	// An anonymous API call carrying a bearer token: answered with JSON, no redirect.
	r := httptest.NewRequest(http.MethodGet, "/api/user/profile", nil)
	r.Header.Set("Authorization", "Bearer bogus")
	handler(httptest.NewRecorder(), r)

	// A browser hitting a page on the same route must still be redirected.
	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/app/secret", nil))

	if w.Code != http.StatusFound {
		t.Fatalf("status = %d, want 302: a prior bearer request leaked into this one", w.Code)
	}
}

// ---- authorization ---------------------------------------------------------

func principalWith(perms ...string) Principal {
	p := Principal{Authenticated: true, Permissions: map[string]bool{}}
	for _, perm := range perms {
		p.Permissions[perm] = true
	}
	return p
}

func TestAuthorized(t *testing.T) {
	a := newHarness(t).auth

	tests := []struct {
		name  string
		held  Principal
		need  []string
		allow bool
	}{
		{"holds the permission", principalWith("read"), []string{"read"}, true},
		{"missing the permission", principalWith("read"), []string{"write"}, false},
		{"holds all of several", principalWith("read", "write"), []string{"read", "write"}, true},
		{"holds only some of several", principalWith("read"), []string{"read", "write"}, false},
		{"super permission satisfies anything", principalWith("*"), []string{"write"}, true},
		{"no requirements", principalWith(), nil, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := a.Authorized(tt.held, tt.need...); got != tt.allow {
				t.Errorf("Authorized = %v, want %v", got, tt.allow)
			}
		})
	}
}

func TestAuthorizedExactIgnoresSuperPermission(t *testing.T) {
	a := newHarness(t).auth

	super := principalWith("*")
	if !a.Authorized(super, "write") {
		t.Error("Authorized: the super permission should satisfy any requirement")
	}
	if a.AuthorizedExact(super, "write") {
		t.Error("AuthorizedExact: the super permission must not bypass an exact check")
	}
	if !a.AuthorizedExact(principalWith("write"), "write") {
		t.Error("AuthorizedExact: a genuinely held permission should pass")
	}
}

func TestAuthorizedAny(t *testing.T) {
	a := newHarness(t).auth

	if !a.AuthorizedAny(principalWith("write"), "read", "write") {
		t.Error("AuthorizedAny: holding one of the listed permissions should pass")
	}
	if a.AuthorizedAny(principalWith("delete"), "read", "write") {
		t.Error("AuthorizedAny: holding none of them should fail")
	}
	if !a.AuthorizedAny(principalWith("*"), "read") {
		t.Error("AuthorizedAny: the super permission should satisfy it")
	}
	if a.AuthorizedAny(principalWith("read")) {
		t.Error("AuthorizedAny: an empty list grants nothing")
	}
}

// requireCase drives a Require-family middleware over a request whose principal
// holds perms, and reports the status and whether the handler ran.
func requireCase(t *testing.T, mw func(http.HandlerFunc) http.HandlerFunc, path string, perms ...string) (int, bool) {
	t.Helper()

	served := false
	r := withPrincipal(httptest.NewRequest(http.MethodGet, path, nil), principalWith(perms...))
	w := httptest.NewRecorder()
	mw(okHandler(&served))(w, r)
	return w.Code, served
}

func TestRequireGatesHandler(t *testing.T) {
	a := newHarness(t).auth

	t.Run("allows a caller holding the permission", func(t *testing.T) {
		mw := func(h http.HandlerFunc) http.HandlerFunc { return a.Require(h, "read") }
		if code, served := requireCase(t, mw, "/api/x", "read"); !served || code != http.StatusOK {
			t.Errorf("status=%d served=%v, want 200/true", code, served)
		}
	})

	t.Run("denies a caller missing it, in JSON on an API route", func(t *testing.T) {
		mw := func(h http.HandlerFunc) http.HandlerFunc { return a.Require(h, "write") }
		code, served := requireCase(t, mw, "/api/x", "read")
		if served || code != http.StatusForbidden {
			t.Errorf("status=%d served=%v, want 403/false", code, served)
		}
	})

	t.Run("denies a browser caller with a bare 403", func(t *testing.T) {
		mw := func(h http.HandlerFunc) http.HandlerFunc { return a.Require(h, "write") }
		if code, served := requireCase(t, mw, "/app/x", "read"); served || code != http.StatusForbidden {
			t.Errorf("status=%d served=%v, want 403/false", code, served)
		}
	})

	t.Run("denies an anonymous caller", func(t *testing.T) {
		served := false
		w := httptest.NewRecorder()
		a.Require(okHandler(&served), "read")(w, httptest.NewRequest(http.MethodGet, "/api/x", nil))
		if served || w.Code != http.StatusForbidden {
			t.Errorf("status=%d served=%v, want 403/false", w.Code, served)
		}
	})
}

func TestRequireExactAndRequireAnyMiddleware(t *testing.T) {
	a := newHarness(t).auth

	exact := func(h http.HandlerFunc) http.HandlerFunc { return a.RequireExact(h, "write") }
	if code, served := requireCase(t, exact, "/api/x", "*"); served || code != http.StatusForbidden {
		t.Errorf("RequireExact: status=%d served=%v, want 403/false for a super-permission holder", code, served)
	}

	any := func(h http.HandlerFunc) http.HandlerFunc { return a.RequireAny(h, "read", "write") }
	if code, served := requireCase(t, any, "/api/x", "write"); !served || code != http.StatusOK {
		t.Errorf("RequireAny: status=%d served=%v, want 200/true", code, served)
	}
	if code, served := requireCase(t, any, "/api/x", "delete"); served || code != http.StatusForbidden {
		t.Errorf("RequireAny: status=%d served=%v, want 403/false", code, served)
	}
}

// ---- implications ----------------------------------------------------------

func TestImplicationsGrantTransitively(t *testing.T) {
	h := newHarness(t, func(c *Config) {
		c.Implications = map[string][]string{
			"manage_events": {"view_events", "view_seasons"},
		}
	})
	h.auth.resolver = fakeResolver{perms: map[string]bool{"manage_events": true}}

	_, key, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{})
	if err != nil {
		t.Fatalf("Login: %v", err)
	}

	session, _ := h.str.FetchByKey(t.Context(), key)
	perms := h.auth.resolvePermissions(t.Context(), session)

	for _, want := range []string{"view_events", "view_seasons"} {
		if !perms[want] {
			t.Errorf("%q was not implied by manage_events", want)
		}
	}
}

func TestImplicationsRespectExplicitDeny(t *testing.T) {
	h := newHarness(t, func(c *Config) {
		c.Implications = map[string][]string{"manage_events": {"view_events"}}
	})
	// An explicit deny — the permission is present and false, not merely absent.
	h.auth.resolver = fakeResolver{perms: map[string]bool{
		"manage_events": true,
		"view_events":   false,
	}}

	perms := h.auth.resolvePermissions(t.Context(), Session{})
	if perms["view_events"] {
		t.Error("an implication overrode an explicit deny")
	}
}

func TestImplicationsIgnoreUnheldSource(t *testing.T) {
	h := newHarness(t, func(c *Config) {
		c.Implications = map[string][]string{"manage_events": {"view_events"}}
	})
	h.auth.resolver = fakeResolver{perms: map[string]bool{"read": true}}

	if perms := h.auth.resolvePermissions(t.Context(), Session{}); perms["view_events"] {
		t.Error("an implication fired though its source permission is not held")
	}
}

// ---- api keys --------------------------------------------------------------

type fakeAPIKeys struct {
	mu      sync.Mutex
	keys    map[string]APIKey
	touched []uuid.UUID
}

func (k *fakeAPIKeys) FindAPIKeyByHash(_ context.Context, hash string) (APIKey, error) {
	k.mu.Lock()
	defer k.mu.Unlock()
	key, ok := k.keys[hash]
	if !ok {
		return APIKey{}, ErrAPIKeyNotFound
	}
	return key, nil
}

func (k *fakeAPIKeys) TouchAPIKey(_ context.Context, id uuid.UUID) {
	k.mu.Lock()
	defer k.mu.Unlock()
	k.touched = append(k.touched, id)
}

func TestLoadAPIKey(t *testing.T) {
	const raw = "sk_live_secret"
	orgID := uuid.New()

	newAuth := func(key APIKey) (*Authenticator, *fakeAPIKeys) {
		keys := &fakeAPIKeys{keys: map[string]APIKey{HashAPIKey(raw): key}}
		h := newHarness(t, func(c *Config) { c.APIKeys = keys })
		return h.auth, keys
	}

	t.Run("a valid key authenticates and scopes the request to its org", func(t *testing.T) {
		a, keys := newAuth(APIKey{ID: uuid.New(), OrgID: orgID})

		var got Principal
		r := httptest.NewRequest(http.MethodGet, "/api/external/data", nil)
		r.Header.Set("Authorization", "Bearer "+raw)
		w := httptest.NewRecorder()
		a.LoadAPIKey(func(w http.ResponseWriter, r *http.Request) { got = PrincipalFrom(r) })(w, r)

		if !got.Authenticated || !got.IsAPIKey {
			t.Fatalf("principal = %+v, want an authenticated API-key principal", got)
		}
		if got.Session.OrgID == nil || *got.Session.OrgID != orgID {
			t.Error("principal was not scoped to the key's org")
		}
		if got.Session.UserID != uuid.Nil {
			t.Error("an API key must not impersonate a user")
		}

		// TouchAPIKey runs on its own goroutine; give it a moment.
		deadline := time.Now().Add(time.Second)
		for time.Now().Before(deadline) {
			keys.mu.Lock()
			n := len(keys.touched)
			keys.mu.Unlock()
			if n == 1 {
				return
			}
			time.Sleep(5 * time.Millisecond)
		}
		t.Error("the key's last-used was never recorded")
	})

	t.Run("a revoked key is rejected", func(t *testing.T) {
		a, _ := newAuth(APIKey{ID: uuid.New(), OrgID: orgID, Revoked: true})

		served := false
		r := httptest.NewRequest(http.MethodGet, "/api/external/data", nil)
		r.Header.Set("Authorization", "Bearer "+raw)
		w := httptest.NewRecorder()
		a.LoadAPIKey(okHandler(&served))(w, r)

		if served || w.Code != http.StatusUnauthorized {
			t.Errorf("status=%d served=%v, want 401/false", w.Code, served)
		}
	})

	t.Run("an unknown key is rejected", func(t *testing.T) {
		a, _ := newAuth(APIKey{ID: uuid.New(), OrgID: orgID})

		served := false
		r := httptest.NewRequest(http.MethodGet, "/api/external/data", nil)
		r.Header.Set("Authorization", "Bearer not-a-key")
		w := httptest.NewRecorder()
		a.LoadAPIKey(okHandler(&served))(w, r)

		if served || w.Code != http.StatusUnauthorized {
			t.Errorf("status=%d served=%v, want 401/false", w.Code, served)
		}
	})

	t.Run("a missing header is rejected", func(t *testing.T) {
		a, _ := newAuth(APIKey{ID: uuid.New(), OrgID: orgID})

		served := false
		w := httptest.NewRecorder()
		a.LoadAPIKey(okHandler(&served))(w, httptest.NewRequest(http.MethodGet, "/api/external/data", nil))

		if served || w.Code != http.StatusUnauthorized {
			t.Errorf("status=%d served=%v, want 401/false", w.Code, served)
		}
	})

	t.Run("the route is closed when no API-key directory is configured", func(t *testing.T) {
		a := newHarness(t).auth // no Config.APIKeys

		served := false
		r := httptest.NewRequest(http.MethodGet, "/api/external/data", nil)
		r.Header.Set("Authorization", "Bearer "+raw)
		w := httptest.NewRecorder()
		a.LoadAPIKey(okHandler(&served))(w, r)

		if served || w.Code != http.StatusUnauthorized {
			t.Errorf("status=%d served=%v, want 401/false", w.Code, served)
		}
	})
}

func TestHashAPIKeyIsStableAndHidesTheKey(t *testing.T) {
	hash := HashAPIKey("sk_live_secret")

	if hash != HashAPIKey("sk_live_secret") {
		t.Error("HashAPIKey is not deterministic")
	}
	if hash == HashAPIKey("sk_live_secret2") {
		t.Error("distinct keys collided")
	}
	if strings.Contains(hash, "sk_live_secret") {
		t.Error("the digest leaks the raw key")
	}
	if len(hash) != 64 {
		t.Errorf("len = %d, want 64 hex chars of SHA-256", len(hash))
	}
}

// ---- cookies ---------------------------------------------------------------

func TestSetSessionCookie(t *testing.T) {
	h := newHarness(t)
	principal, _, err := h.auth.Login(t.Context(), testUsername, testPassword, SessionMeta{})
	if err != nil {
		t.Fatalf("Login: %v", err)
	}

	t.Run("without remember-me it dies with the browser", func(t *testing.T) {
		w := httptest.NewRecorder()
		if err := h.auth.SetSessionCookie(w, httptest.NewRequest(http.MethodPost, "/auth/login", nil), principal, false); err != nil {
			t.Fatalf("SetSessionCookie: %v", err)
		}

		cookie := w.Result().Cookies()[0]
		if cookie.Value != principal.Session.Key {
			t.Error("the cookie does not carry the session key")
		}
		if !cookie.HttpOnly {
			t.Error("the session cookie must be HttpOnly")
		}
		if !cookie.Expires.IsZero() {
			t.Error("without remember-me the cookie must be a session cookie")
		}
	})

	t.Run("with remember-me it carries the session's expiry", func(t *testing.T) {
		w := httptest.NewRecorder()
		if err := h.auth.SetSessionCookie(w, httptest.NewRequest(http.MethodPost, "/auth/login", nil), principal, true); err != nil {
			t.Fatalf("SetSessionCookie: %v", err)
		}

		cookie := w.Result().Cookies()[0]
		if !cookie.Expires.Equal(principal.Session.Expiration.Truncate(time.Second)) &&
			cookie.Expires.Sub(principal.Session.Expiration).Abs() > time.Second {
			t.Errorf("Expires = %v, want the session expiration %v", cookie.Expires, principal.Session.Expiration)
		}
	})

	t.Run("it fails rather than overflow the domain's cookie budget", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodPost, "/auth/login", nil)
		r.AddCookie(&http.Cookie{Name: "bloat", Value: strings.Repeat("x", maxCookieBytes)})

		if err := h.auth.SetSessionCookie(httptest.NewRecorder(), r, principal, true); err == nil {
			t.Error("SetSessionCookie: nil error, want a failure when the budget is blown")
		}
	})
}

// ---- password policy -------------------------------------------------------

func TestCheckPassword(t *testing.T) {
	a := newHarness(t, func(c *Config) {
		c.Password = PasswordPolicy{
			MinLength:         8,
			RequiredUppercase: 1,
			RequiredLowercase: 1,
			RequiredNumbers:   1,
			RequiredSymbols:   1,
		}
	}).auth

	tests := []struct {
		name     string
		password string
		ok       bool
	}{
		{"satisfies every rule", "Passw0rd!", true},
		{"blank", "", false},
		{"whitespace only", "   ", false},
		{"too short", "Pw0!", false},
		{"no uppercase", "passw0rd!", false},
		{"no lowercase", "PASSW0RD!", false},
		{"no number", "Password!", false},
		{"no symbol", "Passw0rdd", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := a.CheckPassword(tt.password)
			if tt.ok && err != nil {
				t.Errorf("CheckPassword(%q) = %v, want nil", tt.password, err)
			}
			if !tt.ok && err == nil {
				t.Errorf("CheckPassword(%q) = nil, want an error", tt.password)
			}
		})
	}
}

func TestZeroPasswordPolicyAcceptsAnyNonBlankPassword(t *testing.T) {
	if err := CheckPasswordPolicy(PasswordPolicy{}, "a"); err != nil {
		t.Errorf("CheckPasswordPolicy = %v, want nil under the zero policy", err)
	}
	if err := CheckPasswordPolicy(PasswordPolicy{}, ""); err == nil {
		t.Error("CheckPasswordPolicy: a blank password must always be rejected")
	}
}

func TestGeneratePasswordResetToken(t *testing.T) {
	first, err := GeneratePasswordResetToken()
	if err != nil {
		t.Fatalf("GeneratePasswordResetToken: %v", err)
	}
	if len(first) != 64 {
		t.Errorf("len = %d, want 64 hex chars of 32 random bytes", len(first))
	}

	second, err := GeneratePasswordResetToken()
	if err != nil {
		t.Fatalf("GeneratePasswordResetToken: %v", err)
	}
	if first == second {
		t.Error("two reset tokens came back identical")
	}
}

// ---- context ---------------------------------------------------------------

func TestPrincipalFromRequestWithoutMiddleware(t *testing.T) {
	p := PrincipalFrom(httptest.NewRequest(http.MethodGet, "/", nil))
	if p.Authenticated {
		t.Error("a request that never passed through the middleware came back authenticated")
	}
}
