package core

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gorilla/websocket"
)

// helpers ------------------------------------------------------------------

func startTestBridge(t *testing.T, token string) (*BridgeServer, string) {
	t.Helper()
	var bs *BridgeServer
	if token == "" {
		// Use insecure mode for tests without token
		bs = NewBridgeServerInsecure(0, token, "/bridge/ws", nil)
	} else {
		bs = NewBridgeServer(0, token, "/bridge/ws", nil)
	}
	if bs == nil {
		t.Fatalf("failed to create bridge server")
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/bridge/ws", bs.handleWS)
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)

	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/bridge/ws"
	return bs, wsURL
}

func dialWS(t *testing.T, url string, headers http.Header) *websocket.Conn {
	t.Helper()
	conn, _, err := websocket.DefaultDialer.Dial(url, headers)
	if err != nil {
		t.Fatalf("dial websocket: %v", err)
	}
	t.Cleanup(func() { conn.Close() })
	return conn
}

func register(t *testing.T, conn *websocket.Conn, platform string, caps []string) {
	t.Helper()
	msg := map[string]any{
		"type":         "register",
		"platform":     platform,
		"capabilities": caps,
	}
	mustWriteJSON(t, conn, msg)
	var ack map[string]any
	mustReadJSON(t, conn, &ack)
	if ack["ok"] != true {
		t.Fatalf("register failed: %v", ack["error"])
	}
}

func registerWithMetadata(t *testing.T, conn *websocket.Conn, platform string, caps []string, metadata map[string]any) {
	t.Helper()
	msg := map[string]any{
		"type":         "register",
		"platform":     platform,
		"capabilities": caps,
		"metadata":     metadata,
	}
	mustWriteJSON(t, conn, msg)
	var ack map[string]any
	mustReadJSON(t, conn, &ack)
	if ack["ok"] != true {
		t.Fatalf("register failed: %v", ack["error"])
	}
}

func readMsg(t *testing.T, conn *websocket.Conn) map[string]any {
	t.Helper()
	if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
		t.Fatalf("set read deadline: %v", err)
	}
	var m map[string]any
	if err := conn.ReadJSON(&m); err != nil {
		t.Fatalf("read message: %v", err)
	}
	return m
}

func mustWriteJSON(t *testing.T, conn *websocket.Conn, v any) {
	t.Helper()
	if err := conn.WriteJSON(v); err != nil {
		t.Fatalf("write JSON: %v", err)
	}
}

func mustReadJSON(t *testing.T, conn *websocket.Conn, v any) {
	t.Helper()
	if err := conn.ReadJSON(v); err != nil {
		t.Fatalf("read JSON: %v", err)
	}
}

func mustDecodeJSON(t *testing.T, r io.Reader, v any) {
	t.Helper()
	if err := json.NewDecoder(r).Decode(v); err != nil {
		t.Fatalf("decode JSON: %v", err)
	}
}

func mustEncodeJSON(t *testing.T, w io.Writer, v any) {
	t.Helper()
	if err := json.NewEncoder(w).Encode(v); err != nil {
		t.Fatalf("encode JSON: %v", err)
	}
}

func mustUnmarshalJSON(t *testing.T, data []byte, v any) {
	t.Helper()
	if err := json.Unmarshal(data, v); err != nil {
		t.Fatalf("unmarshal JSON: %v", err)
	}
}

// tests --------------------------------------------------------------------

func TestBridge_RegisterAndConnect(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	conn := dialWS(t, wsURL, nil)
	register(t, conn, "test-chat", []string{"text", "buttons"})

	adapters := bs.ConnectedAdapters()
	if len(adapters) != 1 || adapters[0] != "test-chat" {
		t.Fatalf("expected [test-chat], got %v", adapters)
	}
}

