// Tests for the M13a W3 JWT gate in admind. We test the gate wiring
// (which routes are protected, which roles are allowed) in
// isolation. The real handlers (handleListDLQ etc.) need a real
// pool + broker; those live in their own integration tests.
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"git3.techno-world.net/lrosales/broad-announce/internal/authd"
)

// newStubAuthd builds an Authd that only validates tokens (no pool
// needed; we never issue).
func newStubAuthd(t *testing.T, secret string) *authd.Authd {
	t.Helper()
	a, err := authd.New(nil, authd.Config{
		JWTSecret:      []byte(secret),
		Issuer:         "broad-announce",
		AccessTokenTTL: 1 * time.Minute,
	})
	if err != nil {
		t.Fatalf("authd.New: %v", err)
	}
	return a
}

func mintTestJWT(t *testing.T, secret, role string) string {
	t.Helper()
	hb, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
	body, _ := json.Marshal(map[string]any{
		"sub":  "u-test",
		"tid":  "t-test",
		"role": role,
		"typ":  "access",
		"iss":  "broad-announce",
		"exp":  time.Now().Add(15 * time.Minute).Unix(),
		"iat":  time.Now().Unix(),
	})
	enc := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(body)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(enc))
	sig := mac.Sum(nil)
	return enc + "." + base64.RawURLEncoding.EncodeToString(sig)
}

func reqWithToken(method, path, token string) *http.Request {
	r := httptest.NewRequest(method, path, nil)
	if token != "" {
		r.Header.Set("Authorization", "Bearer "+token)
	}
	return r
}

func discardLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

// safeServeHTTP runs mux.ServeHTTP in a recover() to catch panics
// from handlers that need real pool/broker. Returns the status
// (or 0 if the handler panicked — which is fine for our purposes,
// the test only cares about NOT being 401).
func safeServeHTTP(t *testing.T, mux http.Handler, r *http.Request) int {
	t.Helper()
	rr := httptest.NewRecorder()
	done := make(chan struct{})
	go func() {
		defer close(done)
		defer func() {
			if rec := recover(); rec != nil {
				// expected: handlers panic with nil pool
			}
		}()
		mux.ServeHTTP(rr, r)
	}()
	<-done
	return rr.Code
}

// TestWireDLQRoutes_NoSecret_Unauthenticated verifies that with
// BA_AUTHD_JWT_SECRET unset, the routes accept any request (the
// M8 backward-compatible behavior).
func TestWireDLQRoutes_NoSecret_Unauthenticated(t *testing.T) {
	os.Unsetenv("BA_AUTHD_JWT_SECRET")

	mux := http.NewServeMux()
	logger := discardLogger()
	wireDLQRoutes(mux, nil, nil, logger)

	// Hit each route without a token — the real handlers panic
	// (pool is nil) but the test only cares about NOT being 401.
	for _, tc := range []struct{ method, path string }{
		{"GET", "/v1/dlq"},
		{"GET", "/v1/dlq/1"},
		{"POST", "/v1/dlq/1/replay"},
		{"POST", "/v1/dlq/1/discard"},
	} {
		code := safeServeHTTP(t, mux, httptest.NewRequest(tc.method, tc.path, nil))
		if code == http.StatusUnauthorized {
			t.Errorf("%s %s with no secret: got 401, want open route (panic/500 OK)", tc.method, tc.path)
		}
	}
}

// TestWireDLQRoutes_WithSecret_Gated verifies that with the secret
// set, all /v1/dlq* routes are 401 without a token.
func TestWireDLQRoutes_WithSecret_Gated(t *testing.T) {
	const secret = "test-secret-with-32-bytes-min-len-abc"
	t.Setenv("BA_AUTHD_JWT_SECRET", secret)
	t.Setenv("BA_AUTHD_ISSUER", "broad-announce")

	mux := http.NewServeMux()
	logger := discardLogger()
	wireDLQRoutes(mux, nil, nil, logger)

	for _, tc := range []struct{ method, path string }{
		{"GET", "/v1/dlq"},
		{"GET", "/v1/dlq/1"},
		{"POST", "/v1/dlq/1/replay"},
		{"POST", "/v1/dlq/1/discard"},
	} {
		rr := httptest.NewRecorder()
		mux.ServeHTTP(rr, httptest.NewRequest(tc.method, tc.path, nil))
		if rr.Code != http.StatusUnauthorized {
			t.Errorf("%s %s with secret+no-token: status = %d, want 401", tc.method, tc.path, rr.Code)
		}
	}
}

// TestDLQGate_RolePolicy verifies the read/destructive split:
//   - GET /v1/dlq* accept any authenticated user
//   - POST /v1/dlq/*/{replay,discard} require admin role
func TestDLQGate_RolePolicy(t *testing.T) {
	const secret = "test-secret-with-32-bytes-min-len-abc"
	a := newStubAuthd(t, secret)

	mux := http.NewServeMux()
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
	mux.Handle("GET /v1/dlq", a.RequireAuth(ok))
	mux.Handle("GET /v1/dlq/{id}", a.RequireAuth(ok))
	mux.Handle("POST /v1/dlq/{id}/replay", a.RequireRole("super_admin", "tenant_admin")(ok))
	mux.Handle("POST /v1/dlq/{id}/discard", a.RequireRole("super_admin", "tenant_admin")(ok))

	cases := []struct {
		name   string
		role   string
		method string
		path   string
		want   int
	}{
		// viewer can list
		{"viewer-list", "viewer", "GET", "/v1/dlq", 200},
		{"viewer-get", "viewer", "GET", "/v1/dlq/1", 200},
		// viewer CANNOT replay/discard
		{"viewer-replay", "viewer", "POST", "/v1/dlq/1/replay", 403},
		{"viewer-discard", "viewer", "POST", "/v1/dlq/1/discard", 403},
		// tenant_admin can do everything
		{"ta-list", "tenant_admin", "GET", "/v1/dlq", 200},
		{"ta-replay", "tenant_admin", "POST", "/v1/dlq/1/replay", 200},
		{"ta-discard", "tenant_admin", "POST", "/v1/dlq/1/discard", 200},
		// super_admin can do everything
		{"sa-replay", "super_admin", "POST", "/v1/dlq/1/replay", 200},
		{"sa-discard", "super_admin", "POST", "/v1/dlq/1/discard", 200},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			tok := mintTestJWT(t, secret, tc.role)
			rr := httptest.NewRecorder()
			mux.ServeHTTP(rr, reqWithToken(tc.method, tc.path, tok))
			if rr.Code != tc.want {
				t.Errorf("%s %s as %s: status = %d, want %d (body: %s)",
					tc.method, tc.path, tc.role, rr.Code, tc.want, rr.Body.String())
			}
		})
	}
}

// TestEnvEnabled verifies the helper that main() uses to decide
// whether to wire the gate.
func TestEnvEnabled(t *testing.T) {
	os.Unsetenv("BA_AUTHD_JWT_SECRET")
	if authd.EnvEnabled() {
		t.Error("EnvEnabled: unset secret, got true, want false")
	}
	t.Setenv("BA_AUTHD_JWT_SECRET", "x")
	if !authd.EnvEnabled() {
		t.Error("EnvEnabled: set secret, got false, want true")
	}
}
