package agent

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
)

func bindCodexActiveTurnForFenceTest(driver *CodexAppServerDriver, turnID string) {
	driver.updateThreadSnapshot(appServerThread{
		ID:     "thread-root",
		Status: appServerThreadStatus{Type: "active"},
		Turns: []appServerTurn{{
			ID:     turnID,
			Status: appServerTurnStatus{Type: "inProgress"},
		}},
	})
}

func terminalCodexErrorForFenceTest(t *testing.T, driver *CodexAppServerDriver, turnID string) {
	t.Helper()
	driver.handleNotification(appServerEnvelope{
		Method: consts.ParsedEventKindError,
		Params: rawRPCResult(t, appServerErrorNotification{
			ThreadID:  "thread-root",
			TurnID:    turnID,
			WillRetry: false,
			Error:     json.RawMessage(`{"message":"terminal provider failure"}`),
		}),
	})
}

func TestCodexAppServerCompletedTurnFencesAllLateRootActivity(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 64)
	bindCodexActiveTurnForFenceTest(driver, "turn-done")

	// Register one real async activity before the terminal boundary. Its local
	// callback retains the turn id even after terminal cleanup forgets the map.
	driver.handleNotification(appServerEnvelope{
		Method: "item/started",
		Params: rawRPCResult(t, appServerItemNotification{
			ThreadID: "thread-root",
			TurnID:   "turn-done",
			Item: appServerItem{
				Type:      "mcpToolCall",
				ID:        "call-async",
				Server:    "agentteam",
				Tool:      "message_send",
				Arguments: json.RawMessage(`{"target":"#agentteam"}`),
			},
		}),
	})
	require.Len(t, driver.translatedLines, 1)
	<-driver.translatedLines

	terminalCodexErrorForFenceTest(t, driver, "turn-done")
	require.Len(t, driver.translatedLines, 2)
	<-driver.translatedLines // Error remains visible.
	<-driver.translatedLines // The authoritative terminal TurnEnd.

	lateDeltas := []struct {
		method string
		itemID string
		delta  string
	}{
		{"item/reasoning/summaryTextDelta", "reasoning-late", "late reasoning"},
		{"item/agentMessage/delta", "message-late", "late text"},
		{"item/plan/delta", "plan-late", "late plan"},
		{"item/commandExecution/outputDelta", "command-late", "late stdout"},
		{"item/fileChange/outputDelta", "file-late", "late patch"},
	}
	for _, late := range lateDeltas {
		driver.handleNotification(appServerEnvelope{
			Method: late.method,
			Params: rawRPCResult(t, appServerItemDeltaNotification{
				ThreadID: "thread-root",
				TurnID:   "turn-done",
				ItemID:   late.itemID,
				Delta:    late.delta,
			}),
		})
	}

	lateItems := []appServerItem{
		{Type: "commandExecution", ID: "command-late", Command: "echo late"},
		{Type: "fileChange", ID: "file-late", Changes: []appServerChange{{Path: "late.go", Kind: "update"}}},
		{Type: "mcpToolCall", ID: "mcp-late", Server: "agentteam", Tool: "message_send"},
		{Type: "dynamicToolCall", ID: "dynamic-late", Namespace: "agentteam", Tool: "message_send"},
		{Type: "webSearch", ID: "web-late", Query: "late query"},
		{Type: "collabAgentToolCall", ID: "collab-late", Tool: "wait", SenderThreadID: "thread-root"},
	}
	for _, item := range lateItems {
		driver.handleNotification(appServerEnvelope{
			Method: "item/started",
			Params: rawRPCResult(t, appServerItemNotification{
				ThreadID: "thread-root",
				TurnID:   "turn-done",
				Item:     item,
			}),
		})
		item.Output = json.RawMessage(`"late output"`)
		driver.handleNotification(appServerEnvelope{
			Method: "item/completed",
			Params: rawRPCResult(t, appServerItemNotification{
				ThreadID: "thread-root",
				TurnID:   "turn-done",
				Item:     item,
			}),
		})
	}

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, appServerItemNotification{
			ThreadID: "thread-root",
			TurnID:   "turn-done",
			Item: appServerItem{
				Type: "agentMessage",
				ID:   "message-completed-late",
				Text: "late completed text",
			},
		}),
	})
	terminalCodexErrorForFenceTest(t, driver, "turn-done")
	driver.handleNotification(appServerEnvelope{
		Method: "turn/started",
		Params: rawRPCResult(t, appServerTurnNotification{
			ThreadID: "thread-root",
			Turn:     appServerTurn{ID: "turn-done", Status: appServerTurnStatus{Type: "inProgress"}},
		}),
	})

	// This is the path used when an MCP HTTP callback finishes after TurnEnd.
	driver.emitMCPToolOutputTextForTurn("thread-root", "turn-done", "call-async", "agentteam.message_send", "late async result")

	require.Empty(t, driver.translatedLines, "no completed-turn activity may follow TurnEnd")
	status, activeTurnID, ok := driver.currentThreadState("thread-root")
	require.True(t, ok)
	require.Equal(t, "idle", normalizeThreadStatus(status).Type)
	require.Empty(t, activeTurnID)
	driver.mu.Lock()
	require.Empty(t, driver.toolActivities)
	driver.mu.Unlock()
}

