package membership

import (
	"context"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"
	"time"

	cs "fn-registry/functions/cybersecurity"

	"github.com/enmanuel/unibus/pkg/blobstore"
	"github.com/enmanuel/unibus/pkg/embeddednats"
	"github.com/enmanuel/unibus/pkg/frame"
	"github.com/nats-io/nats.go"
	"github.com/nats-io/nats.go/jetstream"
)

// historyHarness is an enforce-mode control plane wired to a real embedded NATS
// JetStream, so the history path exercises the production code: the server ensures
// and reads actual durable streams. alice is a seeded admin (and any room's owner),
// bob is a registered user added as a room member, and carol is a registered user
// that is NOT a member of the test room (to exercise the 403 path).
type historyHarness struct {
	ts    *httptest.Server
	store Store
	js    jetstream.JetStream
	nc    *nats.Conn
	alice cs.Identity // admin + room owner
	bob   cs.Identity // room member
	carol cs.Identity // registered, non-member
}

func newHistoryHarness(t *testing.T) *historyHarness {
	t.Helper()
	dir := t.TempDir()
	ns, err := embeddednats.StartServer(embeddednats.ServerConfig{
		StoreDir: filepath.Join(dir, "jetstream"),
		Host:     "127.0.0.1",
		Port:     kvFreePort(t),
	})
	if err != nil {
		t.Fatalf("embedded nats: %v", err)
	}
	nc, err := nats.Connect(ns.ClientURL())
	if err != nil {
		ns.Shutdown()
		t.Fatalf("nats connect: %v", err)
	}
	js, err := jetstream.New(nc)
	if err != nil {
		nc.Close()
		ns.Shutdown()
		t.Fatalf("jetstream: %v", err)
	}
	store, err := Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		nc.Close()
		ns.Shutdown()
		t.Fatalf("open store: %v", err)
	}
	blobs, err := blobstore.New(filepath.Join(dir, "blobs"))
	if err != nil {
		store.Close()
		nc.Close()
		ns.Shutdown()
		t.Fatalf("open blobs: %v", err)
	}
	mustID := func(name string) cs.Identity {
		id, err := cs.GenerateIdentity()
		if err != nil {
			t.Fatalf("identity %s: %v", name, err)
		}
		return id
	}
	alice, bob, carol := mustID("alice"), mustID("bob"), mustID("carol")
	if err := store.AddUser(hex.EncodeToString(alice.SignPub), "alice", RoleAdmin); err != nil {
		t.Fatalf("seed admin: %v", err)
	}
	for _, u := range []struct {
		id     cs.Identity
		handle string
	}{{bob, "bob"}, {carol, "carol"}} {
		if err := store.AddUser(hex.EncodeToString(u.id.SignPub), u.handle, RoleMember); err != nil {
			t.Fatalf("register %s: %v", u.handle, err)
		}
	}

	srv := NewServer(store, blobs, AuthEnforce)
	srv.SetJetStream(js, 1)
	ts := httptest.NewServer(srv)
	t.Cleanup(func() {
		ts.Close()
		store.Close()
		nc.Close()
		ns.Shutdown()
		ns.WaitForShutdown()
	})
	return &historyHarness{ts: ts, store: store, js: js, nc: nc, alice: alice, bob: bob, carol: carol}
}

// seedPersistRoom creates a persisted (Matrix-policy) room directly in the store
// with alice as owner and bob as a member, returning its id and subject. It does
// NOT create the stream — that is left to the code under test (handleCreateRoom or
// the lazy ensure in the history endpoint), which is exactly what we want to verify.
func (h *historyHarness) seedPersistRoom(t *testing.T) (roomID, subject string) {
	t.Helper()
	roomID = newULID()
	subject = "unibus.room." + roomID
	aliceEp := frame.EndpointID(h.alice.SignPub)
	info := RoomInfo{RoomID: roomID, Subject: subject, OwnerEndpoint: aliceEp, Encrypt: true, Persist: true}
	if err := h.store.CreateRoom(info, h.alice.SignPub, h.alice.KexPub, []byte("alice-sealed")); err != nil {
		t.Fatalf("seed room: %v", err)
	}
	bobEp := frame.EndpointID(h.bob.SignPub)
	bobM := Member{Endpoint: bobEp, Role: RoleMember, SignPub: h.bob.SignPub, KexPub: h.bob.KexPub}
	if err := h.store.AddMember(roomID, bobM, 0, []byte("bob-sealed")); err != nil {
		t.Fatalf("add member bob: %v", err)
	}
	return roomID, subject
}

// makeFrame builds a marshaled PUB frame whose payload identifies it, so a test can
// assert exact bytes and ordering after a round trip through the stream + endpoint.
func makeFrame(t *testing.T, subject, sender string, i int) []byte {
	t.Helper()
	f := frame.Frame{
		Type:    frame.PUB,
		Subject: subject,
		Sender:  sender,
		MsgID:   fmt.Sprintf("msg-%02d", i),
		Payload: []byte(fmt.Sprintf("ciphertext-%02d", i)),
	}
	b, err := f.Marshal()
	if err != nil {
		t.Fatalf("marshal frame %d: %v", i, err)
	}
	return b
}

