package transportunibus_test

import (
	"context"
	"net"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/transport"
	"github.com/enmanuel/agents/shell/transportunibus"

	cs "fn-registry/functions/cybersecurity"

	"github.com/enmanuel/unibus/pkg/blobstore"
	"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/enmanuel/unibus/pkg/room"
	server "github.com/nats-io/nats-server/v2/server"
)

// harness boots an embedded NATS + an in-process membershipd, mirroring the
// unibus test harness so this adapter can be exercised without any external
// service.
type harness struct {
	natsURL string
	ctrlURL string
	ns      *server.Server
	httpts  *httptest.Server
}

func freePort(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 newHarness(t *testing.T) *harness {
	t.Helper()
	dir := t.TempDir()
	ns, err := embeddednats.StartHost(filepath.Join(dir, "js"), "127.0.0.1", freePort(t))
	if err != nil {
		t.Fatalf("embedded nats: %v", err)
	}
	store, err := membership.Open(filepath.Join(dir, "unibus.db"))
	if err != nil {
		ns.Shutdown()
		t.Fatalf("membership store: %v", err)
	}
	blobs, err := blobstore.New(filepath.Join(dir, "blobs"))
	if err != nil {
		ns.Shutdown()
		t.Fatalf("blob store: %v", err)
	}
	httpts := httptest.NewServer(membership.NewServer(store, blobs))
	h := &harness{natsURL: embeddednats.ClientURL(ns), ctrlURL: httpts.URL, ns: ns, httpts: httpts}
	t.Cleanup(func() {
		httpts.Close()
		store.Close()
		ns.Shutdown()
		ns.WaitForShutdown()
	})
	return h
}

func waitHealth(t *testing.T, ctrlURL string) {
	t.Helper()
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		resp, err := http.Get(ctrlURL + "/healthz")
		if err == nil && resp.StatusCode == 200 {
			resp.Body.Close()
			return
		}
		if resp != nil {
			resp.Body.Close()
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Fatalf("membershipd never became healthy")
}

// botCfg builds a BusCfg pointing the bot at the harness, with a fresh identity
// file under the test's temp dir.
func botCfg(t *testing.T, h *harness, handle string) config.BusCfg {
	t.Helper()
	return config.BusCfg{
		NatsURL:      h.natsURL,
		CtrlURL:      h.ctrlURL,
		IdentityPath: filepath.Join(t.TempDir(), handle+".id"),
		Handle:       handle,
	}
}

// TestBotEchoesInEncryptedRoom is the headline room-based test: a human peer
// creates an encrypted (room.ModeMatrix) room, invites the bot by its endpoint,
// and publishes a mention. The bot — driven by Transport.Run + a tiny echo
// handler that replies via Reply — answers IN THE SAME room, and the human
// receives the reply decrypted. No Matrix is involved end to end.
func TestBotEchoesInEncryptedRoom(t *testing.T) {
	h := newHarness(t)
	waitHealth(t, h.ctrlURL)

	bot, err := transportunibus.New(botCfg(t, h, "demo"), nil)
	if err != nil {
		t.Fatalf("bot transport: %v", err)
	}
	defer bot.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() { _ = bot.Run(ctx, transportunibus.DemoEchoHandler(bot, nil)) }()

	// Human peer.
	userID, err := cs.GenerateIdentity()
	if err != nil {
		t.Fatalf("user identity: %v", err)
	}
	user, err := client.New(h.natsURL, h.ctrlURL, userID)
	if err != nil {
		t.Fatalf("user client: %v", err)
	}
	defer user.Close()

	// Human creates an encrypted room and invites the bot by its endpoint id.
	roomID, err := user.CreateRoom("conv.demo", room.ModeMatrix)
	if err != nil {
		t.Fatalf("create room: %v", err)
	}
	// Invite the bot by its full endpoint (id + public keys), so the human can
	// seal the encrypted room key for it.
	if err := user.Invite(roomID, bot.BusEndpoint()); err != nil {
		t.Fatalf("invite bot: %v", err)
	}

	// Human subscribes to the same room to receive the bot's reply.
	var mu sync.Mutex
	var bodies []string
	var sawAnchored bool
	if err := user.Join(roomID); err != nil {
		t.Fatalf("user join: %v", err)
	}
	sub, err := user.Subscribe(roomID, func(f frame.Frame, plaintext []byte) {
		mu.Lock()
		bodies = append(bodies, string(plaintext))
		if f.ReplyTo != "" {
			sawAnchored = true
		}
		mu.Unlock()
	})
	if err != nil {
		t.Fatalf("user subscribe: %v", err)
	}
	defer sub.Unsubscribe()

	// Give the bot's discovery ticker time to find, join and subscribe to the room.
	time.Sleep(300 * time.Millisecond)

	// Human posts a message mentioning the bot's handle.
	if err := user.Publish(roomID, []byte("hola demo")); err != nil {
		t.Fatalf("user publish: %v", err)
	}

	if _, ok := waitBody(&mu, &bodies, "echo: hola demo", 5*time.Second); !ok {
		t.Fatalf("never received echo reply; got %v", snapshot(&mu, &bodies))
	}
	mu.Lock()
	anchored := sawAnchored
	mu.Unlock()
	if !anchored {
		t.Fatalf("reply did not carry a ReplyTo anchor")
	}

	// Command over the bus → pong, in the same room.
	if err := user.Publish(roomID, []byte("!ping")); err != nil {
		t.Fatalf("user publish ping: %v", err)
	}
	if _, ok := waitBody(&mu, &bodies, "pong", 5*time.Second); !ok {
		t.Fatalf("never received pong; got %v", snapshot(&mu, &bodies))
	}
}

// TestRunStopsOnContextCancel is an error/lifecycle path: Run must return when
// its context is cancelled rather than blocking forever.
func TestRunStopsOnContextCancel(t *testing.T) {
	h := newHarness(t)
	waitHealth(t, h.ctrlURL)

	bot, err := transportunibus.New(botCfg(t, h, "lifecycle"), nil)
	if err != nil {
		t.Fatalf("bot transport: %v", err)
	}
	defer bot.Close()

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error, 1)
	go func() { done <- bot.Run(ctx, func(context.Context, transport.InboundMessage) {}) }()

	time.Sleep(100 * time.Millisecond)
	cancel()
	select {
	case err := <-done:
		if err != context.Canceled {
			t.Fatalf("Run returned %v, want context.Canceled", err)
		}
	case <-time.After(3 * time.Second):
		t.Fatalf("Run did not return after context cancel")
	}
}

// ---- helpers ----

func waitBody(mu *sync.Mutex, slice *[]string, want string, timeout time.Duration) (string, bool) {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		mu.Lock()
		for _, s := range *slice {
			if s == want {
				mu.Unlock()
				return s, true
			}
		}
		mu.Unlock()
		time.Sleep(25 * time.Millisecond)
	}
	return "", false
}

func snapshot(mu *sync.Mutex, slice *[]string) []string {
	mu.Lock()
	defer mu.Unlock()
	return append([]string(nil), (*slice)...)
}