func TestBridge_RegisterSendsCapabilitiesSnapshotWhenAdapterSupportsIt(t *testing.T) {
	prevVersion, prevCommit, prevBuildTime := CurrentVersion, CurrentCommit, CurrentBuildTime
	CurrentVersion = "v2.0.0"
	CurrentCommit = "deadbeef"
	CurrentBuildTime = "2026-04-11T00:00:00Z"
	defer func() {
		CurrentVersion = prevVersion
		CurrentCommit = prevCommit
		CurrentBuildTime = prevBuildTime
	}()

	bs, wsURL := startTestBridge(t, "")
	bp := bs.NewPlatform("test-proj")
	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	e.AddCommand("deploy", "Deploy app", "ship it", "", "", "config")
	bs.RegisterEngine("test-proj", e, bp)

	conn := dialWS(t, wsURL, nil)
	registerWithMetadata(t, conn, "bridge", []string{"text"}, map[string]any{
		"control_plane": []string{bridgeCapabilitiesSnapshotProto},
	})

	msg := readMsg(t, conn)
	if msg["type"] != bridgeCapabilitiesSnapshotType {
		t.Fatalf("type = %v, want %q", msg["type"], bridgeCapabilitiesSnapshotType)
	}
	if got := int(msg["v"].(float64)); got != 1 {
		t.Fatalf("v = %d, want 1", got)
	}
	host, ok := msg["host"].(map[string]any)
	if !ok {
		t.Fatalf("host = %T, want object", msg["host"])
	}
	if host["cc_connect_version"] != "v2.0.0" {
		t.Fatalf("cc_connect_version = %v, want %q", host["cc_connect_version"], "v2.0.0")
	}
	projects, ok := msg["projects"].([]any)
	if !ok || len(projects) != 1 {
		t.Fatalf("projects = %T/%d, want 1 project", msg["projects"], len(projects))
	}
	project, ok := projects[0].(map[string]any)
	if !ok {
		t.Fatalf("project = %T, want object", projects[0])
	}
	if project["project"] != "test-proj" {
		t.Fatalf("project name = %v, want %q", project["project"], "test-proj")
	}
	commands, ok := project["commands"].([]any)
	if !ok || len(commands) == 0 {
		t.Fatalf("commands = %T/%d, want non-empty list", project["commands"], len(commands))
	}
	foundDeploy := false
	for _, raw := range commands {
		cmd, ok := raw.(map[string]any)
		if !ok {
			t.Fatalf("command = %T, want object", raw)
		}
		if cmd["name"] == "deploy" {
			foundDeploy = true
		}
	}
	if !foundDeploy {
		t.Fatal("expected deploy command in capabilities snapshot")
	}
}

func TestBridge_AuthRequired(t *testing.T) {
	_, wsURL := startTestBridge(t, "secret123")

	// No auth → should fail
	_, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err == nil {
		t.Fatal("expected connection to be rejected")
	}
	if resp != nil && resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", resp.StatusCode)
	}

	// With auth → should succeed
	headers := http.Header{}
	headers.Set("Authorization", "Bearer secret123")
	conn := dialWS(t, wsURL, headers)
	register(t, conn, "authed-chat", []string{"text"})
}

func TestBridge_AuthQueryParam(t *testing.T) {
	_, wsURL := startTestBridge(t, "qtoken")

	conn := dialWS(t, wsURL+"?token=qtoken", nil)
	register(t, conn, "qp-chat", []string{"text"})
}

func TestBridge_RegisterMissingPlatform(t *testing.T) {
	_, wsURL := startTestBridge(t, "")
	conn := dialWS(t, wsURL, nil)

	mustWriteJSON(t, conn, map[string]any{
		"type":         "register",
		"platform":     "",
		"capabilities": []string{"text"},
	})

	var ack map[string]any
	mustReadJSON(t, conn, &ack)
	if ack["ok"] == true {
		t.Fatal("expected registration to fail for empty platform")
	}
}

