package client_test

import (
	"sync"
	"testing"
	"time"

	"github.com/enmanuel/unibus/pkg/client"
	"github.com/enmanuel/unibus/pkg/frame"
	"github.com/enmanuel/unibus/pkg/room"
	"github.com/nats-io/nats.go"
)

// TestReaudit_SigNilSpoof ports the re-auditor's N3 (Alto) finding: in a room
// that REQUIRES per-message signatures, an attacker with data-plane access
// publishes a raw frame with Sig==nil and a forged Sender. Before the fix
// processFrame verified the signature only when one was present
// (`SignMsgs && f.Sig != nil`), so the receiver accepted the unsigned, forged
// frame as authentic. The fix drops any unsigned frame in a SignMsgs room.
//
// Coverage:
//   - golden: a properly signed frame from a real member IS delivered;
//   - error : an unsigned frame with a forged Sender in a SignMsgs room is DROPPED;
//   - edge  : a room WITHOUT SignMsgs still delivers an unsigned frame (the drop
//     is specific to signed rooms, not a blanket reject of unsigned frames).
func TestReaudit_SigNilSpoof(t *testing.T) {
	h := newHarness(t)
	waitHealth(t, h.ctrlURL)

	alice, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
	if err != nil {
		t.Fatalf("connect alice: %v", err)
	}
	defer alice.Close()
	bob, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
	if err != nil {
		t.Fatalf("connect bob: %v", err)
	}
	defer bob.Close()

	// A signed-but-NOT-encrypted room: SignMsgs enforces authorship, and the lack
	// of encryption is exactly the case the auditor flagged as Alto (any peer with
	// the subject can forge a sender if signatures are not strictly required).
	const subject = "room.signed.spoof"
	signedPolicy := room.Policy{Encrypt: false, Persist: false, SignMsgs: true}
	roomID, err := alice.CreateRoom(subject, signedPolicy)
	if err != nil {
		t.Fatalf("alice create signed room: %v", err)
	}
	if err := alice.Invite(roomID, bob.Endpoint()); err != nil {
		t.Fatalf("alice invite bob: %v", err)
	}
	if err := bob.Join(roomID); err != nil {
		t.Fatalf("bob join: %v", err)
	}

	var mu sync.Mutex
	var got []string
	sub, err := bob.Subscribe(roomID, func(_ frame.Frame, plaintext []byte) {
		mu.Lock()
		got = append(got, string(plaintext))
		mu.Unlock()
	})
	if err != nil {
		t.Fatalf("bob subscribe: %v", err)
	}
	defer sub.Unsubscribe()
	time.Sleep(150 * time.Millisecond)

	// Attacker: a raw NATS connection (the dev harness leaves the data plane open),
	// no identity, forged Sender, NO signature.
	const spoofMsg = "I am totally the victim"
	rawAtk, err := nats.Connect(h.natsURL)
	if err != nil {
		t.Fatalf("attacker raw connect: %v", err)
	}
	defer rawAtk.Close()
	spoof := frame.Frame{
		Type:    frame.PUB,
		Subject: subject,
		Sender:  "victim-forged-endpoint",
		MsgID:   "spoof-1",
		Epoch:   1,
		Payload: []byte(spoofMsg),
		// Sig intentionally nil — this is the attack.
	}
	sb, err := spoof.Marshal()
	if err != nil {
		t.Fatalf("marshal spoof: %v", err)
	}
	if err := rawAtk.Publish(subject, sb); err != nil {
		t.Fatalf("attacker publish: %v", err)
	}
	_ = rawAtk.Flush()

	// Golden: alice's properly signed frame must be delivered.
	const goodMsg = "authentic from alice"
	if err := alice.Publish(roomID, []byte(goodMsg)); err != nil {
		t.Fatalf("alice publish: %v", err)
	}
	if !waitFor(&mu, &got, func(rs []string) bool {
		for _, r := range rs {
			if r == goodMsg {
				return true
			}
		}
		return false
	}, 2*time.Second) {
		t.Fatalf("a properly signed frame should be delivered; got %v", snapshot(&mu, &got))
	}

	// Error path: the unsigned, forged frame must NEVER reach the handler.
	for _, r := range snapshot(&mu, &got) {
		if r == spoofMsg {
			t.Fatalf("SIG-NIL SPOOF: receiver accepted an unsigned frame with a forged Sender in a SignMsgs room")
		}
	}

	// Edge: a room WITHOUT SignMsgs still delivers an unsigned raw frame, proving
	// the drop is scoped to signed rooms and did not break the plain-NATS path.
	const subjectOpen = "room.open.nosig"
	openRoom, err := alice.CreateRoom(subjectOpen, room.ModeNATS)
	if err != nil {
		t.Fatalf("alice create open room: %v", err)
	}
	openCol := subscribeCollect(t, alice, openRoom)
	defer openCol.sub.Unsubscribe()
	time.Sleep(150 * time.Millisecond)

	const openMsg = "unsigned but allowed here"
	openFrame := frame.Frame{
		Type:    frame.PUB,
		Subject: subjectOpen,
		Sender:  "anyone",
		MsgID:   "open-1",
		Payload: []byte(openMsg),
		// no Sig — fine in a non-signed room
	}
	ob, _ := openFrame.Marshal()
	if err := rawAtk.Publish(subjectOpen, ob); err != nil {
		t.Fatalf("publish open frame: %v", err)
	}
	_ = rawAtk.Flush()
	if !waitFor(&openCol.mu, &openCol.msgs, func(rs []string) bool {
		for _, r := range rs {
			if r == openMsg {
				return true
			}
		}
		return false
	}, 2*time.Second) {
		t.Fatalf("an unsigned frame in a non-signed room should be delivered; got %v", snapshot(&openCol.mu, &openCol.msgs))
	}
}
