package dispatcher

import (
	"context"
	"testing"
	"time"

	"github.com/pingcap/failpoint"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

func useRunningStubDriver(t *testing.T, runtimeBinaries ...string) {
	t.Helper()

	stubRuntimeBinaries(t, runtimeBinaries...)
	require.NoError(t, failpoint.Enable("dispatcher-new-agent-driver", "return(true)"))
	SetNewAgentDriverHook(func(runtimeconfig.RuntimeType) (drivercontract.Driver, error) {
		return runningStubDriver{}, nil
	})
	t.Cleanup(func() {
		SetNewAgentDriverHook(nil)
		require.NoError(t, failpoint.Disable("dispatcher-new-agent-driver"))
	})
}

func TestDeliverSessionRestartsWhenLiveRuntimeStartFingerprintChanges(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	_ = initTokenForTest(t)
	useRunningStubDriver(t, "claude", "codex")

	d := mustNewDispatcher("http://server.test")
	t.Cleanup(d.Close)

	agentID := "agent-runtime-mismatch"
	claudeAgent := agent.Config{
		ID:             agentID,
		Name:           "Runtime Mismatch Agent",
		Runtime:        "claude",
		PermissionMode: types.AgentPermissionModeFullAccess,
		Model:          "claude-sonnet-4",
	}
	claudeRuntime := runtimeconfig.Runtime{Name: "claude", Type: runtimeconfig.TypeClaudeCode, Binary: "claude"}

	key, err := d.DeliverSession(context.Background(), claudeAgent, claudeRuntime, agentteamRuntimeFingerprintMessage(agentID, "msg-claude", "initial claude turn"))
	require.NoError(t, err)
	require.Equal(t, agentID, key)

	session, ok := d.sessionManager.Get(key)
	require.True(t, ok)
	session.mu.Lock()
	firstProcess := session.process
	firstFingerprint := session.runtimeStartFingerprint
	session.suppressCheckpoints = true
	session.mu.Unlock()
	require.NotNil(t, firstProcess)
	require.NotEmpty(t, firstFingerprint)

	codexAgent := agent.Config{
		ID:             agentID,
		Name:           "Runtime Mismatch Agent",
		Runtime:        "codex",
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
		RuntimePolicy: agent.RuntimePolicySnapshot{
			SchemaVersion:  1,
			Generation:     2,
			Fingerprint:    "server-policy-codex",
			Runtime:        "codex",
			RuntimeType:    "codex",
			PermissionMode: types.AgentPermissionModeWorkspaceWrite,
		},
		Model:           "gpt-5.4",
		ReasoningEffort: "medium",
		LLMMode:         string(agent.LLMModeInline),
		BaseURL:         "https://llm.example.test/v1",
		APIKey:          "sk-current",
		WireAPI:         "responses",
	}
	codexRuntime := runtimeconfig.Runtime{Name: "codex", Type: runtimeconfig.TypeCodex, Binary: "codex"}

	key, err = d.DeliverSession(context.Background(), codexAgent, codexRuntime, agentteamRuntimeFingerprintMessage(agentID, "msg-codex", "deliver to current codex config"))
	require.NoError(t, err)
	require.Equal(t, agentID, key)
	require.True(t, firstProcess.WaitForExit(time.Second), "stale Claude process should be stopped before reuse")

	restarted, ok := d.sessionManager.Get(key)
	require.True(t, ok)
	restarted.mu.Lock()
	defer restarted.mu.Unlock()
	restarted.suppressCheckpoints = true
	require.NotSame(t, firstProcess, restarted.process)
	require.NotEmpty(t, restarted.runtimeStartFingerprint)
	require.NotEqual(t, firstFingerprint, restarted.runtimeStartFingerprint)
	require.Equal(t, runtimeconfig.TypeCodex, restarted.runtime.Type)
	require.Equal(t, "codex", restarted.agent.Runtime)
}

func TestDeliverSessionKeepsLiveProcessWhenRuntimeStartFingerprintMatches(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	_ = initTokenForTest(t)
	useRunningStubDriver(t, "claude")

	d := mustNewDispatcher("http://server.test")
	t.Cleanup(d.Close)

	agentID := "agent-runtime-match"
	ag := agent.Config{
		ID:             agentID,
		Name:           "Original Display Name",
		Runtime:        "claude",
		PermissionMode: types.AgentPermissionModeFullAccess,
		Model:          "claude-sonnet-4",
	}
	rt := runtimeconfig.Runtime{Name: "claude", Type: runtimeconfig.TypeClaudeCode, Binary: "claude"}

	key, err := d.DeliverSession(context.Background(), ag, rt, agentteamRuntimeFingerprintMessage(agentID, "msg-1", "initial turn"))
	require.NoError(t, err)

	session, ok := d.sessionManager.Get(key)
	require.True(t, ok)
	session.mu.Lock()
	firstProcess := session.process
	firstFingerprint := session.runtimeStartFingerprint
	session.suppressCheckpoints = true
	session.mu.Unlock()
	require.NotNil(t, firstProcess)
	require.NotEmpty(t, firstFingerprint)

	ag.Name = "Renamed Display Only"
	key, err = d.DeliverSession(context.Background(), ag, rt, agentteamRuntimeFingerprintMessage(agentID, "msg-2", "same runtime config"))
	require.NoError(t, err)

	session, ok = d.sessionManager.Get(key)
	require.True(t, ok)
	session.mu.Lock()
	currentProcess := session.process
	currentFingerprint := session.runtimeStartFingerprint
	session.suppressCheckpoints = true
	session.mu.Unlock()
	require.Same(t, firstProcess, currentProcess)
	require.Equal(t, firstFingerprint, currentFingerprint)
	require.Equal(t, "running", currentProcess.State())

}

func TestRefreshRunningAgentStartConfigStopsStaleInlineProviderOverride(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	_ = initTokenForTest(t)
	useRunningStubDriver(t, "codex")

	d := mustNewDispatcher("http://server.test")
	t.Cleanup(d.Close)

	agentID := "agent-start-config-inline-provider"
	require.NoError(t, profile.Default().Agents().Add(agent.Config{
		ID:             agentID,
		Name:           "Start Config Inline Provider Agent",
		Runtime:        "codex",
		PermissionMode: types.AgentPermissionModeFullAccess,
		Model:          "gpt-5.4",
		LLMMode:        string(agent.LLMModeInline),
		BaseURL:        "https://llm.initial.test/v1",
		APIKey:         "sk-initial",
		WireAPI:        "responses",
	}))

	ag, ok := profile.Default().Agents().Get(agentID)
	require.True(t, ok)
	rt := runtimeconfig.Runtime{Name: "codex", Type: runtimeconfig.TypeCodex, Binary: "codex"}
	key, err := d.DeliverSession(context.Background(), ag, rt, agentteamRuntimeFingerprintMessage(agentID, "msg-inline-provider", "initial turn"))
	require.NoError(t, err)

	session, ok := d.sessionManager.Get(key)
	require.True(t, ok)
	session.mu.Lock()
	firstProcess := session.process
	firstFingerprint := session.runtimeStartFingerprint
	session.suppressCheckpoints = true
	session.mu.Unlock()
	require.NotNil(t, firstProcess)
	require.NotEmpty(t, firstFingerprint)

	changed := d.RefreshRunningAgentStartConfig(agentID, types.AgentStartConfig{
		Name:           "start-config-inline-provider-agent",
		Runtime:        "codex",
		PermissionMode: types.AgentPermissionModeFullAccess,
		LLMMode:        "inline",
		BaseURL:        "https://llm.updated.test/v1",
		APIKey:         "sk-updated",
		WireAPI:        "responses",
	})
	require.True(t, changed)
	require.Eventually(t, func() bool { return firstProcess.State() == "stopped" }, time.Second, 10*time.Millisecond, "running process should be stopped when agent:start changes inline provider launch config")
	require.True(t, firstProcess.WaitForExit(time.Second))
	_, ok = d.sessionManager.Get(key)
	// p3088 three-state model: a session with a live runtime delivery owner is
	// retained for late-ACK reconciliation instead of removed on config restart.
	require.True(t, ok)
}

func TestRuntimeStartFingerprintChangesWhenRuntimeDebugToggles(t *testing.T) {
	ag := agent.Config{
		ID:             "agent-runtime-debug",
		Name:           "Runtime Debug Agent",
		Runtime:        "opencode",
		PermissionMode: types.AgentPermissionModeFullAccess,
		Model:          "deepseek-v4-pro",
	}
	rt := runtimeconfig.Runtime{Name: "opencode", Type: runtimeconfig.TypeOpenCode, Binary: "opencode"}
	policy := currentAgentRuntimePolicy(ag, rt)
	startCfg := drivercontract.LaunchConfig{
		RuntimeType:    rt.Type,
		PermissionMode: types.AgentPermissionModeFullAccess,
		Model:          "deepseek-v4-pro",
	}

	disabled := runtimeStartFingerprint(ag, rt, policy, startCfg)
	startCfg.RuntimeDebugEnabled = true
	enabled := runtimeStartFingerprint(ag, rt, policy, startCfg)

	require.NotEmpty(t, disabled)
	require.NotEmpty(t, enabled)
	require.NotEqual(t, disabled, enabled)
}

func TestCatalogRevisionHintDoesNotEnterRuntimeStartFingerprint(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	_ = initTokenForTest(t)
	d := mustNewDispatcher("")
	t.Cleanup(d.Close)

	agentID := "agent-extension-replacement"
	ag := agent.Config{
		ID: agentID, Name: "Extension Agent", Runtime: "codex",
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
	}
	rt := runtimeconfig.Runtime{Name: "codex", Type: runtimeconfig.TypeCodex, Binary: "codex"}
	withoutCatalogFingerprint, err := currentRuntimeStartFingerprint(ag, rt)
	require.NoError(t, err)
	process, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{AgentID: agentID, WorkDir: t.TempDir()},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	session := &interactiveSession{
		sessionKey: agentID, agentID: agentID, serverAgentID: agentID,
		agent: ag, runtime: rt,
		binding: conversationBinding{channelID: "agentteam-main", serverAgentID: agentID},
		process: process, runtimeStartFingerprint: withoutCatalogFingerprint,
		suppressCheckpoints: true,
	}
	d.sessionManager.Set(agentID, session)

	restarted, err := d.restartSessionIfRuntimeStartConfigChanged(
		session,
		ag,
		rt,
		conversationBinding{},
	)
	require.NoError(t, err)
	require.False(t, restarted, "a matching no-Catalog process must be retained")
	require.Equal(t, "running", process.State())

	// The machine revision is only a notification. It must not enter the
	// runtime-start fingerprint or retire the live process before the
	// authorization-projected Catalog has actually been fetched and compared.
	d.ObserveAgentExtensionCatalogHints("agentteam-main", []types.MachineAgentInfo{{
		ID:               agentID,
		ExtensionCatalog: &types.ExtensionCatalogHint{Version: 1},
	}})
	require.Equal(t, "running", process.State())

	restarted, err = d.restartSessionIfRuntimeStartConfigChanged(
		session,
		ag,
		rt,
		session.binding,
	)
	require.NoError(t, err)
	require.False(t, restarted)
	require.Equal(t, "running", process.State())
	_, exists := d.sessionManager.Get(agentID)
	require.True(t, exists)
}

func agentteamRuntimeFingerprintMessage(agentID string, messageID string, prompt string) *types.Message {
	return types.NewMessage(messageID, types.MessageInput{
		Prompt:  prompt,
		Channel: "agentteam-main",
		Meta: &types.AgentTeamMeta{
			ID:        messageID,
			Target:    "dm:@alice",
			AgentID:   agentID,
			Timestamp: time.Now().Format(time.RFC3339),
			AuthorProfile: types.AuthorProfile{
				ActorType:   "human",
				DisplayName: "alice",
				TargetID:    "alice",
			},
		},
	})
}