func TestBridge_MessageRouting(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	var received *Message
	var receivedMu sync.Mutex

	bp := bs.NewPlatform("test-proj")

	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	bs.RegisterEngine("test-proj", e, bp)
	bp.handler = func(p Platform, msg *Message) {
		receivedMu.Lock()
		received = msg
		receivedMu.Unlock()
	}

	conn := dialWS(t, wsURL, nil)
	register(t, conn, "mychat", []string{"text"})

	imgData := base64.StdEncoding.EncodeToString([]byte("fakepng"))
	mustWriteJSON(t, conn, map[string]any{
		"type":        "message",
		"msg_id":      "m1",
		"session_key": "mychat:user1:user1",
		"user_id":     "user1",
		"user_name":   "Alice",
		"content":     "hello bridge",
		"reply_ctx":   "conv-1",
		"images":      []map[string]any{{"mime_type": "image/png", "data": imgData, "file_name": "test.png"}},
	})

	time.Sleep(100 * time.Millisecond)

	receivedMu.Lock()
	defer receivedMu.Unlock()
	if received == nil {
		t.Fatal("expected message to be received")
	}
	if received.Content != "hello bridge" {
		t.Fatalf("content = %q, want %q", received.Content, "hello bridge")
	}
	if received.Platform != "mychat" {
		t.Fatalf("platform = %q, want %q", received.Platform, "mychat")
	}
	if received.UserName != "Alice" {
		t.Fatalf("user_name = %q, want %q", received.UserName, "Alice")
	}
	if len(received.Images) != 1 {
		t.Fatalf("images count = %d, want 1", len(received.Images))
	}
	if received.Images[0].FileName != "test.png" {
		t.Fatalf("image filename = %q, want %q", received.Images[0].FileName, "test.png")
	}
}

func TestBridge_MessageReplyCtxCarriesProgressHints(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	gotCh := make(chan *bridgeReplyCtx, 1)

	bp := bs.NewPlatform("test-proj")
	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	bs.RegisterEngine("test-proj", e, bp)
	bp.handler = func(p Platform, msg *Message) {
		rc, ok := msg.ReplyCtx.(*bridgeReplyCtx)
		if !ok {
			t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", msg.ReplyCtx)
		}
		gotCh <- rc
	}

	conn := dialWS(t, wsURL, nil)
	registerWithMetadata(t, conn, "bridge", []string{"text", "card", "preview", "update_message"}, map[string]any{
		"adapter": "bot-gateway",
	})

	mustWriteJSON(t, conn, map[string]any{
		"type":        "message",
		"msg_id":      "m1",
		"session_key": "bridge:room-1:user-1",
		"user_id":     "user-1",
		"content":     "hello",
		"reply_ctx":   "ctx-1",
	})

	var got *bridgeReplyCtx
	select {
	case got = <-gotCh:
	case <-time.After(5 * time.Second):
		t.Fatal("expected reply ctx to be captured")
	}
	if got.progressStyleHint() != progressStyleCard {
		t.Fatalf("progressStyleHint() = %q, want %q", got.progressStyleHint(), progressStyleCard)
	}
	if !got.supportsProgressCardPayloadHint() {
		t.Fatal("supportsProgressCardPayloadHint() = false, want true")
	}
}

func TestBridge_ReplyRouting(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	bp := bs.NewPlatform("test-proj")

	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	bs.RegisterEngine("test-proj", e, bp)
	bp.handler = func(p Platform, msg *Message) {
		if err := p.Reply(context.TODO(), msg.ReplyCtx, "pong"); err != nil {
			t.Fatalf("Reply: %v", err)
		}
	}

	conn := dialWS(t, wsURL, nil)
	register(t, conn, "rc", []string{"text"})

	mustWriteJSON(t, conn, map[string]any{
		"type":        "message",
		"msg_id":      "m1",
		"session_key": "rc:u1:u1",
		"user_id":     "u1",
		"content":     "ping",
		"reply_ctx":   "ctx-1",
	})

	reply := readMsg(t, conn)
	if reply["type"] != "reply" {
		t.Fatalf("type = %q, want reply", reply["type"])
	}
	if reply["content"] != "pong" {
		t.Fatalf("content = %q, want pong", reply["content"])
	}
	if reply["reply_ctx"] != "ctx-1" {
		t.Fatalf("reply_ctx = %q, want ctx-1", reply["reply_ctx"])
	}
}

