package handler

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

	"codeberg.org/VARASYS/ZDDC/zddc/internal/auth"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
)

// newTestTokenStore returns an isolated auth.Store under a TempDir.
func newTestTokenStore(t *testing.T) *auth.Store {
	t.Helper()
	root := t.TempDir()
	store, err := auth.NewStore(root)
	if err != nil {
		t.Fatalf("auth.NewStore: %v", err)
	}
	return store
}

// authedReq builds an HTTP request with the authenticated email set in
// the request context (mimicking what ACLMiddleware would do upstream
// of the handler under test).
func authedReq(method, path, email string, body []byte) *http.Request {
	var r *http.Request
	if body != nil {
		r = httptest.NewRequest(method, path, bytes.NewReader(body))
		r.Header.Set("Content-Type", "application/json")
	} else {
		r = httptest.NewRequest(method, path, nil)
	}
	if email != "" {
		r = r.WithContext(WithEmail(r.Context(), email))
	}
	return r
}

func TestServeTokensAPI_Anonymous401(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodGet, "/.api/tokens", nil)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("anonymous GET /.api/tokens = %d, want 401", rec.Code)
	}
}

func TestServeTokensAPI_NoStore503(t *testing.T) {
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodGet, "/.api/tokens", "alice@example.com", nil)
	ServeTokensAPI(config.Config{}, nil, rec, r)
	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("nil store = %d, want 503", rec.Code)
	}
}

func TestServeTokensAPI_ListEmpty(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodGet, "/.api/tokens", "alice@example.com", nil)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusOK {
		t.Errorf("GET = %d, want 200", rec.Code)
	}
	if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
		t.Errorf("Content-Type = %q", got)
	}
	var list []tokenAPIView
	if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil {
		t.Fatalf("unmarshal: %v body=%q", err, rec.Body.String())
	}
	if len(list) != 0 {
		t.Errorf("expected empty list, got %d entries", len(list))
	}
}

func TestServeTokensAPI_CreateAndList(t *testing.T) {
	store := newTestTokenStore(t)

	body := []byte(`{"description":"laptop"}`)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodPost, "/.api/tokens", "alice@example.com", body)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusCreated {
		t.Fatalf("POST = %d, want 201; body=%q", rec.Code, rec.Body.String())
	}
	var resp tokenCreateResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal POST resp: %v", err)
	}
	if resp.Token == "" {
		t.Fatal("plaintext token missing from response")
	}
	if resp.ID == "" {
		t.Fatal("ID missing")
	}
	if resp.Email != "alice@example.com" {
		t.Errorf("Email = %q", resp.Email)
	}
	if resp.Description != "laptop" {
		t.Errorf("Description = %q", resp.Description)
	}

	// The token must validate as a Bearer.
	tok, err := store.Validate(resp.Token)
	if err != nil {
		t.Fatalf("Validate the new plaintext: %v", err)
	}
	if tok.ID() != resp.ID {
		t.Errorf("validated ID %q != response ID %q", tok.ID(), resp.ID)
	}

	// Subsequent list contains the token (without plaintext).
	rec2 := httptest.NewRecorder()
	r2 := authedReq(http.MethodGet, "/.api/tokens", "alice@example.com", nil)
	ServeTokensAPI(config.Config{}, store, rec2, r2)
	if rec2.Code != http.StatusOK {
		t.Fatalf("GET = %d", rec2.Code)
	}
	var list []tokenAPIView
	_ = json.Unmarshal(rec2.Body.Bytes(), &list)
	if len(list) != 1 {
		t.Fatalf("list = %d entries, want 1", len(list))
	}
	if list[0].ID != resp.ID {
		t.Errorf("listed ID mismatch")
	}
	// Plaintext token MUST NOT appear in any list response.
	if strings.Contains(rec2.Body.String(), resp.Token) {
		t.Error("plaintext token leaked in list response")
	}
}

func TestServeTokensAPI_CreateRejectsPastExpiry(t *testing.T) {
	store := newTestTokenStore(t)
	past := time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)
	body := []byte(`{"expires":"` + past + `"}`)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodPost, "/.api/tokens", "alice@example.com", body)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("past-expiry create = %d, want 400", rec.Code)
	}
}

func TestServeTokensAPI_CreateRejectsLongDescription(t *testing.T) {
	store := newTestTokenStore(t)
	huge := strings.Repeat("x", MaxTokenDescription+1)
	body, _ := json.Marshal(tokenCreateRequest{Description: huge})
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodPost, "/.api/tokens", "alice@example.com", body)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("long description = %d, want 400", rec.Code)
	}
}

