package dispatcher

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"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/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

func TestApplyCodexAppServerResumePolicy(t *testing.T) {
	t.Run("legacy thread cuts over to a fresh thread", func(t *testing.T) {
		workDir := t.TempDir()
		require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
			LastStartConfig: &types.AgentStartConfig{InstructionsFingerprint: ""},
		}))

		startCfg := testLaunchConfig(drivercontract.LaunchConfig{SessionID: "sess-1"}, testLaunchSurfaces{BaseInstructions: "base", DeveloperInstructions: "developer"})
		applyCodexAppServerResumePolicy(workDir, &startCfg, "")

		require.Empty(t, startCfg.SessionID)
	})

	t.Run("changed instructions cut over to a fresh thread", func(t *testing.T) {
		workDir := t.TempDir()
		require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
			LastStartConfig: &types.AgentStartConfig{
				InstructionsFingerprint: codexAppServerInstructionsFingerprint("old-base", "old-developer"),
			},
		}))

		startCfg := testLaunchConfig(drivercontract.LaunchConfig{SessionID: "sess-2"}, testLaunchSurfaces{BaseInstructions: "new-base", DeveloperInstructions: "new-developer"})
		applyCodexAppServerResumePolicy(workDir, &startCfg, "")

		require.Empty(t, startCfg.SessionID)
	})

	t.Run("matching instructions keep normal resume", func(t *testing.T) {
		workDir := t.TempDir()
		fingerprint := codexAppServerInstructionsFingerprint("base", "developer")
		require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
			LastStartConfig: &types.AgentStartConfig{
				InstructionsFingerprint: fingerprint,
			},
		}))

		startCfg := testLaunchConfig(drivercontract.LaunchConfig{SessionID: "sess-3"}, testLaunchSurfaces{BaseInstructions: "base", DeveloperInstructions: "developer"})
		applyCodexAppServerResumePolicy(workDir, &startCfg, "")

		require.Equal(t, "sess-3", startCfg.SessionID)
	})

	t.Run("changed MCP configuration keeps the provider thread", func(t *testing.T) {
		workDir := t.TempDir()
		baseFingerprint := instructionsFingerprint("base", "developer")
		oldToolSet := []types.NamedMCPConfig{{
			Name: "agentteam",
			URL:  "https://agentteam.example.com/mcp/server/main/agent-1",
		}}
		newToolSet := []types.NamedMCPConfig{{
			Name: "agentteam",
			URL:  "https://agentteam.example.com/mcp/server/main/agent-2",
		}}
		require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
			LastStartConfig: &types.AgentStartConfig{
				InstructionsFingerprint: codexAppServerSessionFingerprint(baseFingerprint, oldToolSet),
			},
		}))

		startCfg := testLaunchConfig(drivercontract.LaunchConfig{SessionID: "sess-4"}, testLaunchSurfaces{BaseInstructions: "base", DeveloperInstructions: "developer"})
		applyCodexAppServerResumePolicy(workDir, &startCfg, codexAppServerSessionFingerprint(baseFingerprint, newToolSet))

		require.Equal(t, "sess-4", startCfg.SessionID)
	})
}

func TestCodexAppServerLegacyMCPFingerprintMatchesInstructionsOnly(t *testing.T) {
	baseFingerprint := instructionsFingerprint("base", "developer")
	legacy := baseFingerprint + ":codex-app-server-dynamic-tools-v2:mcp:legacy-surface"
	current := codexAppServerInstructionsFingerprint("base", "developer")
	require.True(t, codexAppServerInstructionsFingerprintMatches(legacy, current))
	require.False(t, codexAppServerInstructionsFingerprintMatches(
		instructionsFingerprint("base", "changed"),
		current,
	))
}

