package profile

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v3"

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

func TestServerManagerAddAllowsMultipleAgentTeamServers(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())

	first := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(first))
	require.NotEmpty(t, first.ID)
	require.Empty(t, first.Name)

	second := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(second))
	require.NotEmpty(t, second.ID)
	require.NotEqual(t, first.ID, second.ID)
	require.Empty(t, second.Name)

	channels := mgr.Servers().All()
	require.Len(t, channels, 2)
}

func TestServerManagerAddReloadsLatestServersBeforeSaving(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	mgrA := New(dir)
	mgrB := New(dir)

	first := &AgentTeamServer{
		Enabled: true,
		Config:  []byte("server_url: https://agentteam.example.com\napi_key: key-one\n"),
	}
	second := &AgentTeamServer{
		Enabled: true,
		Config:  []byte("server_url: https://agentteam.example.com\napi_key: key-two\n"),
	}

	require.NoError(t, mgrA.Servers().Add(first))
	require.NoError(t, mgrB.Servers().Add(second))

	finalMgr := New(dir)
	channels := finalMgr.Servers().All()
	require.Len(t, channels, 2)

	apiKeys := make(map[string]struct{}, len(channels))
	for _, ch := range channels {
		apiKeys[channelConfigValue(t, ch, "api_key")] = struct{}{}
	}
	require.Contains(t, apiKeys, "key-one")
	require.Contains(t, apiKeys, "key-two")
}

func TestServerManagerUpdateConfigOverridesTypedServerFields(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())
	ch := &AgentTeamServer{
		Enabled:     true,
		ServerURL:   "https://old.example.test",
		WorkspaceID: "wsp_old",
		MachineID:   "mac_old",
		APIKey:      "key-old",
	}
	require.NoError(t, mgr.Servers().Add(ch))

	updated := *ch
	updated.Config = []byte(`
server_url: https://new.example.test
workspace_id: wsp_new
machine_id: mac_new
api_key: key-new
`)
	require.NoError(t, mgr.Servers().Update(updated))

	got, ok := mgr.Servers().Get(ch.ID)
	require.True(t, ok)
	require.Equal(t, "https://new.example.test", got.ServerURL)
	require.Equal(t, "wsp_new", got.WorkspaceID)
	require.Equal(t, "mac_new", got.MachineID)
	require.Equal(t, "key-new", got.APIKey)
	require.Equal(t, "https://new.example.test", channelConfigValue(t, got, "server_url"))
	require.Equal(t, "wsp_new", channelConfigValue(t, got, "workspace_id"))
	require.Equal(t, "mac_new", channelConfigValue(t, got, "machine_id"))
	require.Equal(t, "key-new", channelConfigValue(t, got, "api_key"))
}

func TestServerManagerCompareAndSwapDoesNotRestoreOverNewerConfig(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())
	initial := &AgentTeamServer{
		Enabled:   true,
		Name:      "initial-a",
		ServerURL: "https://a.example.test",
		APIKey:    "key-a",
	}
	require.NoError(t, mgr.Servers().Add(initial))
	require.Equal(t, uint64(1), initial.Revision)

	failedB := *initial
	failedB.Name = "failed-b"
	failedB.ServerURL = "https://b.example.test"
	require.NoError(t, mgr.Servers().Update(failedB))
	failedB, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, uint64(2), failedB.Revision)

	winnerC := failedB
	winnerC.Name = "winner-c"
	winnerC.ServerURL = "https://c.example.test"
	require.NoError(t, mgr.Servers().Update(winnerC))
	winnerC, ok = mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, uint64(3), winnerC.Revision)

	swapped, err := mgr.Servers().CompareAndSwap(failedB, *initial)
	require.NoError(t, err)
	require.False(t, swapped, "stale expected value replaced a newer configuration")
	got, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, winnerC, got)

	swapped, err = mgr.Servers().CompareAndSwap(winnerC, *initial)
	require.NoError(t, err)
	require.True(t, swapped)
	got, ok = mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, uint64(4), got.Revision)
	require.Equal(t, initial.Name, got.Name)
	require.Equal(t, initial.ServerURL, got.ServerURL)
}

func TestServerManagerCompareAndSwapRejectsABAValue(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())
	initial := &AgentTeamServer{Enabled: true, Name: "initial-a"}
	require.NoError(t, mgr.Servers().Add(initial))

	failedB := *initial
	failedB.Name = "failed-b"
	require.NoError(t, mgr.Servers().Update(failedB))
	staleB, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)

	winnerC := staleB
	winnerC.Name = "winner-c"
	require.NoError(t, mgr.Servers().Update(winnerC))
	winnerC, ok = mgr.Servers().Get(initial.ID)
	require.True(t, ok)

	newerB := winnerC
	newerB.Name = staleB.Name
	require.NoError(t, mgr.Servers().Update(newerB))
	currentB, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, staleB.Name, currentB.Name)
	require.NotEqual(t, staleB.Revision, currentB.Revision)

	swapped, err := mgr.Servers().CompareAndSwap(staleB, *initial)
	require.NoError(t, err)
	require.False(t, swapped, "stale compare-and-swap accepted an ABA value")
	got, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, currentB, got)
}