func TestBridge_ReconstructReplyCtx_RequiresCapability(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")
	bp := bs.NewPlatform("advisor-gemini")

	conn := dialWS(t, wsURL, nil)
	register(t, conn, "bridge", []string{"text"})

	_, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay")
	if err == nil || !strings.Contains(err.Error(), "does not support reconstruct_reply") {
		t.Fatalf("ReconstructReplyCtx() error = %v, want reconstruct_reply capability error", err)
	}
}

func TestBridge_ReconstructReplyCtx_UsesStructuredPayload(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")
	bp := bs.NewPlatform("advisor-gemini")

	conn := dialWS(t, wsURL, nil)
	register(t, conn, "bridge", []string{"text", "reconstruct_reply"})

	replyCtx, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay")
	if err != nil {
		t.Fatalf("ReconstructReplyCtx() error = %v", err)
	}

	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", replyCtx)
	}
	if rc.Platform != "bridge" {
		t.Fatalf("Platform = %q, want bridge", rc.Platform)
	}
	if rc.SessionKey != "bridge:1491487450722341088:relay" {
		t.Fatalf("SessionKey = %q, want relay session key", rc.SessionKey)
	}

	var payload bridgeReconstructReplyCtxPayload
	if err := json.Unmarshal([]byte(rc.ReplyCtx), &payload); err != nil {
		t.Fatalf("unmarshal reply_ctx: %v", err)
	}
	if payload.Kind != bridgeReconstructReplyCtxKind {
		t.Fatalf("kind = %q, want %q", payload.Kind, bridgeReconstructReplyCtxKind)
	}
	if payload.Version != 1 {
		t.Fatalf("version = %d, want 1", payload.Version)
	}
	if payload.SenderProject != "advisor-gemini" {
		t.Fatalf("sender_project = %q, want advisor-gemini", payload.SenderProject)
	}
	if payload.TransportChatID != "1491487450722341088" {
		t.Fatalf("transport_chat_id = %q, want 1491487450722341088", payload.TransportChatID)
	}
	if payload.TransportSessionKey != "bridge:1491487450722341088:relay" {
		t.Fatalf("transport_session_key = %q, want relay session key", payload.TransportSessionKey)
	}
}

func TestBridge_ReconstructReplyCtx_UsesAdapterProgressHints(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")
	bp := bs.NewPlatform("test-proj")

	conn := dialWS(t, wsURL, nil)
	registerWithMetadata(t, conn, "bridge", []string{"text", "card", "preview", "update_message", "reconstruct_reply"}, map[string]any{
		"adapter": "bot-gateway",
	})

	replyCtx, err := bp.ReconstructReplyCtx("bridge:room-1:user-1")
	if err != nil {
		t.Fatalf("ReconstructReplyCtx() error = %v", err)
	}

	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", replyCtx)
	}
	if rc.progressStyleHint() != progressStyleCard {
		t.Fatalf("progressStyleHint() = %q, want %q", rc.progressStyleHint(), progressStyleCard)
	}
	if !rc.supportsProgressCardPayloadHint() {
		t.Fatal("supportsProgressCardPayloadHint() = false, want true")
	}
}

