package dispatcher

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

	"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/wire/manager"
)

func TestRuntimeProcessRegistryTracksProcessLifecycle(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	workDir := filepath.Join(configDir, "agents", "agent-runtime")

	d := mustNewDispatcher("http://server.test")
	proc, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{
			AgentID: "agent-runtime",
			WorkDir: workDir,
		},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		proc.Stop()
		require.True(t, proc.WaitForExit(time.Second))
	})

	sess := &interactiveSession{
		agentID:   "agent-runtime",
		runtime:   runtimeconfig.Runtime{Type: runtimeconfig.TypeOpenCode},
		workDir:   workDir,
		startedAt: time.Now(),
		process:   proc,
	}
	d.registerRuntimeProcess(sess, proc)

	entries := d.runtimeProcessRegistry.Entries()
	require.Len(t, entries, 1)
	require.Equal(t, proc.PID(), entries[0].PID)
	require.Equal(t, "agent-runtime", entries[0].AgentID)
	require.Equal(t, "opencode", entries[0].Runtime)
	require.Equal(t, workDir, entries[0].WorkDir)

	d.unregisterRuntimeProcess(proc)
	require.Empty(t, d.runtimeProcessRegistry.Entries())
}

func TestActiveRuntimeWorkDirsIncludesOnlyLiveProcesses(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	activeWorkDir := filepath.Join(configDir, "agents", "active-agent")
	stoppedWorkDir := filepath.Join(configDir, "agents", "stopped-agent")

	d := mustNewDispatcher("http://server.test")
	proc, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{
			AgentID: "active-agent",
			WorkDir: activeWorkDir,
		},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		proc.Stop()
		require.True(t, proc.WaitForExit(time.Second))
	})

	d.sessionManager.Set("active-agent", &interactiveSession{
		agentID: "active-agent",
		workDir: activeWorkDir,
		process: proc,
	})
	d.sessionManager.Set("stopped-agent", &interactiveSession{
		agentID: "stopped-agent",
		workDir: stoppedWorkDir,
		process: nil,
	})

	active := d.activeRuntimeWorkDirs()
	require.Contains(t, active, activeWorkDir)
	require.NotContains(t, active, stoppedWorkDir)
}

func TestRuntimeProcessRegistryUsesSessionRuntimeWhenAvailable(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	workDir := filepath.Join(configDir, "agents", "agent-claude")

	d := mustNewDispatcher("http://server.test")
	proc, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{
			AgentID: "agent-claude",
			WorkDir: workDir,
		},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		proc.Stop()
		require.True(t, proc.WaitForExit(time.Second))
	})

	d.registerRuntimeProcess(&interactiveSession{
		agentID:   "agent-claude",
		runtime:   runtimeconfig.Runtime{Type: runtimeconfig.TypeClaudeCode},
		workDir:   workDir,
		startedAt: time.Now(),
		process:   proc,
	}, proc)

	entries := d.runtimeProcessRegistry.Entries()
	require.Len(t, entries, 1)
	require.Equal(t, "claude", entries[0].Runtime)
}

func TestRuntimeProcessRegistryFallsBackToDriverName(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	workDir := filepath.Join(configDir, "agents", "agent-driver-fallback")

	d := mustNewDispatcher("http://server.test")
	proc, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{
			AgentID: "agent-driver-fallback",
			WorkDir: workDir,
		},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		proc.Stop()
		require.True(t, proc.WaitForExit(time.Second))
	})

	d.registerRuntimeProcess(&interactiveSession{
		agentID:   "agent-driver-fallback",
		workDir:   workDir,
		startedAt: time.Now(),
		process:   proc,
	}, proc)

	entries := d.runtimeProcessRegistry.Entries()
	require.Len(t, entries, 1)
	require.Equal(t, "stub", entries[0].Runtime)
}

func TestStartLaunchesRuntimeJanitorOnlyOnce(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	d := mustNewDispatcher("http://server.test")
	mgr := manager.New(nil, nil)
	d.serverConnections = mgr

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	require.NoError(t, d.Start(ctx))
	require.NoError(t, d.Start(ctx))

	// The integration assertion is intentionally on observable state rather than
	// goroutine counts: Start must wire a janitor with the dispatcher active-workdir
	// callback, and sync.Once must keep repeated Start calls from replacing it.
	janitor := d.runtimeProcessJanitor
	require.NotNil(t, janitor)
	require.Same(t, janitor, d.runtimeProcessJanitor)
}
