package membership_test

import (
	"encoding/hex"
	"net"
	"net/http/httptest"
	"path/filepath"
	"testing"
	"time"

	cs "fn-registry/functions/cybersecurity"

	"github.com/enmanuel/unibus/pkg/blobstore"
	"github.com/enmanuel/unibus/pkg/busauth"
	"github.com/enmanuel/unibus/pkg/client"
	"github.com/enmanuel/unibus/pkg/embeddednats"
	"github.com/enmanuel/unibus/pkg/frame"
	"github.com/enmanuel/unibus/pkg/membership"
	"github.com/nats-io/nats.go"
	server "github.com/nats-io/nats-server/v2/server"
)

func aclFreePort(t *testing.T) int {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("free port: %v", err)
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port
}

func mustID(t *testing.T) cs.Identity {
	t.Helper()
	id, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("identity: %v", err)
	}
	return id
}

// aclPermsFunc builds the per-subject PermissionsFunc the ACL authenticator
// expects. It delegates to the SAME production wiring membershipd uses
// (busauth.PermissionsFromSubjects over membership.SubjectACLFor), so this test
// exercises the real path rather than a test-only reimplementation.
func aclPermsFunc(store membership.Store) busauth.PermissionsFunc {
	return busauth.PermissionsFromSubjects(membership.SubjectACLFor(store))
}

// startACLNats boots an embedded NATS whose authenticator confines each peer to
// the subjects of the rooms it belongs to (audit H4 residual).
func startACLNats(t *testing.T, store membership.Store) *server.Server {
	t.Helper()
	auth := busauth.NewNkeyAuthenticatorACL(store.IsAuthorized, aclPermsFunc(store))
	ns, err := embeddednats.StartServer(embeddednats.ServerConfig{
		StoreDir: t.TempDir(), Host: "127.0.0.1", Port: aclFreePort(t), Auth: auth,
	})
	if err != nil {
		t.Fatalf("acl nats: %v", err)
	}
	t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() })
	return ns
}

func nkeyConn(t *testing.T, natsURL string, id cs.Identity, errCh chan error) *nats.Conn {
	t.Helper()
	pub, sign, err := busauth.ClientNkey(id.SignPriv)
	if err != nil {
		t.Fatalf("nkey: %v", err)
	}
	nc, err := nats.Connect(natsURL,
		nats.Nkey(pub, sign),
		nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, e error) {
			select {
			case errCh <- e:
			default:
			}
		}),
	)
	if err != nil {
		t.Fatalf("connect nkey: %v", err)
	}
	t.Cleanup(nc.Close)
	return nc
}

func mustAddUser(t *testing.T, store membership.Store, id cs.Identity, handle string) {
	t.Helper()
	if err := store.AddUser(hex.EncodeToString(id.SignPub), handle, membership.RoleMember); err != nil {
		t.Fatalf("add user %s: %v", handle, err)
	}
}

func mustCreateRoom(t *testing.T, store membership.Store, roomID, subject, ownerEP string, owner cs.Identity) {
	t.Helper()
	info := membership.RoomInfo{RoomID: roomID, Subject: subject, OwnerEndpoint: ownerEP}
	if err := store.CreateRoom(info, owner.SignPub, owner.KexPub, nil); err != nil {
		t.Fatalf("create room %s: %v", roomID, err)
	}
}

func newCtrl(t *testing.T, store membership.Store, blobs blobstore.Store) string {
	t.Helper()
	ts := httptest.NewServer(membership.NewServer(store, blobs, membership.AuthOff))
	t.Cleanup(ts.Close)
	return ts.URL
}

func waitErr(ch chan error, d time.Duration) error {
	select {
	case e := <-ch:
		return e
	case <-time.After(d):
		return nil
	}
}

func drain(ch chan error) {
	for {
		select {
		case <-ch:
		default:
			return
		}
	}
}