func TestServeTokensAPI_RevokeOwnToken(t *testing.T) {
	store := newTestTokenStore(t)
	plaintext, tok, err := store.Generate("alice@example.com", "", time.Time{})
	if err != nil {
		t.Fatalf("seed Generate: %v", err)
	}
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodDelete, "/.api/tokens/"+tok.ID(), "alice@example.com", nil)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusNoContent {
		t.Errorf("DELETE = %d, want 204; body=%q", rec.Code, rec.Body.String())
	}
	// Token no longer validates.
	if _, err := store.Validate(plaintext); err == nil {
		t.Error("token still validates after revoke")
	}
}

func TestServeTokensAPI_RevokeOtherUsers404(t *testing.T) {
	store := newTestTokenStore(t)
	_, tok, err := store.Generate("alice@example.com", "", time.Time{})
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodDelete, "/.api/tokens/"+tok.ID(), "bob@example.com", nil)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusNotFound {
		t.Errorf("DELETE other user's = %d, want 404", rec.Code)
	}
}

func TestServeTokensAPI_RevokeMissing404(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodDelete, "/.api/tokens/deadbeef", "alice@example.com", nil)
	ServeTokensAPI(config.Config{}, store, rec, r)
	if rec.Code != http.StatusNotFound {
		t.Errorf("DELETE nonexistent = %d, want 404", rec.Code)
	}
}

func TestServeTokensAPI_RejectsBadMethods(t *testing.T) {
	store := newTestTokenStore(t)
	cases := []struct {
		path   string
		method string
	}{
		{"/.api/tokens", http.MethodPut},
		{"/.api/tokens", http.MethodDelete},
		{"/.api/tokens/abc12345", http.MethodGet},
		{"/.api/tokens/abc12345", http.MethodPost},
	}
	for _, c := range cases {
		rec := httptest.NewRecorder()
		r := authedReq(c.method, c.path, "alice@example.com", nil)
		ServeTokensAPI(config.Config{}, store, rec, r)
		if rec.Code != http.StatusMethodNotAllowed {
			t.Errorf("%s %s = %d, want 405", c.method, c.path, rec.Code)
		}
	}
}

func TestServeTokensPage_Authenticated(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodGet, "/.tokens", "alice@example.com", nil)
	ServeTokensPage(config.Config{}, store, rec, r)
	if rec.Code != http.StatusOK {
		t.Fatalf("GET /.tokens = %d", rec.Code)
	}
	if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
		t.Errorf("Content-Type = %q", got)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "alice@example.com") {
		t.Error("page does not show authenticated email")
	}
	if !strings.Contains(body, "/.api/tokens") {
		t.Error("page does not reference the API endpoint")
	}
}

func TestServeTokensPage_Anonymous401(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodGet, "/.tokens", nil)
	ServeTokensPage(config.Config{}, store, rec, r)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("anonymous = %d, want 401", rec.Code)
	}
}

func TestServeTokensPage_NoStoreShowsWarning(t *testing.T) {
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodGet, "/.tokens", "alice@example.com", nil)
	ServeTokensPage(config.Config{}, nil, rec, r)
	if rec.Code != http.StatusOK {
		t.Fatalf("authenticated with nil store = %d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "unavailable") {
		t.Error("expected warning about token store unavailability")
	}
}

func TestServeTokensPage_RejectsNonGET(t *testing.T) {
	store := newTestTokenStore(t)
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodPost, "/.tokens", "alice@example.com", nil)
	ServeTokensPage(config.Config{}, store, rec, r)
	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("POST = %d, want 405", rec.Code)
	}
}

func TestACLMiddleware_BearerSetsEmailFromToken(t *testing.T) {
	store := newTestTokenStore(t)
	plaintext, _, err := store.Generate("bearer-user@example.com", "", time.Time{})
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	var seenEmail string
	chain := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seenEmail = EmailFromContext(r)
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer "+plaintext)
	rec := httptest.NewRecorder()
	chain.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d", rec.Code)
	}
	if seenEmail != "bearer-user@example.com" {
		t.Errorf("seen email = %q, want bearer-user@example.com", seenEmail)
	}
}

func TestACLMiddleware_InvalidBearerRejected(t *testing.T) {
	store := newTestTokenStore(t)
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	chain := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Error("inner handler should not run on invalid bearer")
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer not-a-real-token")
	// And a valid header email — Bearer must still take precedence and fail.
	req.Header.Set("X-Auth-Request-Email", "alice@example.com")
	rec := httptest.NewRecorder()
	chain.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("invalid bearer = %d, want 401", rec.Code)
	}
}

