package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"strings"
	"testing"
	"time"

	"reasonix/internal/bot"
	"reasonix/internal/event"
)

type bridgeNotifyCall struct {
	connectionID string
	domain       string
	msg          bot.OutboundMessage
}

type bridgeTestEnv struct {
	hub        *botBridgeHub
	notified   chan bridgeNotifyCall
	approves   chan [2]string // [tabID, id+":"+allow]
	answers    chan [2]string // [tabID, id]
	driven     chan [2]string // [tabID, text]
	announced  chan [2]string // [tabID, text]
	persisted  chan []bot.DesktopWatchRoute
	driveErr   error
	persistErr error
}

func tabsToSessions(tabs []TabMeta) []bot.DesktopSessionInfo {
	out := make([]bot.DesktopSessionInfo, 0, len(tabs))
	for _, t := range tabs {
		out = append(out, bot.DesktopSessionInfo{
			TabID:         t.ID,
			Label:         t.Label,
			Workspace:     t.WorkspaceName,
			Topic:         t.TopicTitle,
			Ready:         t.Ready,
			Running:       t.Running,
			PendingPrompt: t.PendingPrompt,
		})
	}
	return out
}

func newBridgeTestEnv(tabs []TabMeta) *bridgeTestEnv {
	return newBridgeTestEnvSessions(tabsToSessions(tabs))
}

func newBridgeTestEnvSessions(sessions []bot.DesktopSessionInfo) *bridgeTestEnv {
	env := &bridgeTestEnv{
		notified:  make(chan bridgeNotifyCall, 16),
		approves:  make(chan [2]string, 16),
		answers:   make(chan [2]string, 16),
		driven:    make(chan [2]string, 16),
		announced: make(chan [2]string, 16),
		persisted: make(chan []bot.DesktopWatchRoute, 16),
	}
	env.hub = newBotBridgeHub(botBridgeDeps{
		sessions: func() []bot.DesktopSessionInfo { return sessions },
		approveTab: func(tabID, id string, allow, session, persist bool) {
			env.approves <- [2]string{tabID, fmt.Sprintf("%s:%t", id, allow)}
		},
		answerTab: func(tabID, id string, answers []QuestionAnswer) {
			env.answers <- [2]string{tabID, id}
		},
		notify: func(ctx context.Context, connectionID, domain string, msg bot.OutboundMessage) (bot.SendResult, error) {
			env.notified <- bridgeNotifyCall{connectionID: connectionID, domain: domain, msg: msg}
			return bot.SendResult{MessageID: "sent-1"}, nil
		},
		drive: func(tabID, text string, route bot.DesktopWatchRoute) error {
			if env.driveErr != nil {
				return env.driveErr
			}
			env.driven <- [2]string{tabID, text}
			return nil
		},
		announce: func(tabID, text string) {
			env.announced <- [2]string{tabID, text}
		},
		persistWatchers: func(routes []bot.DesktopWatchRoute) error {
			env.persisted <- routes
			return env.persistErr
		},
		logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
	})
	return env
}

func (env *bridgeTestEnv) waitNotification(t *testing.T) bridgeNotifyCall {
	t.Helper()
	select {
	case call := <-env.notified:
		return call
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for bridge notification")
		return bridgeNotifyCall{}
	}
}

func (env *bridgeTestEnv) expectNoNotification(t *testing.T) {
	t.Helper()
	select {
	case call := <-env.notified:
		t.Fatalf("unexpected notification: %+v", call)
	case <-time.After(100 * time.Millisecond):
	}
}

func testWatchRoute() bot.DesktopWatchRoute {
	return bot.DesktopWatchRoute{
		ConnectionID: "feishu-main",
		Domain:       "feishu",
		Platform:     bot.PlatformFeishu,
		ChatType:     bot.ChatDM,
		ChatID:       "chat-god",
	}
}

func testGroupRoute() bot.DesktopWatchRoute {
	r := testWatchRoute()
	r.ChatType = bot.ChatGroup
	r.ChatID = "group-god"
	return r
}

func TestBridgeTakeoverRejectsGroupChat(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一", Ready: true}})
	if _, err := env.hub.Takeover(testGroupRoute(), "tab-1"); err == nil {
		t.Fatal("takeover from a group chat must be rejected (non-admin members could otherwise drive it)")
	}
	if _, err := env.hub.Takeover(testWatchRoute(), "tab-1"); err != nil {
		t.Fatalf("DM takeover should work: %v", err)
	}
}