func TestBridge_CardFallback(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	bp := bs.NewPlatform("test-proj")

	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	bs.RegisterEngine("test-proj", e, bp)
	bp.handler = func(p Platform, msg *Message) {
		cs, ok := p.(CardSender)
		if !ok {
			t.Fatal("BridgePlatform should implement CardSender")
		}
		card := NewCard().Title("Test", "blue").Markdown("hello").Build()
		if err := cs.SendCard(context.TODO(), msg.ReplyCtx, card); err != nil {
			t.Fatalf("SendCard: %v", err)
		}
	}

	// Adapter declares NO card capability → should get text fallback
	conn := dialWS(t, wsURL, nil)
	register(t, conn, "nocards", []string{"text"})

	mustWriteJSON(t, conn, map[string]any{
		"type":        "message",
		"msg_id":      "m1",
		"session_key": "nocards:u1:u1",
		"user_id":     "u1",
		"content":     "hi",
		"reply_ctx":   "c1",
	})

	reply := readMsg(t, conn)
	if reply["type"] != "reply" {
		t.Fatalf("expected text fallback, got type=%q", reply["type"])
	}
	content, _ := reply["content"].(string)
	if !strings.Contains(content, "hello") {
		t.Fatalf("fallback should contain 'hello', got %q", content)
	}
}

func TestBridge_CardNative(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	bp := bs.NewPlatform("test-proj")

	e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish)
	bs.RegisterEngine("test-proj", e, bp)
	bp.handler = func(p Platform, msg *Message) {
		cs := p.(CardSender)
		card := NewCard().Title("Test", "blue").Markdown("hello").Build()
		if err := cs.SendCard(context.TODO(), msg.ReplyCtx, card); err != nil {
			t.Fatalf("SendCard: %v", err)
		}
	}

	// Adapter declares card capability → should get card
	conn := dialWS(t, wsURL, nil)
	register(t, conn, "withcards", []string{"text", "card"})

	mustWriteJSON(t, conn, map[string]any{
		"type":        "message",
		"msg_id":      "m1",
		"session_key": "withcards:u1:u1",
		"user_id":     "u1",
		"content":     "hi",
		"reply_ctx":   "c1",
	})

	reply := readMsg(t, conn)
	if reply["type"] != "card" {
		t.Fatalf("expected card, got type=%q", reply["type"])
	}
	cardData, ok := reply["card"].(map[string]any)
	if !ok {
		t.Fatal("card field should be a map")
	}
	header, _ := cardData["header"].(map[string]any)
	if header["title"] != "Test" {
		t.Fatalf("card title = %q, want Test", header["title"])
	}
}

func TestBridge_Ping(t *testing.T) {
	_, wsURL := startTestBridge(t, "")
	conn := dialWS(t, wsURL, nil)
	register(t, conn, "pingtest", []string{"text"})

	mustWriteJSON(t, conn, map[string]any{"type": "ping", "ts": time.Now().UnixMilli()})
	pong := readMsg(t, conn)
	if pong["type"] != "pong" {
		t.Fatalf("expected pong, got %q", pong["type"])
	}
}

func TestBridge_AdapterReplace(t *testing.T) {
	bs, wsURL := startTestBridge(t, "")

	conn1 := dialWS(t, wsURL, nil)
	register(t, conn1, "replaceme", []string{"text"})

	if len(bs.ConnectedAdapters()) != 1 {
		t.Fatal("expected 1 adapter")
	}

	conn2 := dialWS(t, wsURL, nil)
	register(t, conn2, "replaceme", []string{"text", "card"})

	if len(bs.ConnectedAdapters()) != 1 {
		t.Fatal("expected still 1 adapter after replace")
	}

	a := bs.getAdapter("replaceme")
	if !a.capabilities["card"] {
		t.Fatal("replaced adapter should have card capability")
	}
}