// getHistory signs a GET /rooms/{id}/history request as id and returns the status,
// the raw body, and the decoded envelope. query is the raw query string (e.g.
// "limit=2") or "". The signed path includes the query because the server verifies
// the signature over r.URL.RequestURI(), which carries it.
func (h *historyHarness) getHistory(t *testing.T, id cs.Identity, roomID, query string, n int) (int, string, historyResp) {
	t.Helper()
	path := "/rooms/" + roomID + "/history"
	if query != "" {
		path += "?" + query
	}
	req := signedReq(t, h.ts.URL, "GET", path, nil, id, time.Now().Unix(), nonceN(n))
	code, body := do(t, req)
	var out historyResp
	if code == 200 {
		if err := json.Unmarshal([]byte(body), &out); err != nil {
			t.Fatalf("decode history: %v (%s)", err, body)
		}
	}
	return code, body, out
}

// TestCreateRoomEnsuresStream verifies handleCreateRoom creates the durable stream
// for a persisted room before responding, so capture starts at room creation.
func TestCreateRoomEnsuresStream(t *testing.T) {
	h := newHistoryHarness(t)
	aliceEp := frame.EndpointID(h.alice.SignPub)
	reqBody := createRoomReq{
		Subject:       "unibus.room.created",
		Policy:        policyJSON{Encrypt: true, Persist: true},
		Owner:         endpointJSON{Endpoint: aliceEp, SignPub: h.alice.SignPub, KexPub: h.alice.KexPub},
		SealedKeySelf: []byte("alice-sealed"),
	}
	body, _ := json.Marshal(reqBody)
	req := signedReq(t, h.ts.URL, "POST", "/rooms", body, h.alice, time.Now().Unix(), nonceN(1))
	code, respBody := do(t, req)
	if code != 201 {
		t.Fatalf("create room: want 201, got %d (%s)", code, respBody)
	}
	var cr createRoomResp
	if err := json.Unmarshal([]byte(respBody), &cr); err != nil {
		t.Fatalf("decode create resp: %v (%s)", err, respBody)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	if _, err := h.js.Stream(ctx, roomStreamName(cr.RoomID)); err != nil {
		t.Fatalf("stream for created persist room should exist: %v", err)
	}
}

// TestRoomHistoryGolden is the golden path: three frames published to a persisted
// room's stream come back from the endpoint base64-encoded, in chronological order,
// and decode to the exact frames that were published.
func TestRoomHistoryGolden(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, subject := h.seedPersistRoom(t)
	bobEp := frame.EndpointID(h.bob.SignPub)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := ensureRoomStream(ctx, h.js, roomID, subject, 1); err != nil {
		t.Fatalf("ensure stream: %v", err)
	}
	want := make([][]byte, 3)
	for i := 0; i < 3; i++ {
		want[i] = makeFrame(t, subject, bobEp, i)
		// js.Publish waits for the stream ack, so the message is durably stored before
		// the next iteration — no sleeps, deterministic ordering.
		if _, err := h.js.Publish(ctx, subject, want[i]); err != nil {
			t.Fatalf("publish %d: %v", i, err)
		}
	}

	code, raw, hr := h.getHistory(t, h.bob, roomID, "", 10)
	if code != 200 {
		t.Fatalf("history: want 200, got %d (%s)", code, raw)
	}
	if len(hr.Messages) != 3 {
		t.Fatalf("want 3 messages, got %d (%s)", len(hr.Messages), raw)
	}
	for i, m := range hr.Messages {
		decoded, err := base64.StdEncoding.DecodeString(m)
		if err != nil {
			t.Fatalf("message %d not valid base64: %v", i, err)
		}
		if string(decoded) != string(want[i]) {
			t.Fatalf("message %d bytes mismatch (order or content)", i)
		}
		f, err := frame.Unmarshal(decoded)
		if err != nil {
			t.Fatalf("message %d does not decode to a frame: %v", i, err)
		}
		if f.MsgID != fmt.Sprintf("msg-%02d", i) {
			t.Fatalf("message %d: want MsgID msg-%02d, got %q", i, i, f.MsgID)
		}
	}
}

// TestRoomHistoryCapturesCoreNATSPublish proves the central fix: a message
// published over PLAIN core NATS (as the JetStream-less browser client uniweb does)
// is captured by the server-owned stream and served by the endpoint. Without the
// server ensuring the stream, this message would be captured nowhere.
func TestRoomHistoryCapturesCoreNATSPublish(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, subject := h.seedPersistRoom(t)
	bobEp := frame.EndpointID(h.bob.SignPub)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := ensureRoomStream(ctx, h.js, roomID, subject, 1); err != nil {
		t.Fatalf("ensure stream: %v", err)
	}
	sent := makeFrame(t, subject, bobEp, 7)
	if err := h.nc.Publish(subject, sent); err != nil {
		t.Fatalf("core publish: %v", err)
	}
	if err := h.nc.Flush(); err != nil {
		t.Fatalf("flush: %v", err)
	}
	// Core NATS publish has no stream ack; poll the stream until the message lands.
	h.waitMsgs(t, roomID, 1)

	code, raw, hr := h.getHistory(t, h.bob, roomID, "", 11)
	if code != 200 {
		t.Fatalf("history: want 200, got %d (%s)", code, raw)
	}
	if len(hr.Messages) != 1 {
		t.Fatalf("want 1 captured message, got %d (%s)", len(hr.Messages), raw)
	}
	decoded, err := base64.StdEncoding.DecodeString(hr.Messages[0])
	if err != nil || string(decoded) != string(sent) {
		t.Fatalf("captured core-NATS message round-trip mismatch (err=%v)", err)
	}
}

// TestRoomHistoryLimit verifies ?limit caps the response to the most recent N
// messages, oldest→newest within the window.
func TestRoomHistoryLimit(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, subject := h.seedPersistRoom(t)
	bobEp := frame.EndpointID(h.bob.SignPub)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := ensureRoomStream(ctx, h.js, roomID, subject, 1); err != nil {
		t.Fatalf("ensure stream: %v", err)
	}
	for i := 0; i < 5; i++ {
		if _, err := h.js.Publish(ctx, subject, makeFrame(t, subject, bobEp, i)); err != nil {
			t.Fatalf("publish %d: %v", i, err)
		}
	}

	code, raw, hr := h.getHistory(t, h.bob, roomID, "limit=2", 12)
	if code != 200 {
		t.Fatalf("history: want 200, got %d (%s)", code, raw)
	}
	if len(hr.Messages) != 2 {
		t.Fatalf("limit=2 over 5 messages: want 2, got %d", len(hr.Messages))
	}
	// The window is the last two messages (indices 3 and 4), in order.
	for off, m := range hr.Messages {
		decoded, _ := base64.StdEncoding.DecodeString(m)
		f, err := frame.Unmarshal(decoded)
		if err != nil {
			t.Fatalf("limited message %d does not decode: %v", off, err)
		}
		want := fmt.Sprintf("msg-%02d", off+3)
		if f.MsgID != want {
			t.Fatalf("limited message %d: want MsgID %s, got %q", off, want, f.MsgID)
		}
	}
}

// TestRoomHistoryEmptyRoom verifies a persisted room with no messages returns an
// empty (non-null) array, lazily ensuring the stream on the way.
func TestRoomHistoryEmptyRoom(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, _ := h.seedPersistRoom(t)

	code, raw, hr := h.getHistory(t, h.bob, roomID, "", 13)
	if code != 200 {
		t.Fatalf("history: want 200, got %d (%s)", code, raw)
	}
	if hr.Messages == nil {
		t.Fatalf("empty room must return [] not null (%s)", raw)
	}
	if len(hr.Messages) != 0 {
		t.Fatalf("empty room: want 0 messages, got %d", len(hr.Messages))
	}
	// The lazy ensure should have created the stream even though no message exists.
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	if _, err := h.js.Stream(ctx, roomStreamName(roomID)); err != nil {
		t.Fatalf("lazy ensure should have created the stream: %v", err)
	}
}

// TestRoomHistoryUnauthenticated verifies an unsigned request is rejected with 401
// under enforce, before the handler runs.
func TestRoomHistoryUnauthenticated(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, _ := h.seedPersistRoom(t)
	// No signing headers: plain GET against the enforce-mode control plane.
	req, err := http.NewRequest("GET", h.ts.URL+"/rooms/"+roomID+"/history", nil)
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	code, body := do(t, req)
	if code != 401 {
		t.Fatalf("unauthenticated history: want 401, got %d (%s)", code, body)
	}
}

// TestRoomHistoryNonMember verifies a registered user who is NOT a member of the
// room is rejected with 403.
func TestRoomHistoryNonMember(t *testing.T) {
	h := newHistoryHarness(t)
	roomID, _ := h.seedPersistRoom(t)
	code, body, _ := h.getHistory(t, h.carol, roomID, "", 14)
	if code != 403 {
		t.Fatalf("non-member history: want 403, got %d (%s)", code, body)
	}
}

// TestRoomHistoryRoomNotFound verifies a request for a non-existent room is a 404,
// distinct from the 403 a non-member of an existing room gets.
func TestRoomHistoryRoomNotFound(t *testing.T) {
	h := newHistoryHarness(t)
	code, body, _ := h.getHistory(t, h.alice, newULID(), "", 15)
	if code != 404 {
		t.Fatalf("missing room history: want 404, got %d (%s)", code, body)
	}
}

// waitMsgs polls the room's stream until it holds at least want messages or a short
// deadline elapses, so a core-NATS publish (which carries no stream ack) is observed
// deterministically without a fixed sleep.
func (h *historyHarness) waitMsgs(t *testing.T, roomID string, want uint64) {
	t.Helper()
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		st, err := h.js.Stream(ctx, roomStreamName(roomID))
		if err == nil {
			si, ierr := st.Info(ctx)
			if ierr == nil && si.State.Msgs >= want {
				cancel()
				return
			}
		}
		cancel()
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("stream for room %s never reached %d message(s)", roomID, want)
}
