package daemon

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

type managedSkillRefreshTestSyncer struct {
	calls chan string
	ctxs  chan context.Context
}

type managedAssetRefreshTestMemoryProvider struct {
	calls chan string
	ctxs  chan context.Context
}

func (p *managedAssetRefreshTestMemoryProvider) Lookup(ctx context.Context, agentID string) (dispatcher.MemoryAccess, dispatcher.MemoryAccessState, error) {
	p.calls <- agentID
	p.ctxs <- ctx
	return dispatcher.MemoryAccess{}, dispatcher.MemoryAccessUnbound, nil
}

type managedAssetRefreshTestKBSyncer struct {
	calls chan string
	ctxs  chan context.Context
}

func (s *managedAssetRefreshTestKBSyncer) SyncAgent(ctx context.Context, agentID, _ string) (dispatcher.SkillLookupState, error) {
	s.calls <- agentID
	s.ctxs <- ctx
	return dispatcher.SkillLookupUnbound, nil
}

func newManagedSkillRefreshTestSyncer() *managedSkillRefreshTestSyncer {
	return &managedSkillRefreshTestSyncer{calls: make(chan string, 8), ctxs: make(chan context.Context, 1)}
}

func (s *managedSkillRefreshTestSyncer) SyncAgent(ctx context.Context, agentID, _ string) ([]string, dispatcher.SkillLookupState, error) {
	s.calls <- agentID
	s.ctxs <- ctx
	return nil, dispatcher.SkillLookupUnbound, nil
}

func (s *managedSkillRefreshTestSyncer) CachedPaths(string) []string   { return nil }
func (s *managedSkillRefreshTestSyncer) CachedRepoIDs(string) []string { return nil }
func (s *managedSkillRefreshTestSyncer) CacheState(string) (dispatcher.SkillCacheState, bool) {
	return dispatcher.SkillCacheAuthoritativeEmpty, true
}

type managedSkillRefreshChannelRegistrar interface {
	Register(types.ServerConnection)
}

type managedSkillRefreshTestChannel struct {
	id          string
	serverURL   string
	workspaceID string
}

func (c *managedSkillRefreshTestChannel) ID() string                  { return c.id }
func (c *managedSkillRefreshTestChannel) Enabled() bool               { return true }
func (c *managedSkillRefreshTestChannel) Start(context.Context) error { return nil }
func (c *managedSkillRefreshTestChannel) Stop(context.Context) error  { return nil }
func (c *managedSkillRefreshTestChannel) Send(context.Context, types.Metadata, types.OutboundMessage) error {
	return nil
}
func (c *managedSkillRefreshTestChannel) ServerURL() string   { return c.serverURL }
func (c *managedSkillRefreshTestChannel) WorkspaceID() string { return c.workspaceID }

func TestManagedAssetRefreshLoopRefreshesEveryAssetImmediately(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	require.NoError(t, profile.Default().Servers().Add(&profile.AgentTeamServer{ID: "agentteam-main", Enabled: true}))
	require.NoError(t, profile.Default().Agents().Add(agent.Config{
		ID:      "agt_startup",
		Runtime: "codex",
		Source:  agent.NewAgentTeamSource("agentteam-main"),
	}))

	disp := newDaemonTestDispatcher("http://server.test")
	registrar, ok := disp.ServerConnections().(managedSkillRefreshChannelRegistrar)
	require.True(t, ok)
	registrar.Register(&managedSkillRefreshTestChannel{id: "agentteam-main", serverURL: "https://agentteam.test", workspaceID: "ws_1"})
	syncer := newManagedSkillRefreshTestSyncer()
	disp.SetSkillSyncer(syncer)
	memoryProvider := &managedAssetRefreshTestMemoryProvider{calls: make(chan string, 1), ctxs: make(chan context.Context, 1)}
	disp.SetMemoryAccessProvider(memoryProvider)
	kbSyncer := &managedAssetRefreshTestKBSyncer{calls: make(chan string, 1), ctxs: make(chan context.Context, 1)}
	disp.SetKnowledgeBaseSyncer(kbSyncer)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	d := &Daemon{dispatcher: disp}
	d.startManagedAssetRefreshLoop(ctx)

	requireManagedAssetRefreshCall(t, "skill", syncer.calls)
	requireManagedAssetRefreshCall(t, "knowledge base", kbSyncer.calls)
	requireManagedAssetRefreshCall(t, "memory", memoryProvider.calls)
	skillCtx := <-syncer.ctxs
	kbCtx := <-kbSyncer.ctxs
	memoryCtx := <-memoryProvider.ctxs
	require.NotEqual(t, skillCtx, kbCtx)
	require.NotEqual(t, kbCtx, memoryCtx)
	_, skillHasDeadline := skillCtx.Deadline()
	_, kbHasDeadline := kbCtx.Deadline()
	_, memoryHasDeadline := memoryCtx.Deadline()
	require.True(t, skillHasDeadline)
	require.True(t, kbHasDeadline)
	require.True(t, memoryHasDeadline)
}

func requireManagedAssetRefreshCall(t *testing.T, asset string, calls <-chan string) {
	t.Helper()
	select {
	case got := <-calls:
		require.Equal(t, "agt_startup", got)
	case <-time.After(time.Second):
		t.Fatalf("managed %s startup refresh did not run immediately", asset)
	}
}

func TestReconcileManagedRepositoriesSkipsWhenSkillCacheIncomplete(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	require.NoError(t, profile.Default().Servers().Add(&profile.AgentTeamServer{ID: "agentteam-main", Enabled: true}))
	require.NoError(t, profile.Default().Agents().Add(agent.Config{
		ID:      "agt_unsynced",
		Runtime: "codex",
		Source:  agent.NewAgentTeamSource("agentteam-main"),
	}))
	manager := profile.Default().ManagedSkillRepositories()
	require.NoError(t, manager.Upsert(profile.ManagedSkillRepositoryRecord{RepoID: "sk_cached", CloneURL: "https://agentteam.test/sk_cached.git", Branch: "main", LocalPath: t.TempDir()}))

	disp := newDaemonTestDispatcher("http://server.test")
	registrar, ok := disp.ServerConnections().(managedSkillRefreshChannelRegistrar)
	require.True(t, ok)
	registrar.Register(&managedSkillRefreshTestChannel{id: "agentteam-main", serverURL: "https://agentteam.test", workspaceID: "ws_1"})
	// No syncer cache has been populated for agt_unsynced, so reconcile must
	// preserve local records instead of treating an empty active set as proof
	// that every repository is orphaned.
	disp.SetSkillSyncer(&startupCacheMissSkillSyncer{})

	d := &Daemon{dispatcher: disp}
	d.reconcileManagedRepositories(context.Background())

	_, stillPresent := manager.Get("sk_cached")
	require.True(t, stillPresent, "incomplete skill cache must not delete local managed skill repositories")
}

type startupCacheMissSkillSyncer struct{}

func (*startupCacheMissSkillSyncer) SyncAgent(context.Context, string, string) ([]string, dispatcher.SkillLookupState, error) {
	return nil, dispatcher.SkillLookupUnknown, nil
}
func (*startupCacheMissSkillSyncer) CachedPaths(string) []string   { return nil }
func (*startupCacheMissSkillSyncer) CachedRepoIDs(string) []string { return nil }
func (*startupCacheMissSkillSyncer) CacheState(string) (dispatcher.SkillCacheState, bool) {
	return dispatcher.SkillCacheMiss, false
}