func TestServerManagerUpdateSnapshotRejectsStaleRevision(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())
	initial := &AgentTeamServer{Enabled: true, Name: "initial"}
	require.NoError(t, mgr.Servers().Add(initial))
	stale := *initial

	first := *initial
	first.Name = "first"
	committed, err := mgr.Servers().UpdateSnapshot(first)
	require.NoError(t, err)
	require.Equal(t, uint64(2), committed.Revision)
	require.Equal(t, "first", committed.Name)

	stale.Name = "stale"
	_, err = mgr.Servers().UpdateSnapshot(stale)
	require.ErrorIs(t, err, ErrServerRevisionConflict)
	got, ok := mgr.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, committed, got)
}

func TestServerManagerUpdateSnapshotConflictRefreshesDurableWinner(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	local := New(dir)
	initial := &AgentTeamServer{Enabled: true, Name: "initial"}
	require.NoError(t, local.Servers().Add(initial))
	stale := *initial

	remote := New(dir)
	winner, ok := remote.Servers().Get(initial.ID)
	require.True(t, ok)
	winner.Name = "remote winner"
	committed, err := remote.Servers().UpdateSnapshot(winner)
	require.NoError(t, err)

	stale.Name = "stale loser"
	_, err = local.Servers().UpdateSnapshot(stale)
	require.ErrorIs(t, err, ErrServerRevisionConflict)
	got, ok := local.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, committed, got, "conflict left Get pinned to the stale in-memory snapshot")
}

func TestServerManagerDeleteIfRevisionRejectsStaleSnapshot(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())
	server := &AgentTeamServer{Enabled: true, Name: "initial"}
	require.NoError(t, mgr.Servers().Add(server))
	staleRevision := server.Revision

	require.NoError(t, mgr.Servers().SetEnabled(server.ID, false))
	err := mgr.Servers().DeleteIfRevision(server.ID, staleRevision)
	require.ErrorIs(t, err, ErrServerRevisionConflict)
	current, ok := mgr.Servers().Get(server.ID)
	require.True(t, ok)
	require.False(t, current.Enabled)

	require.NoError(t, mgr.Servers().DeleteIfRevision(server.ID, current.Revision))
	_, ok = mgr.Servers().Get(server.ID)
	require.False(t, ok)
}

func TestServerManagerDeleteConflictRefreshesDurableWinner(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	local := New(dir)
	initial := &AgentTeamServer{Enabled: true, Name: "initial"}
	require.NoError(t, local.Servers().Add(initial))
	staleRevision := initial.Revision

	remote := New(dir)
	winner, ok := remote.Servers().Get(initial.ID)
	require.True(t, ok)
	winner.Name = "remote winner"
	committed, err := remote.Servers().UpdateSnapshot(winner)
	require.NoError(t, err)

	err = local.Servers().DeleteIfRevision(initial.ID, staleRevision)
	require.ErrorIs(t, err, ErrServerRevisionConflict)
	got, ok := local.Servers().Get(initial.ID)
	require.True(t, ok)
	require.Equal(t, committed, got, "delete conflict left retries on a stale in-memory revision")
}

func TestChannelDelete_RemovesSyncedAgentsAfterReload(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	mgrCreate := New(dir)

	ch := &AgentTeamServer{}
	require.NoError(t, mgrCreate.Servers().Add(ch))

	agentteamAgent := agent.Config{
		ID:      "agt_agentteam_reload",
		Name:    "AgentTeam Agent Reload",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch.ID),
	}
	require.NoError(t, mgrCreate.Agents().Add(agentteamAgent))

	mgrDelete := New(dir)
	require.NoError(t, mgrDelete.Servers().Delete(ch.ID))

	finalMgr := New(dir)
	_, found := finalMgr.Servers().Get(ch.ID)
	require.False(t, found)
	require.Empty(t, finalMgr.Agents().All())
}

func TestChannelDelete_RemovesDefaultSyncedAgentWithoutDeadlock(t *testing.T) {
	dir := t.TempDir()
	t.Setenv(ConfigDirEnvKey, dir)
	resetDefaultManagerForChannelTest(t)

	mgr := Default()
	ch := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(ch))

	agentteamAgent := agent.Config{
		ID:      "agt_agentteam_default",
		Name:    "AgentTeam Default",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch.ID),
	}
	require.NoError(t, mgr.Agents().Add(agentteamAgent))
	require.Equal(t, agentteamAgent.ID, mgr.Agents().Default())

	done := make(chan error, 1)
	go func() {
		done <- mgr.Servers().Delete(ch.ID)
	}()

	select {
	case err := <-done:
		require.NoError(t, err)
	case <-time.After(3 * time.Second):
		t.Fatal("channel delete deadlocked while removing the default agentteam-synced agent")
	}

	finalMgr := New(dir)
	require.Empty(t, finalMgr.Servers().All())
	require.Empty(t, finalMgr.Agents().All())
	require.Empty(t, finalMgr.Config().DefaultAgent)
}

