package membership

import (
	"bytes"
	"encoding/base64"
	"encoding/hex"
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	cs "fn-registry/functions/cybersecurity"

	"github.com/enmanuel/unibus/pkg/blobstore"
	"github.com/enmanuel/unibus/pkg/frame"
)

// authHarness boots an in-process membershipd HTTP server in the given auth mode
// with a fresh store + blob store, and seeds one active admin ("alice").
type authHarness struct {
	ts       *httptest.Server
	store    Store
	alice    cs.Identity
	alicePub string // hex
}

func newAuthHarness(t *testing.T, mode AuthMode) *authHarness {
	t.Helper()
	dir := t.TempDir()
	store, err := Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	blobs, err := blobstore.New(filepath.Join(dir, "blobs"))
	if err != nil {
		t.Fatalf("open blobs: %v", err)
	}
	alice, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("identity: %v", err)
	}
	alicePub := hex.EncodeToString(alice.SignPub)
	if err := store.AddUser(alicePub, "alice", RoleAdmin); err != nil {
		t.Fatalf("seed admin: %v", err)
	}
	srv := NewServer(store, blobs, mode)
	ts := httptest.NewServer(srv)
	t.Cleanup(func() {
		ts.Close()
		store.Close()
	})
	return &authHarness{ts: ts, store: store, alice: alice, alicePub: alicePub}
}

// signedReq builds a control-plane request signed by id, with explicit ts/nonce
// so tests can force skew and replay. It signs via the same CanonicalRequest the
// production client uses, so the test verifies the real wire contract.
func signedReq(t *testing.T, base, method, path string, body []byte, id cs.Identity, ts int64, nonce string) *http.Request {
	t.Helper()
	var rdr io.Reader
	if body != nil {
		rdr = bytes.NewReader(body)
	}
	req, err := http.NewRequest(method, base+path, rdr)
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	tss := strconv.FormatInt(ts, 10)
	canonical := CanonicalRequest(method, path, tss, nonce, body)
	sig := cs.SignEd25519(id.SignPriv, canonical)
	req.Header.Set(hdrPub, hex.EncodeToString(id.SignPub))
	req.Header.Set(hdrTs, tss)
	req.Header.Set(hdrNonce, nonce)
	req.Header.Set(hdrSig, base64.StdEncoding.EncodeToString(sig))
	return req
}

func do(t *testing.T, req *http.Request) (int, string) {
	t.Helper()
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request: %v", err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(b)
}

// okPath is a path that authenticates and returns 200 with an empty list when
// the request carries NO membership-bound signer (AuthOff/soft/missing-headers
// tests). Under enforce, the per-endpoint room directory is now restricted to
// the signer's own endpoint (audit H3), so tests that sign as alice use
// aliceRoomsPath instead.
const okPath = "/members/alice-endpoint/rooms"

// aliceRoomsPath is alice's own room directory — the canonical "authenticated
// and authorized" 200 path under enforce after H3.
func aliceRoomsPath(h *authHarness) string {
	return "/members/" + frame.EndpointID(h.alice.SignPub) + "/rooms"
}

// Golden: a request signed by a registered, active identity is accepted.
func TestAuthGoldenAccepted(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	now := time.Now().Unix()
	code, _ := do(t, signedReq(t, h.ts.URL, "GET", aliceRoomsPath(h), nil, h.alice, now, "nonce-golden"))
	if code != http.StatusOK {
		t.Fatalf("golden signed request should be 200, got %d", code)
	}
}

// Error path: a structurally valid signature from an identity that is NOT in the
// allowlist is rejected with 401.
func TestAuthUnregisteredRejected(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	bob, _ := cs.GenerateIdentity()
	now := time.Now().Unix()
	code, body := do(t, signedReq(t, h.ts.URL, "GET", okPath, nil, bob, now, "nonce-bob"))
	if code != http.StatusUnauthorized {
		t.Fatalf("unregistered identity should be 401, got %d (%s)", code, body)
	}
}

// Error path: replaying a captured request (same nonce + signature) is rejected.
func TestAuthReplayRejected(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	now := time.Now().Unix()
	first := signedReq(t, h.ts.URL, "GET", aliceRoomsPath(h), nil, h.alice, now, "nonce-replay")
	if code, body := do(t, first); code != http.StatusOK {
		t.Fatalf("first request should be 200, got %d (%s)", code, body)
	}
	// Identical ts + nonce + signature: a replay.
	second := signedReq(t, h.ts.URL, "GET", aliceRoomsPath(h), nil, h.alice, now, "nonce-replay")
	if code, body := do(t, second); code != http.StatusUnauthorized {
		t.Fatalf("replayed request should be 401, got %d (%s)", code, body)
	}
}

// Error path: a timestamp outside the ±30s window is rejected even with a valid
// signature (defends against long-delayed captured requests).
func TestAuthClockSkewRejected(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	stale := time.Now().Unix() - 120
	code, body := do(t, signedReq(t, h.ts.URL, "GET", okPath, nil, h.alice, stale, "nonce-skew"))
	if code != http.StatusUnauthorized {
		t.Fatalf("clock-skewed request should be 401, got %d (%s)", code, body)
	}
}

// Error path: tampering the body after signing invalidates the signature.
func TestAuthTamperedBodyRejected(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	now := time.Now().Unix()
	req := signedReq(t, h.ts.URL, "POST", "/rooms", []byte(`{"subject":"x"}`), h.alice, now, "nonce-tamper")
	// Swap the body for different bytes the signature does not cover.
	req.Body = io.NopCloser(bytes.NewReader([]byte(`{"subject":"evil"}`)))
	req.ContentLength = int64(len(`{"subject":"evil"}`))
	code, body := do(t, req)
	if code != http.StatusUnauthorized {
		t.Fatalf("tampered body should be 401, got %d (%s)", code, body)
	}
}

// Error path: missing auth headers under enforce are rejected.
func TestAuthMissingHeadersRejected(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	req, _ := http.NewRequest("GET", h.ts.URL+okPath, nil)
	code, _ := do(t, req)
	if code != http.StatusUnauthorized {
		t.Fatalf("unsigned request under enforce should be 401, got %d", code)
	}
}

// Exemption: the health probe bypasses auth even under enforce.
func TestAuthHealthExempt(t *testing.T) {
	h := newAuthHarness(t, AuthEnforce)
	req, _ := http.NewRequest("GET", h.ts.URL+"/healthz", nil)
	code, _ := do(t, req)
	if code != http.StatusOK {
		t.Fatalf("/healthz must be reachable without auth, got %d", code)
	}
}

// Soft mode: an unauthenticated request is logged but allowed through, so
// clients can migrate without an outage.
func TestAuthSoftAllowsUnauthenticated(t *testing.T) {
	h := newAuthHarness(t, AuthSoft)
	req, _ := http.NewRequest("GET", h.ts.URL+okPath, nil)
	code, _ := do(t, req)
	if code != http.StatusOK {
		t.Fatalf("soft mode should allow unsigned request, got %d", code)
	}
}

// Off mode (default for legacy callers): no verification at all.
func TestAuthOffNoVerification(t *testing.T) {
	h := newAuthHarness(t, AuthOff)
	req, _ := http.NewRequest("GET", h.ts.URL+okPath, nil)
	code, _ := do(t, req)
	if code != http.StatusOK {
		t.Fatalf("off mode should allow unsigned request, got %d", code)
	}
}