func TestCodexAppServerCompletedTurnPreservesAckUsageAndSettings(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 8)
	driver.effectiveModel = "gpt-before"
	bindCodexActiveTurnForFenceTest(driver, "turn-done")
	terminalCodexErrorForFenceTest(t, driver, "turn-done")
	<-driver.translatedLines
	<-driver.translatedLines

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, appServerItemNotification{
			ThreadID: "thread-root",
			TurnID:   "turn-done",
			Item: appServerItem{
				Type:     "userMessage",
				ID:       "user-late",
				ClientID: "runtime-input-accepted",
			},
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "thread/settings/updated",
		Params: rawRPCResult(t, map[string]any{
			"threadId": "thread-root",
			"threadSettings": map[string]any{
				"model": "gpt-after",
			},
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "thread/tokenUsage/updated",
		Params: rawRPCResult(t, appServerThreadTokenUsageNotification{
			ThreadID: "thread-root",
			TurnID:   "turn-done",
			TokenUsage: appServerThreadTokenUsage{
				Last:  appServerTokenUsageBreakdown{InputTokens: 3, OutputTokens: 2},
				Total: appServerTokenUsageBreakdown{InputTokens: 3, OutputTokens: 2},
			},
		}),
	})

	require.Len(t, driver.translatedLines, 2)
	ack := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, ack, 1)
	require.Equal(t, consts.ParsedEventKindRuntimeInputAcked, ack[0].Kind)
	require.Equal(t, "runtime-input-accepted", ack[0].RuntimeInputID)
	usage := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, usage, 1)
	require.Equal(t, consts.ParsedEventKindUsage, usage[0].Kind)
	require.Equal(t, "gpt-after", usage[0].Model)
	driver.mu.Lock()
	require.Equal(t, "gpt-after", driver.modelByThread["thread-root"])
	driver.mu.Unlock()
}

func TestCodexAppServerOnlyCurrentTurnMayPublishRootActivity(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 4)
	driver.updateThreadSnapshot(appServerThread{
		ID:     "thread-root",
		Status: appServerThreadStatus{Type: "active"},
		Turns: []appServerTurn{
			{ID: "turn-old", Status: appServerTurnStatus{Type: "completed"}},
			{ID: "turn-current", Status: appServerTurnStatus{Type: "inProgress"}},
		},
	})

	for _, turnID := range []string{"turn-old", "turn-current"} {
		driver.handleNotification(appServerEnvelope{
			Method: "item/agentMessage/delta",
			Params: rawRPCResult(t, appServerItemDeltaNotification{
				ThreadID: "thread-root",
				TurnID:   turnID,
				ItemID:   "message-" + turnID,
				Delta:    "text from " + turnID,
			}),
		})
	}

	require.Len(t, driver.translatedLines, 1)
	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, "text from turn-current", events[0].Text)
}

func TestCodexAppServerLatestTurnStartedReplacesStaleCachedActiveTurn(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)
	bindCodexActiveTurnForFenceTest(driver, "turn-stale-cache")

	driver.handleNotification(appServerEnvelope{
		Method: "turn/started",
		Params: rawRPCResult(t, appServerTurnNotification{
			ThreadID: "thread-root",
			Turn:     appServerTurn{ID: "turn-authoritative", Status: appServerTurnStatus{Type: "inProgress"}},
		}),
	})

	require.Equal(t, "turn-authoritative", driver.currentActiveTurnID(), "latest TurnStarted is authoritative even if an older completion was missed")
	require.Len(t, driver.translatedLines, 1)
	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindThinking, events[0].Kind)
	require.True(t, parsedBoolMetadata(events[0].Metadata, codexAuthoritativeTurnStartMetadataKey))
}