func TestSerializeCard(t *testing.T) {
	card := NewCard().
		Title("Model", "blue").
		Markdown("Choose:").
		Buttons(PrimaryBtn("GPT-4", "cmd:/model switch gpt-4"), DefaultBtn("Claude", "cmd:/model switch claude")).
		Divider().
		Note("tip").
		Build()

	result := serializeCard(card)

	header, _ := result["header"].(map[string]string)
	if header["title"] != "Model" || header["color"] != "blue" {
		t.Fatalf("header = %v", header)
	}

	elements, _ := result["elements"].([]map[string]any)
	if len(elements) != 4 {
		t.Fatalf("elements count = %d, want 4", len(elements))
	}
	if elements[0]["type"] != "markdown" {
		t.Fatalf("first element type = %q", elements[0]["type"])
	}
	if elements[1]["type"] != "actions" {
		t.Fatalf("second element type = %q", elements[1]["type"])
	}
	if elements[2]["type"] != "divider" {
		t.Fatalf("third element type = %q", elements[2]["type"])
	}
	if elements[3]["type"] != "note" {
		t.Fatalf("fourth element type = %q", elements[3]["type"])
	}

	btns, _ := elements[1]["buttons"].([]map[string]any)
	if len(btns) != 2 {
		t.Fatalf("buttons count = %d", len(btns))
	}
	if btns[0]["text"] != "GPT-4" || btns[0]["value"] != "cmd:/model switch gpt-4" {
		t.Fatalf("button[0] = %v", btns[0])
	}

	data, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("json marshal: %v", err)
	}
	if len(data) == 0 {
		t.Fatal("serialized card is empty")
	}
}

// ---------------------------------------------------------------------------
// Session Management REST API tests
// ---------------------------------------------------------------------------

// startTestBridgeWithREST creates a bridge server with both WS and REST endpoints.
func startTestBridgeWithREST(t *testing.T, token string) (*BridgeServer, string) {
	t.Helper()
	var bs *BridgeServer
	if token == "" {
		bs = NewBridgeServerInsecure(0, token, "/bridge/ws", nil)
	} else {
		bs = NewBridgeServer(0, token, "/bridge/ws", nil)
	}
	if bs == nil {
		t.Fatalf("failed to create bridge server")
	}

	agent := &stubAgent{}
	sm := NewSessionManager("")
	engine := NewEngine("test-proj", agent, nil, "", LangEnglish)
	engine.sessions = sm

	bp := bs.NewPlatform("test-proj")
	bs.RegisterEngine("test-proj", engine, bp)

	mux := http.NewServeMux()
	mux.HandleFunc("/bridge/ws", bs.handleWS)
	mux.HandleFunc("/bridge/sessions", bs.authHTTP(bs.handleSessions))
	mux.HandleFunc("/bridge/sessions/", bs.authHTTP(bs.handleSessionRoutes))
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)

	return bs, srv.URL
}

type bridgeAPIResponse struct {
	OK    bool            `json:"ok"`
	Data  json.RawMessage `json:"data,omitempty"`
	Error string          `json:"error,omitempty"`
}

func bridgeGet(t *testing.T, url, token string) bridgeAPIResponse {
	t.Helper()
	req, _ := http.NewRequest("GET", url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("GET %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r bridgeAPIResponse
	mustDecodeJSON(t, resp.Body, &r)
	return r
}

func bridgePost(t *testing.T, url, token string, body any) bridgeAPIResponse {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		mustEncodeJSON(t, &buf, body)
	}
	req, _ := http.NewRequest("POST", url, &buf)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("POST %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r bridgeAPIResponse
	mustDecodeJSON(t, resp.Body, &r)
	return r
}

func bridgeDel(t *testing.T, url, token string) bridgeAPIResponse {
	t.Helper()
	req, _ := http.NewRequest("DELETE", url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("DELETE %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r bridgeAPIResponse
	mustDecodeJSON(t, resp.Body, &r)
	return r
}

func TestBridge_SessionList(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "tok")

	// List sessions for a new key — should create a default session
	r := bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1&token=tok", "")
	if !r.OK {
		t.Logf("no sessions yet: %s", r.Error)
	}

	// Create a session first
	r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"name":        "work",
	})
	if !r.OK {
		t.Fatalf("create session failed: %s", r.Error)
	}
	var created struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}
	mustUnmarshalJSON(t, r.Data, &created)
	if created.ID == "" {
		t.Fatal("expected session ID")
	}
	if created.Name != "work" {
		t.Fatalf("expected name 'work', got %q", created.Name)
	}

	// Now list — should have 1 session
	r = bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "tok")
	if !r.OK {
		t.Fatalf("list sessions failed: %s", r.Error)
	}
	var listData struct {
		Sessions []map[string]any `json:"sessions"`
	}
	mustUnmarshalJSON(t, r.Data, &listData)
	if len(listData.Sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(listData.Sessions))
	}
}