func TestChannelDelete_RemovesSyncedAgents(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())

	// Add a channel.
	ch := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(ch))
	require.NotEmpty(t, ch.ID)

	// Add a local agent.
	localAgent := agent.Config{
		ID:      "agt_local",
		Name:    "Local Agent",
		Runtime: "claude",
		Source:  agent.NewLocalSource(),
	}
	require.NoError(t, mgr.Agents().Add(localAgent))

	// Add agentteam-synced agents from this channel.
	agentteamAgent1 := agent.Config{
		ID:      "agt_agentteam1",
		Name:    "AgentTeam Agent 1",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch.ID),
	}
	agentteamAgent2 := agent.Config{
		ID:      "agt_agentteam2",
		Name:    "AgentTeam Agent 2",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch.ID),
	}
	require.NoError(t, mgr.Agents().Add(agentteamAgent1))
	require.NoError(t, mgr.Agents().Add(agentteamAgent2))

	// Verify all 3 agents exist.
	require.Len(t, mgr.Agents().All(), 3)
	require.NoError(t, mgr.Agents().SetDefault(""))

	// Delete the channel.
	require.NoError(t, mgr.Servers().Delete(ch.ID))

	// AgentTeam-synced agents should be removed; local agent should remain.
	agents := mgr.Agents().All()
	require.Len(t, agents, 1)
	require.Equal(t, "agt_local", agents[0].ID)
	require.Empty(t, mgr.Agents().Default())

	// AgentTeamServer should be gone.
	_, found := mgr.Servers().Get(ch.ID)
	require.False(t, found)
}

func TestChannelDelete_DoesNotAffectOtherChannelAgents(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())

	// Add two channels.
	ch1 := &AgentTeamServer{}
	ch2 := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(ch1))
	require.NoError(t, mgr.Servers().Add(ch2))

	// Add agents from both channels.
	agent1 := agent.Config{
		ID:      "agt_from_ch1",
		Name:    "Agent from CH1",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch1.ID),
	}
	agent2 := agent.Config{
		ID:      "agt_from_ch2",
		Name:    "Agent from CH2",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch2.ID),
	}
	require.NoError(t, mgr.Agents().Add(agent1))
	require.NoError(t, mgr.Agents().Add(agent2))

	// Delete only ch1.
	require.NoError(t, mgr.Servers().Delete(ch1.ID))

	// Only agent from ch1 should be removed.
	agents := mgr.Agents().All()
	require.Len(t, agents, 1)
	require.Equal(t, "agt_from_ch2", agents[0].ID)
}

func TestAgentAll_CleansUpOrphanAgents(t *testing.T) {
	t.Parallel()

	mgr := New(t.TempDir())

	// Add a channel.
	ch := &AgentTeamServer{}
	require.NoError(t, mgr.Servers().Add(ch))

	// Add a agentteam-synced agent.
	agentteamAgent := agent.Config{
		ID:      "agt_orphan",
		Name:    "Orphan Agent",
		Runtime: "claude",
		Source:  agent.NewAgentTeamSource(ch.ID),
	}
	require.NoError(t, mgr.Agents().Add(agentteamAgent))

	// Add a local agent.
	localAgent := agent.Config{
		ID:      "agt_local",
		Name:    "Local Agent",
		Runtime: "claude",
		Source:  agent.NewLocalSource(),
	}
	require.NoError(t, mgr.Agents().Add(localAgent))

	require.Len(t, mgr.Agents().All(), 2)

	// Manually remove the server from the underlying data to simulate
	// external deletion (e.g., config file edit).
	mgr.mu.Lock()
	mgr.servers = nil
	mgr.mu.Unlock()

	// All() should clean up the orphan.
	agents := mgr.Agents().All()
	require.Len(t, agents, 1)
	require.Equal(t, "agt_local", agents[0].ID)
}

func channelConfigValue(t *testing.T, ch AgentTeamServer, key string) string {
	t.Helper()

	var cfg map[string]string
	require.NoError(t, yaml.Unmarshal(ch.Config, &cfg))
	return cfg[key]
}

func resetDefaultManagerForChannelTest(t *testing.T) {
	t.Helper()

	defaultMu.Lock()
	previousManager := defaultManager
	previousDir := defaultDir
	defaultManager = nil
	defaultDir = ""
	defaultMu.Unlock()

	t.Cleanup(func() {
		defaultMu.Lock()
		defaultManager = previousManager
		defaultDir = previousDir
		defaultMu.Unlock()
	})
}