func TestCodexAppServerSessionFingerprintIgnoresMCPConfiguration(t *testing.T) {
	instructionFingerprint := instructionsFingerprint("base", "developer")
	mcps := []types.NamedMCPConfig{
		{
			Name:    "agentteam",
			URL:     "https://agentteam.example.com/mcp/server/main/agent-1",
			Headers: map[string]string{"Authorization": "Bearer token-1"},
		},
	}
	sameConfigDifferentToken := []types.NamedMCPConfig{
		{
			Name:    "agentteam",
			URL:     "https://agentteam.example.com/mcp/server/main/agent-1",
			Headers: map[string]string{"Authorization": "Bearer token-2"},
		},
	}
	changedConfig := []types.NamedMCPConfig{
		{
			Name:    "agentteam",
			URL:     "https://agentteam.example.com/mcp/server/main/agent-1",
			Headers: map[string]string{"Authorization": "Bearer token-1"},
		},
		{
			Name: "external",
			URL:  "https://mcp.example.com/rpc",
		},
	}

	fingerprint := codexAppServerSessionFingerprint(instructionFingerprint, mcps)
	require.NotEmpty(t, fingerprint)
	require.Equal(t, fingerprint, codexAppServerSessionFingerprint(instructionFingerprint, sameConfigDifferentToken))
	require.Equal(t, fingerprint, codexAppServerSessionFingerprint(instructionFingerprint, changedConfig))
}

func TestValidateSessionResume_DaemonURLChangeDoesNotDropProviderSession(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(sessionResumeStatePath(workDir), []byte(`{
		"lastSessionId":"sess-port-zero",
		"lastServerUrl":"http://127.0.0.1:43000",
		"lastStartConfig":{"sessionId":"sess-port-zero","instructionsFingerprint":"`+instructionsFingerprint("base", "developer")+`"}
	}`), 0o600))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-port-zero",
		string(runtimeconfig.TypeOpenCode),
		"http://127.0.0.1:43101",
		instructionsFingerprint("base", "developer"),
	)
	require.Equal(t, "sess-port-zero", sessionID)
}

func TestValidateSessionResume_CodexAppServerAllowsResume(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()

	sessionID := dispatcher.validateSessionResume(workDir, "sess-1", string(runtimeconfig.TypeCodex), "https://agentteam.example.com", "")
	require.Equal(t, "sess-1", sessionID)
}

func TestValidateSessionResume_CodexDaemonRestartAllowsSavedSafeSession(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastSessionID:     "sess-safe",
		SessionResumeSafe: true,
		LastStartConfig: &types.AgentStartConfig{
			SessionID: "sess-safe",
		},
	}))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-safe",
		string(runtimeconfig.TypeCodex),
		"https://agentteam.example.com",
		"",
	)
	require.Equal(t, "sess-safe", sessionID)
}

func TestValidateSessionResume_CodexDaemonRestartKeepsSavedUnsafeSessionForRuntimeAuthority(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastSessionID:     "sess-unsafe",
		SessionResumeSafe: false,
		LastStartConfig: &types.AgentStartConfig{
			SessionID: "sess-unsafe",
		},
	}))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-unsafe",
		string(runtimeconfig.TypeCodex),
		"https://agentteam.example.com",
		"",
	)
	require.Equal(t, "sess-unsafe", sessionID)
}

func TestValidateSessionResume_Format3ProviderSealDoesNotBlockRuntimeAuthority(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(sessionResumeStatePath(workDir), []byte(`{
		"lastSessionId": "sess-sealed",
		"sessionResumeSafe": true,
		"lastStartConfig": {"runtime": "codex", "sessionId": "sess-sealed"},
		"providerSessionSeal": {"providerSessionId": "sess-sealed"},
		"runtimeDeliveryOwnershipVersion": 3
	}`), 0o600))
	records, err := newSessionDeliveryOwnershipStore(workDir).LoadDeliveryOwnership()
	require.NoError(t, err)
	require.Empty(t, records)

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-sealed",
		string(runtimeconfig.TypeCodex),
		"https://agentteam.example.com",
		"",
	)

	require.Equal(t, "sess-sealed", sessionID)
	_, err = persistStartConfigState(workDir, string(runtimeconfig.TypeCodex), "https://agentteam.example.com", "", &types.AgentStartConfig{
		Runtime:   string(runtimeconfig.TypeCodex),
		ServerURL: "https://agentteam.example.com",
	})
	require.NoError(t, err)
	data, err := os.ReadFile(sessionResumeStatePath(workDir))
	require.NoError(t, err)
	require.Contains(t, string(data), `"runtimeDeliveryOwnershipVersion":2`)
	require.NotContains(t, string(data), "providerSessionSeal")
}