func TestBridge_SessionCreateAndDetail(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "tok")

	// Create
	r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"name":        "dev",
	})
	if !r.OK {
		t.Fatalf("create failed: %s", r.Error)
	}
	var created struct {
		ID string `json:"id"`
	}
	mustUnmarshalJSON(t, r.Data, &created)

	// Get detail
	r = bridgeGet(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok")
	if !r.OK {
		t.Fatalf("get detail failed: %s", r.Error)
	}
	var detail struct {
		ID      string           `json:"id"`
		Name    string           `json:"name"`
		History []map[string]any `json:"history"`
	}
	mustUnmarshalJSON(t, r.Data, &detail)
	if detail.ID != created.ID {
		t.Fatalf("expected id %q, got %q", created.ID, detail.ID)
	}
	if detail.Name != "dev" {
		t.Fatalf("expected name 'dev', got %q", detail.Name)
	}
}

func TestBridge_SessionDelete(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "tok")

	r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"name":        "temp",
	})
	if !r.OK {
		t.Fatalf("create failed: %s", r.Error)
	}
	var created struct {
		ID string `json:"id"`
	}
	mustUnmarshalJSON(t, r.Data, &created)

	// Delete
	r = bridgeDel(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok")
	if !r.OK {
		t.Fatalf("delete failed: %s", r.Error)
	}

	// Verify deleted
	r = bridgeGet(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok")
	if r.OK {
		t.Fatal("expected 404 after deletion")
	}
}

func TestBridge_SessionSwitch(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "tok")

	// Create two sessions
	r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"name":        "first",
	})
	if !r.OK {
		t.Fatalf("create first failed: %s", r.Error)
	}

	r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"name":        "second",
	})
	if !r.OK {
		t.Fatalf("create second failed: %s", r.Error)
	}
	var second struct {
		ID string `json:"id"`
	}
	mustUnmarshalJSON(t, r.Data, &second)

	// Switch to second
	r = bridgePost(t, baseURL+"/bridge/sessions/switch", "tok", map[string]string{
		"session_key": "test:u1:u1",
		"target":      second.ID,
	})
	if !r.OK {
		t.Fatalf("switch failed: %s", r.Error)
	}
	var switched struct {
		ActiveSessionID string `json:"active_session_id"`
	}
	mustUnmarshalJSON(t, r.Data, &switched)
	if switched.ActiveSessionID != second.ID {
		t.Fatalf("expected active=%s, got %s", second.ID, switched.ActiveSessionID)
	}
}

func TestBridge_SessionAuthRequired(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "secret")

	r := bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "")
	if r.OK {
		t.Fatal("expected auth failure without token")
	}

	r = bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "secret")
	if !r.OK {
		t.Fatalf("expected success with token, got: %s", r.Error)
	}
}

func TestBridge_SessionMissingParams(t *testing.T) {
	_, baseURL := startTestBridgeWithREST(t, "tok")

	// Missing session_key
	r := bridgeGet(t, baseURL+"/bridge/sessions", "tok")
	if r.OK {
		t.Fatal("expected error without session_key")
	}

	// Missing session_key in POST
	r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{
		"name": "test",
	})
	if r.OK {
		t.Fatal("expected error without session_key in POST")
	}

	// Missing params in switch
	r = bridgePost(t, baseURL+"/bridge/sessions/switch", "tok", map[string]string{
		"session_key": "test:u1:u1",
	})
	if r.OK {
		t.Fatal("expected error without target in switch")
	}
}