// TestSubjectACLIsolation closes the audit H4 residual: a registered peer is
// confined to the subjects of the rooms it belongs to. alice (member of room.A)
// may sub/pub room.A but is DENIED sub/pub on room.B, and never reads what bob
// (member of room.B) publishes there.
func TestSubjectACLIsolation(t *testing.T) {
	dir := t.TempDir()
	store, err := membership.Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { store.Close() })

	alice, bob := mustID(t), mustID(t)
	aliceEP, bobEP := frame.EndpointID(alice.SignPub), frame.EndpointID(bob.SignPub)
	mustAddUser(t, store, alice, "alice")
	mustAddUser(t, store, bob, "bob")
	const subjA, subjB = "room.acl.a", "room.acl.b"
	mustCreateRoom(t, store, "ROOMA", subjA, aliceEP, alice)
	mustCreateRoom(t, store, "ROOMB", subjB, bobEP, bob)

	srv := startACLNats(t, store)
	url := srv.ClientURL()
	aliceErr := make(chan error, 4)
	bobErr := make(chan error, 4)
	aliceNC := nkeyConn(t, url, alice, aliceErr)
	bobNC := nkeyConn(t, url, bob, bobErr)

	// alice may subscribe to her own room (no error).
	aliceGot := make(chan string, 4)
	if _, err := aliceNC.Subscribe(subjA, func(m *nats.Msg) { aliceGot <- string(m.Data) }); err != nil {
		t.Fatalf("alice sub A: %v", err)
	}
	_ = aliceNC.Flush()
	if e := waitErr(aliceErr, 300*time.Millisecond); e != nil {
		t.Fatalf("alice sub to her OWN room raised an error: %v", e)
	}

	// alice subscribing to bob's room is a permissions violation.
	if _, err := aliceNC.Subscribe(subjB, func(m *nats.Msg) { aliceGot <- "LEAK:" + string(m.Data) }); err != nil {
		t.Fatalf("alice sub B (queue): %v", err)
	}
	_ = aliceNC.Flush()
	if e := waitErr(aliceErr, 1*time.Second); e == nil {
		t.Fatalf("alice subscribing to bob's room should raise a permissions violation")
	}

	// bob publishes in his room; alice (denied) must not receive it.
	bobGot := make(chan string, 4)
	if _, err := bobNC.Subscribe(subjB, func(m *nats.Msg) { bobGot <- string(m.Data) }); err != nil {
		t.Fatalf("bob sub B: %v", err)
	}
	_ = bobNC.Flush()
	if err := bobNC.Publish(subjB, []byte("internal-bob")); err != nil {
		t.Fatalf("bob pub B: %v", err)
	}
	_ = bobNC.Flush()
	select {
	case got := <-bobGot:
		if got != "internal-bob" {
			t.Fatalf("bob got %q", got)
		}
	case <-time.After(2 * time.Second):
		t.Fatalf("bob did not receive his own message")
	}
	select {
	case leak := <-aliceGot:
		t.Fatalf("alice received bob's room traffic despite the ACL: %q", leak)
	case <-time.After(500 * time.Millisecond):
		// good: alice never got it
	}

	// alice publishing into bob's room is denied; bob must not receive it.
	drain(aliceErr)
	if err := aliceNC.Publish(subjB, []byte("intruder")); err != nil {
		t.Fatalf("alice pub B (queue): %v", err)
	}
	_ = aliceNC.Flush()
	if e := waitErr(aliceErr, 1*time.Second); e == nil {
		t.Fatalf("alice publishing into bob's room should raise a permissions violation")
	}
	select {
	case got := <-bobGot:
		t.Fatalf("bob received alice's cross-room publish despite the ACL: %q", got)
	case <-time.After(500 * time.Millisecond):
		// good
	}
}

// TestReaudit_H4_WildcardMetadataLeak ports the re-auditor's H4 vector. Before
// the per-subject ACL was WIRED into membershipd (it existed in pkg/membership and
// pkg/busauth but the binary used the plain NewNkeyAuthenticator), a registered
// NON-member could open a raw NATS connection, Subscribe(">"), and capture every
// room's subject plus JetStream stream/advisory activity — the payload stayed E2E
// ciphertext, but the metadata leaked. With NewNkeyAuthenticatorACL wired via the
// production path (busauth.PermissionsFromSubjects(membership.SubjectACLFor)), a
// non-member is confined to the client-infra subjects, so the wildcard and any
// foreign room subject are denied.
//
// Coverage:
//   - error : a non-member's Subscribe(">") raises a permission violation;
//   - edge  : a non-member subscribing to another room's exact subject is denied;
//   - golden: the member still pub/subs her own room, and the non-member never
//     captures that traffic.
//
// Residual now CLOSED (issue 0006b, audit 0008 N2): the client-infra grant no
// longer includes "$JS.API.>". JetStream API access is granted per-room only
// (membership.jsSubjectsFor), so a peer can reach the API of its OWN rooms'
// streams but not the control-plane KV buckets (KV_UNIBUS_*) nor another room's
// stream. See TestAttack0008_N2 for the closed-leak regression.
func TestReaudit_H4_WildcardMetadataLeak(t *testing.T) {
	dir := t.TempDir()
	store, err := membership.Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { store.Close() })

	alice, eve := mustID(t), mustID(t)
	aliceEP := frame.EndpointID(alice.SignPub)
	mustAddUser(t, store, alice, "alice")
	mustAddUser(t, store, eve, "eve") // eve is REGISTERED but never a member of alice's room
	const subject = "room.e2e.confidential"
	mustCreateRoom(t, store, "ROOMA", subject, aliceEP, alice)

	srv := startACLNats(t, store)
	url := srv.ClientURL()

	eveErr := make(chan error, 8)
	eveNC := nkeyConn(t, url, eve, eveErr)
	eveAll := make(chan *nats.Msg, 16)

	// Error: eve's wildcard subscription is rejected. nats.go creates the local sub
	// object and the server rejects it asynchronously (delivered to ErrorHandler).
	if _, err := eveNC.Subscribe(">", func(m *nats.Msg) { eveAll <- m }); err != nil {
		t.Fatalf("eve sub >: %v", err)
	}
	_ = eveNC.Flush()
	if e := waitErr(eveErr, 1*time.Second); e == nil {
		t.Fatalf("a non-member's Subscribe(\">\") must raise a permissions violation (wildcard metadata leak still open)")
	}

	// Edge: eve subscribing to the foreign room's EXACT subject is also denied.
	drain(eveErr)
	if _, err := eveNC.Subscribe(subject, func(m *nats.Msg) { eveAll <- m }); err != nil {
		t.Fatalf("eve sub subject: %v", err)
	}
	_ = eveNC.Flush()
	if e := waitErr(eveErr, 1*time.Second); e == nil {
		t.Fatalf("a non-member subscribing to another room's subject must be denied")
	}

	// Golden: alice (the member) pub/subs her own room with no violation, and eve
	// never captured the traffic despite her (rejected) wildcard.
	aliceErr := make(chan error, 4)
	aliceNC := nkeyConn(t, url, alice, aliceErr)
	aliceGot := make(chan string, 4)
	if _, err := aliceNC.Subscribe(subject, func(m *nats.Msg) { aliceGot <- string(m.Data) }); err != nil {
		t.Fatalf("alice sub own room: %v", err)
	}
	_ = aliceNC.Flush()
	if e := waitErr(aliceErr, 300*time.Millisecond); e != nil {
		t.Fatalf("alice subscribing to her OWN room raised an error: %v", e)
	}
	if err := aliceNC.Publish(subject, []byte("members-only metadata")); err != nil {
		t.Fatalf("alice publish: %v", err)
	}
	_ = aliceNC.Flush()
	select {
	case got := <-aliceGot:
		if got != "members-only metadata" {
			t.Fatalf("alice got %q", got)
		}
	case <-time.After(2 * time.Second):
		t.Fatalf("alice did not receive her own room's message")
	}
	select {
	case m := <-eveAll:
		t.Fatalf("eve captured room traffic despite the ACL: subject=%q data=%q", m.Subject, m.Data)
	case <-time.After(500 * time.Millisecond):
		// good: eve captured nothing
	}
}