func TestValidateSessionResume_CodexLegacyLocalResumeStateUsesRuntimeAuthority(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	// Legacy records did not persist SessionResumeSafe. That delivery fence does
	// not decide provider-thread existence; runtime authority receives the id.
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, sessionResumeStateFilename),
		[]byte(`{"lastSessionId":"sess-legacy-local"}`),
		0o644,
	))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-legacy-local",
		string(runtimeconfig.TypeCodex),
		"https://agentteam.example.com",
		"",
	)
	require.Equal(t, "sess-legacy-local", sessionID)
}

func TestValidateSessionResume_CodexUnsafeLocalStateDoesNotBlockDifferentServerSession(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastSessionID:     "sess-old-unsafe",
		SessionResumeSafe: false,
	}))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-server-authoritative",
		string(runtimeconfig.TypeCodex),
		"https://agentteam.example.com",
		"",
	)
	require.Equal(t, "sess-server-authoritative", sessionID)
}

func TestValidateSessionResume_DropsLegacySessionWithoutStoredFingerprint(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-legacy",
		string(runtimeconfig.TypeOpenCode),
		"https://agentteam.example.com",
		"current-fingerprint",
	)
	require.Empty(t, sessionID)
}

func TestValidateSessionResume_DropsUnreadableResumeStateForResumableRuntime(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, sessionResumeStateFilename), []byte(`{"lastStartConfig":`), 0o644))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-corrupt",
		string(runtimeconfig.TypeOpenCode),
		"https://agentteam.example.com",
		"current-fingerprint",
	)
	require.Empty(t, sessionID)
}

func TestValidateSessionResume_DropsChangedInstructionsForResumableRuntime(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastStartConfig: &types.AgentStartConfig{
			InstructionsFingerprint: "old-fingerprint",
		},
	}))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-changed",
		string(runtimeconfig.TypeOpenCode),
		"https://agentteam.example.com",
		"new-fingerprint",
	)
	require.Empty(t, sessionID)
}

func TestValidateSessionResume_KeepsMatchingInstructionsForResumableRuntime(t *testing.T) {
	dispatcher := &Dispatcher{}
	workDir := t.TempDir()
	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastStartConfig: &types.AgentStartConfig{
			InstructionsFingerprint: "same-fingerprint",
		},
	}))

	sessionID := dispatcher.validateSessionResume(
		workDir,
		"sess-stable",
		string(runtimeconfig.TypeOpenCode),
		"https://agentteam.example.com",
		"same-fingerprint",
	)
	require.Equal(t, "sess-stable", sessionID)
}

func TestResolveCompiledSessionID_CodexAppServerUsesCompilerInstructions(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte("Always validate migrations before shipping."), 0o644))

	ag := agent.Config{
		ID:          "agent-1",
		Name:        "reviewer",
		Description: "Review backend changes carefully.",
	}
	binding := conversationBinding{
		channelID:       "channel-1",
		targetScopeType: "channel",
		targetID:        "thread:alpha",
		serverSessionID: "thread-existing",
	}
	msg := types.InboxMessage{
		MessageID:     "msg-1",
		Target:        "thread:alpha",
		AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice", TargetID: "alice"},
		Content:       "Please continue the rollout.",
	}
	skillDocParts := types.SkillDocParts{Core: "Core rules"}

	input := buildPromptCompileInput(
		workDir,
		ag,
		runtimeconfig.TypeCodex,
		binding,
		msg,
		resolvedInstructionContext{skillDocParts: skillDocParts},
	)
	compiler := promptcompiler.New(promptcompiler.DefaultPolicy())
	compiled, err := compiler.CompileThreadContract(context.Background(), promptcompiler.EventThreadResume, "evt-preview", input)
	require.NoError(t, err)
	rendered, err := compiler.RenderRuntimeInput(context.Background(), compiled, string(runtimeconfig.TypeCodex), promptcompiler.RenderOptions{})
	require.NoError(t, err)

	require.NoError(t, saveSessionResumeState(workDir, sessionResumeState{
		LastStartConfig: &types.AgentStartConfig{
			InstructionsFingerprint: codexAppServerInstructionsFingerprint(
				promptcompiler.ExtractStringSlot(rendered, "baseInstructions"),
				promptcompiler.ExtractStringSlot(rendered, "developerInstructions"),
			),
		},
	}))

	sessionID, err := resolveCompiledSessionID(
		workDir,
		ag,
		runtimeconfig.TypeCodex,
		binding,
		msg,
		resolvedInstructionContext{skillDocParts: skillDocParts},
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, "thread-existing", sessionID)
}