func TestACLMiddleware_BearerWithoutStoreRejected(t *testing.T) {
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	chain := ACLMiddleware(cfg, nil, nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Error("inner handler should not run when bearer present and store absent")
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer something")
	rec := httptest.NewRecorder()
	chain.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("bearer w/ nil store = %d, want 401", rec.Code)
	}
}

func TestACLMiddleware_NoBearerFallsBackToHeader(t *testing.T) {
	store := newTestTokenStore(t)
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	var seen string
	chain := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seen = EmailFromContext(r)
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("X-Auth-Request-Email", "alice@example.com")
	rec := httptest.NewRecorder()
	chain.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d", rec.Code)
	}
	if seen != "alice@example.com" {
		t.Errorf("email = %q, want alice@example.com (from header)", seen)
	}
}

func TestACLMiddleware_BearerCaseInsensitive(t *testing.T) {
	store := newTestTokenStore(t)
	plaintext, _, err := store.Generate("alice@example.com", "", time.Time{})
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	for _, prefix := range []string{"Bearer ", "bearer ", "BEARER ", "BeArEr "} {
		var seen string
		chain := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			seen = EmailFromContext(r)
			w.WriteHeader(http.StatusOK)
		}))
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("Authorization", prefix+plaintext)
		rec := httptest.NewRecorder()
		chain.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Errorf("prefix %q: status %d", prefix, rec.Code)
		}
		if seen != "alice@example.com" {
			t.Errorf("prefix %q: email %q, want alice@example.com", prefix, seen)
		}
	}
}

func TestACLMiddleware_ExpiredBearerRejected(t *testing.T) {
	store := newTestTokenStore(t)
	plaintext, _, err := store.Generate("alice@example.com", "", time.Now().Add(-time.Hour))
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}
	chain := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Error("inner handler should not run on expired bearer")
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer "+plaintext)
	rec := httptest.NewRecorder()
	chain.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("expired bearer = %d, want 401", rec.Code)
	}
}

// TestEndToEndCreateAuthenticateRevoke walks the full happy path: an
// authenticated user creates a token via the API, uses it as a Bearer
// for a separate request, then revokes it and confirms the next Bearer
// fails.
func TestEndToEndCreateAuthenticateRevoke(t *testing.T) {
	store := newTestTokenStore(t)
	cfg := config.Config{EmailHeader: "X-Auth-Request-Email"}

	// 1. Create token via API as alice (authenticated via context).
	rec := httptest.NewRecorder()
	r := authedReq(http.MethodPost, "/.api/tokens", "alice@example.com", []byte(`{"description":"e2e"}`))
	ServeTokensAPI(cfg, store, rec, r)
	if rec.Code != http.StatusCreated {
		t.Fatalf("create: %d", rec.Code)
	}
	var resp tokenCreateResponse
	_ = json.Unmarshal(rec.Body.Bytes(), &resp)

	// 2. Use the token as a Bearer through ACLMiddleware.
	var seen string
	bearerHandler := ACLMiddleware(cfg, nil, store, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seen = EmailFromContext(r)
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer "+resp.Token)
	rec2 := httptest.NewRecorder()
	bearerHandler.ServeHTTP(rec2, req)
	if rec2.Code != http.StatusOK || seen != "alice@example.com" {
		t.Fatalf("authenticate: code=%d email=%q", rec2.Code, seen)
	}

	// 3. Revoke via API as alice.
	rec3 := httptest.NewRecorder()
	r3 := authedReq(http.MethodDelete, "/.api/tokens/"+resp.ID, "alice@example.com", nil)
	ServeTokensAPI(cfg, store, rec3, r3)
	if rec3.Code != http.StatusNoContent {
		t.Fatalf("revoke: %d", rec3.Code)
	}

	// 4. Bearer no longer authenticates.
	req4 := httptest.NewRequest(http.MethodGet, "/", nil)
	req4.Header.Set("Authorization", "Bearer "+resp.Token)
	rec4 := httptest.NewRecorder()
	bearerHandler.ServeHTTP(rec4, req4)
	if rec4.Code != http.StatusUnauthorized {
		t.Errorf("post-revoke Bearer = %d, want 401", rec4.Code)
	}
}

// Sanity: WithEmail-from-context helper works as expected when used
// directly without the middleware (test seam).
func TestWithEmail(t *testing.T) {
	ctx := WithEmail(context.Background(), "carol@example.com")
	r := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
	if got := EmailFromContext(r); got != "carol@example.com" {
		t.Errorf("EmailFromContext = %q", got)
	}
}