func TestBridgeTakeoverSwitchAnnouncesReleaseToOldTab(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{
		{TabID: "tab-a", Label: "A", Ready: true},
		{TabID: "tab-b", Label: "B", Ready: true},
	})
	route := testWatchRoute()
	if _, err := env.hub.Takeover(route, "tab-a"); err != nil {
		t.Fatalf("takeover A: %v", err)
	}
	if got := <-env.announced; got[0] != "tab-a" {
		t.Fatalf("first announce = %v, want tab-a takeover", got)
	}
	if _, err := env.hub.Takeover(route, "tab-b"); err != nil {
		t.Fatalf("switch to B: %v", err)
	}
	seen := map[string]bool{}
	for range 2 {
		select {
		case got := <-env.announced:
			seen[got[0]] = true
		case <-time.After(time.Second):
			t.Fatalf("missing announce after switch; seen=%v", seen)
		}
	}
	if !seen["tab-a"] || !seen["tab-b"] {
		t.Fatalf("switch should announce release to tab-a and takeover to tab-b; seen=%v", seen)
	}
}

func TestBridgeDriveInputBusyReturnsBusyMessage(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一", Ready: true}})
	route := testWatchRoute()
	if _, err := env.hub.Takeover(route, "tab-1"); err != nil {
		t.Fatalf("Takeover: %v", err)
	}
	<-env.announced
	env.driveErr = errDriveBusy
	_, err := env.hub.DriveInput(route, "hi")
	if err == nil || !strings.Contains(err.Error(), "正在执行中") {
		t.Fatalf("busy drive should surface a clean busy message, got %v", err)
	}
}

func TestBridgeApprovalRedactsSubjectInGroup(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一"}})
	env.hub.SetWatch(testGroupRoute(), true)
	<-env.persisted
	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "a1", Tool: "bash", Subject: "rm -rf /secret"}})
	call := env.waitNotification(t)
	if strings.Contains(call.msg.Text, "rm -rf /secret") {
		t.Fatalf("group notification leaked the command line: %q", call.msg.Text)
	}
	if call.msg.Card != nil {
		for _, el := range call.msg.Card.Elements {
			if strings.Contains(el.Content, "rm -rf /secret") {
				t.Fatal("group card leaked the command line")
			}
		}
	}
}

func TestBridgeApprovalShowsSubjectInDM(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一"}})
	env.hub.SetWatch(testWatchRoute(), true)
	<-env.persisted
	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "a1", Tool: "bash", Subject: "rm -rf build"}})
	call := env.waitNotification(t)
	if !strings.Contains(call.msg.Text, "rm -rf build") {
		t.Fatalf("DM notification should show the command line: %q", call.msg.Text)
	}
}

func TestBridgeAskRedactsPromptAndOptionsInGroup(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一"}})
	if err := env.hub.SetWatch(testGroupRoute(), true); err != nil {
		t.Fatalf("SetWatch: %v", err)
	}
	<-env.persisted
	env.hub.observe("tab-1", event.Event{Kind: event.AskRequest, Ask: event.Ask{
		ID: "redacted-ask",
		Questions: []event.AskQuestion{{
			ID: "q1", Prompt: "INTERNAL_ONLY_PROMPT", Options: []event.AskOption{{Label: "CHOICE_INTERNAL"}},
		}},
	}})
	call := env.waitNotification(t)
	if strings.Contains(call.msg.Text, "INTERNAL_ONLY_PROMPT") || strings.Contains(call.msg.Text, "CHOICE_INTERNAL") {
		t.Fatalf("group notification leaked ask details: %q", call.msg.Text)
	}
	if call.msg.Card != nil {
		t.Fatal("group ask notification must not include option buttons")
	}
}

func TestBridgeSessionsIncludePendingIDs(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{{TabID: "tab-1", Label: "会话一"}})
	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "a1", Tool: "bash"}})
	env.hub.observe("tab-1", event.Event{Kind: event.AskRequest, Ask: event.Ask{ID: "q1", Questions: []event.AskQuestion{{ID: "x", Prompt: "?"}}}})
	found := map[string]bool{}
	for _, s := range env.hub.Sessions() {
		if s.TabID != "tab-1" {
			continue
		}
		for _, p := range s.Pending {
			found[p.ID] = true
		}
	}
	if !found["a1"] || !found["q1"] {
		t.Fatalf("Sessions() should surface pending approval and ask ids; got %v", found)
	}
}

func TestBridgePersistDropsStaleSnapshot(t *testing.T) {
	env := newBridgeTestEnvSessions(nil)
	// Two subscribes: the second (newer seq) must be the persisted result even
	// though we invoke the seed-restore afterward.
	env.hub.SetWatch(testWatchRoute(), true)
	<-env.persisted
	env.hub.SetWatch(testGroupRoute(), true)
	routes := <-env.persisted
	if len(routes) != 2 {
		t.Fatalf("persisted routes = %d, want both subscriptions", len(routes))
	}
}

