package client_test

import (
	"bytes"
	"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"
)

// TestAudit_NoSubjectACL ports the auditor's H4 (Alto) finding under the minimum
// defense chosen for this issue (forbid cleartext rooms in public; see
// dev/0004d-dataplane-acl.md). The NATS data plane still has no per-subject ACL,
// so the guarantee we make is CONTENT confidentiality, proven three ways:
//
//	error : a cleartext (ModeNATS) room cannot be created under the public posture;
//	golden: a legitimate member (bob) decrypts the secret;
//	edge  : eve, sniffing the raw subject off the data plane, receives only
//	        ciphertext — she never recovers the plaintext the auditor's eve did.
func TestAudit_NoSubjectACL(t *testing.T) {
	h := newHarness(t)
	h.srv.RequireEncryptedRooms = true // the public posture
	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()

	// Error path: a cleartext room is refused, so no payload ever rides a subject
	// in the clear for a sniffer to read (the exact vector the auditor exploited).
	if _, err := alice.CreateRoom("secret.subject.payroll", room.ModeNATS); err == nil {
		t.Fatalf("cleartext room must be refused on a public deployment")
	}

	// alice creates an encrypted room and invites bob (the legitimate reader).
	const subject = "secret.subject.payroll.e2e"
	const secret = "internal: salary numbers"
	roomID, err := alice.CreateRoom(subject, room.ModeMatrix)
	if err != nil {
		t.Fatalf("alice create encrypted room: %v", err)
	}
	bob, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
	if err != nil {
		t.Fatalf("connect bob: %v", err)
	}
	defer bob.Close()
	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)
	}

	// Golden: bob (a member) subscribes and decrypts the secret.
	var bmu sync.Mutex
	var bobGot []string
	bobSub, err := bob.Subscribe(roomID, func(_ frame.Frame, plaintext []byte) {
		bmu.Lock()
		bobGot = append(bobGot, string(plaintext))
		bmu.Unlock()
	})
	if err != nil {
		t.Fatalf("bob subscribe: %v", err)
	}
	defer bobSub.Unsubscribe()

	// Edge: eve sniffs the raw subject directly off NATS (no membership, no key).
	rawEve, err := nats.Connect(h.natsURL)
	if err != nil {
		t.Fatalf("eve raw connect: %v", err)
	}
	defer rawEve.Close()
	eveGot := make(chan []byte, 8)
	if _, err := rawEve.Subscribe(subject, func(m *nats.Msg) { eveGot <- m.Data }); err != nil {
		t.Fatalf("eve raw subscribe: %v", err)
	}
	if err := rawEve.Flush(); err != nil {
		t.Fatalf("eve flush: %v", err)
	}
	time.Sleep(200 * time.Millisecond) // let both subscriptions settle

	if err := alice.Publish(roomID, []byte(secret)); err != nil {
		t.Fatalf("alice publish: %v", err)
	}

	// bob must decrypt the secret.
	if !waitFor(&bmu, &bobGot, func(rs []string) bool {
		for _, r := range rs {
			if r == secret {
				return true
			}
		}
		return false
	}, 2*time.Second) {
		t.Fatalf("bob (member) should decrypt the secret; got %v", snapshot(&bmu, &bobGot))
	}

	// eve must receive only ciphertext — never the plaintext.
	select {
	case data := <-eveGot:
		if bytes.Contains(data, []byte(secret)) {
			t.Fatalf("eve sniffed the plaintext off the data plane: %q", data)
		}
		f, err := frame.Unmarshal(data)
		if err != nil {
			t.Fatalf("eve received an undecodable frame: %v", err)
		}
		if string(f.Payload) == secret {
			t.Fatalf("eve read the secret from the frame payload")
		}
		if len(f.Nonce) == 0 {
			t.Fatalf("expected an AEAD-encrypted payload (non-empty nonce), got cleartext frame")
		}
	case <-time.After(2 * time.Second):
		// eve receiving nothing is also a safe outcome; the assertion is only that
		// she never gets the plaintext, which holds vacuously here.
	}
}
