package main

// Wiring tests for issue 0006c: --store kv selects the replicated JetStream KV
// control plane, the authenticator serves from it through the storeHolder, and a
// new node sees state created by another (the divergence that per-node SQLite
// caused — audit 0008 N5 — is gone). Branch-by-abstraction is verified elsewhere
// (the SQLite default path is the unchanged baseline covered by the existing
// suite).

import (
	"encoding/hex"
	"testing"
	"time"

	cs "fn-registry/functions/cybersecurity"

	"github.com/enmanuel/unibus/pkg/busauth"
	"github.com/enmanuel/unibus/pkg/embeddednats"
	"github.com/enmanuel/unibus/pkg/membership"
	"github.com/nats-io/nats.go"
	"github.com/nats-io/nats.go/jetstream"
)

// TestKVStoreBootstrapUnderEnforce drives the exact decentralized boot the binary
// performs: build the authenticator over an empty holder, start NATS, open the
// privileged internal connection, open the KV store, publish it into the holder,
// then a real bus user (seeded into the KV store) authenticates over nkey. This
// proves the bootstrap cycle is broken correctly — the KV-backed control plane
// authorizes live clients under enforce.
func TestKVStoreBootstrapUnderEnforce(t *testing.T) {
	internalID, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("internal identity: %v", err)
	}
	holder := &storeHolder{}
	auth := busauth.NewNkeyAuthenticatorACLInternal(
		holder.IsAuthorized,
		busauth.PermissionsFromSubjects(holder.subjectACL),
		hex.EncodeToString(internalID.SignPub),
	)
	ns, err := embeddednats.StartServer(embeddednats.ServerConfig{
		StoreDir: t.TempDir(), Host: "127.0.0.1", Port: freePort(t), Auth: auth,
	})
	if err != nil {
		t.Fatalf("nats: %v", err)
	}
	t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() })

	// Privileged internal connection opens the KV store while the holder still
	// denies every normal client.
	intNC, js, err := connectInternalJS(ns, internalID, true)
	if err != nil {
		t.Fatalf("connectInternalJS: %v", err)
	}
	t.Cleanup(intNC.Close)
	kvStore, err := membership.OpenJetStream(js, membership.JetStreamConfig{Replicas: 1, OpTimeout: 3 * time.Second})
	if err != nil {
		t.Fatalf("open kv store: %v", err)
	}
	holder.set(kvStore)

	// Seed a bus user into the KV control plane.
	alice, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("alice: %v", err)
	}
	if err := kvStore.AddUser(hex.EncodeToString(alice.SignPub), "alice", membership.RoleMember); err != nil {
		t.Fatalf("seed alice: %v", err)
	}

	// alice authenticates over nkey — authorized via the KV store through the holder.
	pub, sign, err := busauth.ClientNkey(alice.SignPriv)
	if err != nil {
		t.Fatalf("alice nkey: %v", err)
	}
	aliceNC, err := nats.Connect(ns.ClientURL(), nats.Nkey(pub, sign), nats.MaxReconnects(0), nats.Timeout(2*time.Second))
	if err != nil {
		t.Fatalf("alice (KV-authorized) must connect under enforce: %v", err)
	}
	aliceNC.Close()

	// An outsider not in the KV store is denied (fail closed).
	outsider, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("outsider: %v", err)
	}
	opub, osign, err := busauth.ClientNkey(outsider.SignPriv)
	if err != nil {
		t.Fatalf("outsider nkey: %v", err)
	}
	if oc, err := nats.Connect(ns.ClientURL(), nats.Nkey(opub, osign), nats.MaxReconnects(0), nats.Timeout(2*time.Second)); err == nil {
		oc.Close()
		t.Fatalf("an outsider absent from the KV store must be rejected")
	}
}

// TestKVStoreDecentralizedConsistency: a room/user created via one node's KV store
// is immediately visible to another node's KV store over the same JetStream — the
// shared, replicated control plane that ends the per-node SQLite divergence.
func TestKVStoreDecentralizedConsistency(t *testing.T) {
	ns, err := embeddednats.StartServer(embeddednats.ServerConfig{
		StoreDir: t.TempDir(), Host: "127.0.0.1", Port: freePort(t),
	})
	if err != nil {
		t.Fatalf("nats: %v", err)
	}
	t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() })

	open := func() membership.Store {
		nc, err := nats.Connect(ns.ClientURL())
		if err != nil {
			t.Fatalf("connect: %v", err)
		}
		t.Cleanup(nc.Close)
		js, err := jetstream.New(nc)
		if err != nil {
			t.Fatalf("jetstream: %v", err)
		}
		st, err := membership.OpenJetStream(js, membership.JetStreamConfig{Replicas: 1, OpTimeout: 3 * time.Second})
		if err != nil {
			t.Fatalf("open kv: %v", err)
		}
		return st
	}
	nodeA := open()
	nodeB := open()

	owner, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("owner: %v", err)
	}
	ownerPub := hex.EncodeToString(owner.SignPub)
	if err := nodeA.AddUser(ownerPub, "owner", membership.RoleAdmin); err != nil {
		t.Fatalf("nodeA add user: %v", err)
	}
	if err := nodeA.CreateRoom(
		membership.RoomInfo{RoomID: "ROOMX", Subject: "room.shared.x", OwnerEndpoint: "owner-ep"},
		owner.SignPub, owner.KexPub, nil,
	); err != nil {
		t.Fatalf("nodeA create room: %v", err)
	}

	// nodeB (a different connection, same buckets) sees both immediately.
	if !nodeB.IsAuthorized(ownerPub) {
		t.Fatalf("nodeB must see the user created on nodeA (decentralized state divergence)")
	}
	got, err := nodeB.GetRoom("ROOMX")
	if err != nil {
		t.Fatalf("nodeB must see the room created on nodeA: %v", err)
	}
	if got.Subject != "room.shared.x" {
		t.Fatalf("nodeB read wrong room subject: %q", got.Subject)
	}
}