func TestBridgeApprovalNotifiesWatchersAndRoutesApproval(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-1", Label: "修复登录"}})
	env.hub.SetWatch(testWatchRoute(), true)

	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{
		ID: "appr-1", Tool: "bash", Subject: "rm -rf build",
	}})

	call := env.waitNotification(t)
	if call.connectionID != "feishu-main" || call.msg.ChatID != "chat-god" {
		t.Fatalf("notification routed to %s/%s, want feishu-main/chat-god", call.connectionID, call.msg.ChatID)
	}
	for _, want := range []string{"修复登录", "bash", "rm -rf build", "/desktop approve appr-1"} {
		if !strings.Contains(call.msg.Text, want) {
			t.Fatalf("notification text = %q, want it to contain %q", call.msg.Text, want)
		}
	}
	if call.msg.Card == nil {
		t.Fatal("approval notification should carry an interactive card")
	}

	feedback, err := env.hub.Approve("appr-1", true)
	if err != nil {
		t.Fatalf("Approve: %v", err)
	}
	if !strings.Contains(feedback, "先到者为准") {
		t.Fatalf("feedback = %q, want first-wins note", feedback)
	}
	select {
	case got := <-env.approves:
		if got[0] != "tab-1" || got[1] != "appr-1:true" {
			t.Fatalf("approve routed as %v, want tab-1/appr-1:true", got)
		}
	case <-time.After(time.Second):
		t.Fatal("approve was not routed to the tab")
	}

	// 同一 ID 第二次应答:pending 已清,返回未找到。
	if _, err := env.hub.Approve("appr-1", false); err == nil {
		t.Fatal("second Approve on the same id should fail")
	}
}

func TestBridgePendingRecordedWithoutWatchers(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-1", Label: "会话"}})

	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "appr-2", Tool: "bash"}})
	env.expectNoNotification(t)

	if _, err := env.hub.Approve("appr-2", false); err != nil {
		t.Fatalf("Approve without watchers should still work: %v", err)
	}
	select {
	case got := <-env.approves:
		if got[1] != "appr-2:false" {
			t.Fatalf("deny routed as %v", got)
		}
	case <-time.After(time.Second):
		t.Fatal("deny was not routed")
	}
}

func TestBridgeTurnDoneClearsPendingAndNotifies(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-1", Label: "会话一"}})
	env.hub.SetWatch(testWatchRoute(), true)

	env.hub.observe("tab-1", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "appr-3", Tool: "bash"}})
	env.waitNotification(t)

	env.hub.observe("tab-1", event.Event{Kind: event.TurnDone})
	call := env.waitNotification(t)
	if !strings.Contains(call.msg.Text, "✅") || !strings.Contains(call.msg.Text, "会话一") {
		t.Fatalf("turn-done text = %q", call.msg.Text)
	}

	if _, err := env.hub.Approve("appr-3", true); err == nil {
		t.Fatal("pending approval should be cleared by TurnDone")
	}
}

func TestBridgeSuppressesCanceledTurnAndErrorsNotify(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-1", Label: "会话一"}})
	env.hub.SetWatch(testWatchRoute(), true)

	env.hub.observe("tab-1", event.Event{Kind: event.TurnDone, Err: errors.New("context canceled")})
	env.expectNoNotification(t)

	env.hub.observe("tab-1", event.Event{Kind: event.TurnDone, Err: errors.New("boom")})
	call := env.waitNotification(t)
	if !strings.Contains(call.msg.Text, "❌") || !strings.Contains(call.msg.Text, "boom") {
		t.Fatalf("error text = %q", call.msg.Text)
	}
}

func TestBridgeRecoveryPauseNotifiesAsControlledPause(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-1", Label: "会话一"}})
	env.hub.SetWatch(testWatchRoute(), true)

	env.hub.observe("tab-1", event.Event{
		Kind:    event.TurnDone,
		Err:     errors.New("automatic recovery paused"),
		Outcome: event.TurnOutcomeRecoveryPaused,
	})
	call := env.waitNotification(t)
	if strings.Contains(call.msg.Text, "❌") || !strings.Contains(call.msg.Text, "已暂停自动重试") || !strings.Contains(call.msg.Text, "继续") {
		t.Fatalf("recovery pause text = %q, want a neutral actionable pause notice", call.msg.Text)
	}
}