// TestRefreshSessionGainsNewRoom is the "permissions refreshed on join" path:
// alice is not in room B, so her connection has no permission for its subject;
// after she is added to room B and calls RefreshSession, the reconnect
// re-derives her permissions and she gains the room's subject.
func TestRefreshSessionGainsNewRoom(t *testing.T) {
	dir := t.TempDir()
	store, err := membership.Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { store.Close() })

	alice, bob := mustID(t), mustID(t)
	aliceEP, bobEP := frame.EndpointID(alice.SignPub), frame.EndpointID(bob.SignPub)
	mustAddUser(t, store, alice, "alice")
	mustAddUser(t, store, bob, "bob")
	const subjB = "room.refresh.b"
	mustCreateRoom(t, store, "ROOMB", subjB, bobEP, bob)

	srv := startACLNats(t, store)
	blobs, _ := blobstore.New(filepath.Join(dir, "blobs"))
	ctrl := newCtrl(t, store, blobs)

	aliceC, err := client.NewWithOptions(srv.ClientURL(), ctrl, alice, client.Options{UseNkey: true})
	if err != nil {
		t.Fatalf("connect alice: %v", err)
	}
	defer aliceC.Close()

	// Add alice to room B (as if invited), then RefreshSession so the
	// authenticator re-derives her permissions on reconnect.
	if _, err := store.GetMember("ROOMB", aliceEP); err == nil {
		t.Fatalf("alice should not be a member yet")
	}
	if err := store.AddMember("ROOMB", membership.Member{Endpoint: aliceEP, Role: "member", SignPub: alice.SignPub, KexPub: alice.KexPub}, 1, nil); err != nil {
		t.Fatalf("add alice to room B: %v", err)
	}
	if err := aliceC.RefreshSession(); err != nil {
		t.Fatalf("refresh session: %v", err)
	}

	bobErr := make(chan error, 2)
	bobNC := nkeyConn(t, srv.ClientURL(), bob, bobErr)

	got := make(chan string, 2)
	sub, err := aliceC.Subscribe("ROOMB", func(_ frame.Frame, plaintext []byte) { got <- string(plaintext) })
	if err != nil {
		t.Fatalf("alice subscribe room B after refresh: %v", err)
	}
	defer sub.Unsubscribe()
	time.Sleep(200 * time.Millisecond)

	// bob publishes a minimal cleartext frame on subjB.
	f := frame.Frame{Type: frame.PUB, Subject: subjB, Sender: bobEP, MsgID: "m1", Payload: []byte("hello-after-join")}
	b, _ := f.Marshal()
	if err := bobNC.Publish(subjB, b); err != nil {
		t.Fatalf("bob publish: %v", err)
	}
	_ = bobNC.Flush()

	select {
	case msg := <-got:
		if msg != "hello-after-join" {
			t.Fatalf("alice got %q", msg)
		}
	case <-time.After(3 * time.Second):
		t.Fatalf("alice did not receive room B traffic after RefreshSession (permissions not refreshed)")
	}
}