func TestBridgeAskAnswerRoundTrip(t *testing.T) {
	env := newBridgeTestEnv([]TabMeta{{ID: "tab-2", Label: "问答会话"}})
	env.hub.SetWatch(testWatchRoute(), true)

	env.hub.observe("tab-2", event.Event{Kind: event.AskRequest, Ask: event.Ask{
		ID: "ask-1",
		Questions: []event.AskQuestion{{
			ID:      "q1",
			Prompt:  "选一个方案",
			Options: []event.AskOption{{Label: "A"}, {Label: "B"}},
		}},
	}})
	call := env.waitNotification(t)
	if !strings.Contains(call.msg.Text, "/desktop answer ask-1") {
		t.Fatalf("ask notification = %q, want answer hint", call.msg.Text)
	}
	if call.msg.Card == nil {
		t.Fatal("single-choice ask should carry option buttons")
	}

	questions, ok := env.hub.AskQuestions("ask-1")
	if !ok || len(questions) != 1 {
		t.Fatalf("AskQuestions = %v/%v", questions, ok)
	}
	if _, err := env.hub.Answer("ask-1", []event.AskAnswer{{QuestionID: "q1", Selected: []string{"B"}}}); err != nil {
		t.Fatalf("Answer: %v", err)
	}
	select {
	case got := <-env.answers:
		if got[0] != "tab-2" || got[1] != "ask-1" {
			t.Fatalf("answer routed as %v", got)
		}
	case <-time.After(time.Second):
		t.Fatal("answer was not routed")
	}
}

func TestBridgeWatchLifecycleStopsNotifications(t *testing.T) {
	env := newBridgeTestEnv(nil)
	route := testWatchRoute()

	env.hub.SetWatch(route, true)
	if !env.hub.Watching(route) {
		t.Fatal("route should be watching after SetWatch(true)")
	}
	env.hub.SetWatch(route, false)
	if env.hub.Watching(route) {
		t.Fatal("route should not be watching after SetWatch(false)")
	}

	env.hub.observe("tab-x", event.Event{Kind: event.TurnDone})
	env.expectNoNotification(t)
}

func TestBridgeSetWatchPersistsAndSeedRestores(t *testing.T) {
	env := newBridgeTestEnv(nil)
	route := testWatchRoute()

	env.hub.SetWatch(route, true)
	select {
	case routes := <-env.persisted:
		if len(routes) != 1 || routes[0].Key() != route.Key() {
			t.Fatalf("persisted = %+v, want the subscribed route", routes)
		}
	case <-time.After(time.Second):
		t.Fatal("SetWatch did not persist watchers")
	}

	// 模拟重启:全新 hub 从配置种子恢复。
	env2 := newBridgeTestEnv(nil)
	env2.hub.seedWatchers([]bot.DesktopWatchRoute{route}, env2.hub.watcherVersion())
	if !env2.hub.Watching(route) {
		t.Fatal("seeded hub should be watching the persisted route")
	}
	env2.hub.observe("tab-x", event.Event{Kind: event.TurnDone})
	if call := env2.waitNotification(t); !strings.Contains(call.msg.Text, "✅") {
		t.Fatalf("seeded watcher did not receive notifications: %q", call.msg.Text)
	}
}

func TestBridgeSeedDoesNotOverwriteNewerRuntimeWatch(t *testing.T) {
	env := newBridgeTestEnv(nil)
	route := testWatchRoute()
	staleVersion := env.hub.watcherVersion()
	if err := env.hub.SetWatch(route, true); err != nil {
		t.Fatalf("SetWatch: %v", err)
	}
	<-env.persisted

	// Simulate a runtime refresh carrying a config snapshot loaded before the
	// watch command persisted. It must not erase the newer in-process route.
	env.hub.seedWatchers(nil, staleVersion)
	if !env.hub.Watching(route) {
		t.Fatal("stale config seed overwrote the newer runtime subscription")
	}
}

func TestBridgeSeedPreservesWatchAfterPersistFailure(t *testing.T) {
	env := newBridgeTestEnv(nil)
	env.persistErr = errors.New("disk unavailable")
	route := testWatchRoute()
	if err := env.hub.SetWatch(route, true); err == nil {
		t.Fatal("SetWatch should report the persistence failure")
	}
	<-env.persisted

	env.hub.seedWatchers(nil, env.hub.watcherVersion())
	if !env.hub.Watching(route) {
		t.Fatal("disk snapshot erased a runtime watch whose persistence failed")
	}
}

func TestBridgeSeedAppliesFreshExternalConfig(t *testing.T) {
	env := newBridgeTestEnv(nil)
	route := testWatchRoute()
	version := env.hub.watcherVersion()
	env.hub.seedWatchers([]bot.DesktopWatchRoute{route}, version)
	if !env.hub.Watching(route) {
		t.Fatal("initial config seed did not apply")
	}

	env.hub.seedWatchers(nil, version)
	if env.hub.Watching(route) {
		t.Fatal("fresh external config update did not replace the watcher set")
	}
}

func TestBridgeApprovalRoutesToDetachedSession(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{
		{TabID: "tab-bg", Label: "后台任务", Detached: true, Ready: true},
	})

	env.hub.observe("tab-bg", event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ID: "appr-bg", Tool: "bash"}})
	if _, err := env.hub.Approve("appr-bg", true); err != nil {
		t.Fatalf("Approve on detached session: %v", err)
	}
	select {
	case got := <-env.approves:
		if got[0] != "tab-bg" {
			t.Fatalf("approve routed to %v, want tab-bg", got)
		}
	case <-time.After(time.Second):
		t.Fatal("detached approval was not routed")
	}
}

func TestBridgeTakeoverLifecycle(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{
		{TabID: "tab-1", Label: "会话一", Ready: true},
		{TabID: "tab-bg", Label: "后台", Detached: true},
	})
	route := testWatchRoute()

	// 后台会话拒绝接管。
	if _, err := env.hub.Takeover(route, "tab-bg"); err == nil {
		t.Fatal("takeover of a detached session should fail")
	}

	feedback, err := env.hub.Takeover(route, "tab-1")
	if err != nil {
		t.Fatalf("Takeover: %v", err)
	}
	if !strings.Contains(feedback, "已接管") {
		t.Fatalf("feedback = %q", feedback)
	}
	if env.hub.TakeoverTab(route) != "tab-1" {
		t.Fatalf("TakeoverTab = %q, want tab-1", env.hub.TakeoverTab(route))
	}
	select {
	case got := <-env.announced:
		if got[0] != "tab-1" || !strings.Contains(got[1], "接管") {
			t.Fatalf("announce = %v", got)
		}
	case <-time.After(time.Second):
		t.Fatal("takeover was not announced to the desktop transcript")
	}

	// 驱动输入路由到 tab。
	if _, err := env.hub.DriveInput(route, "跑一下测试"); err != nil {
		t.Fatalf("DriveInput: %v", err)
	}
	select {
	case got := <-env.driven:
		if got[0] != "tab-1" || got[1] != "跑一下测试" {
			t.Fatalf("driven = %v", got)
		}
	case <-time.After(time.Second):
		t.Fatal("drive input was not routed")
	}

	// 另一个聊天抢同一会话被拒。
	other := route
	other.ChatID = "chat-other"
	if _, err := env.hub.Takeover(other, "tab-1"); err == nil {
		t.Fatal("takeover by another chat should be rejected while held")
	}

	// 释放。
	if _, err := env.hub.Release(route); err != nil {
		t.Fatalf("Release: %v", err)
	}
	if env.hub.TakeoverTab(route) != "" {
		t.Fatal("binding should be cleared after release")
	}
	if _, err := env.hub.Release(route); err == nil {
		t.Fatal("second release should report no binding")
	}
}

func TestBridgeDriveInputRejectsRunningSession(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{
		{TabID: "tab-1", Label: "会话一", Ready: true, Running: true},
	})
	route := testWatchRoute()
	if _, err := env.hub.Takeover(route, "tab-1"); err != nil {
		t.Fatalf("Takeover: %v", err)
	}
	<-env.announced
	if _, err := env.hub.DriveInput(route, "hello"); err == nil || !strings.Contains(err.Error(), "正在执行中") {
		t.Fatalf("DriveInput on running session = %v, want busy rejection", err)
	}
}

func TestBridgeReclaimFromDesktopNotifiesController(t *testing.T) {
	env := newBridgeTestEnvSessions([]bot.DesktopSessionInfo{
		{TabID: "tab-1", Label: "会话一", Ready: true},
	})
	route := testWatchRoute()
	if _, err := env.hub.Takeover(route, "tab-1"); err != nil {
		t.Fatalf("Takeover: %v", err)
	}
	<-env.announced

	env.hub.reclaimFromDesktop("tab-1")
	if env.hub.TakeoverTab(route) != "" {
		t.Fatal("reclaim should clear the binding")
	}
	call := env.waitNotification(t)
	if !strings.Contains(call.msg.Text, "收回") || call.msg.ChatID != route.ChatID {
		t.Fatalf("reclaim notification = %+v", call)
	}

	// 未接管 tab 的 reclaim 是 no-op。
	env.hub.reclaimFromDesktop("tab-1")
	env.expectNoNotification(t)
}
