package dispatcher

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"

	"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 TestEnsureSessionWorkDirDurableSyncsNestedWorkflowChainLeafToRoot(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	agentDir := filepath.Join(configDir, "agents", "nested-session-agent")
	workDir := filepath.Join(agentDir, "sessions", "0123456789abcdef")

	var mu sync.Mutex
	var synced []string
	d := mustNewDispatcher("http://server.test")
	d.sessionWorkDirSync = func(dir string) error {
		mu.Lock()
		synced = append(synced, filepath.Clean(dir))
		mu.Unlock()
		return nil
	}

	require.NoError(t, d.ensureSessionWorkDirDurable(workDir))
	require.DirExists(t, workDir)
	mu.Lock()
	got := append([]string(nil), synced...)
	mu.Unlock()
	require.Equal(t, []string{
		filepath.Clean(workDir),
		filepath.Join(agentDir, "sessions"),
		filepath.Clean(agentDir),
		filepath.Join(configDir, "agents"),
		filepath.Clean(configDir),
	}, got)
}

func TestStartSessionDirectorySyncFailurePrecedesOwnershipAndSpawn(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	const agentID = "nested-session-fsync-failure"
	sessionKey := NewSessionKey(agentID, "internal", "maintenance").WithKind("ephemeral").String()
	workDir := sessionWorkDirForKey(agentID, sessionKey, true)
	failingParent := filepath.Join(configDir, "agents", agentID, "sessions")
	d := mustNewDispatcher("http://server.test")
	d.sessionWorkDirSync = func(dir string) error {
		if filepath.Clean(dir) == filepath.Clean(failingParent) {
			return errors.New("simulated parent directory fsync failure")
		}
		return nil
	}

	require.NoError(t, failpoint.Enable("dispatcher-new-agent-process", "return(true)"))
	var spawnCalls atomic.Int32
	SetNewAgentProcessHook(func(
		context.Context,
		drivercontract.Driver,
		drivercontract.LaunchConfig,
		agent.ProcessOptions,
	) (*agent.Process, error) {
		spawnCalls.Add(1)
		return nil, errors.New("spawn must not be reached")
	})
	t.Cleanup(func() {
		SetNewAgentProcessHook(nil)
		require.NoError(t, failpoint.Disable("dispatcher-new-agent-process"))
	})

	_, err := d.startSession(
		context.Background(),
		sessionKey,
		agent.Config{ID: agentID, Runtime: "codex"},
		runtimeconfig.Runtime{Type: runtimeconfig.TypeCodex, Binary: "codex"},
		conversationBinding{channelID: "agentteam-main", serverAgentID: agentID},
		types.InboxMessage{MessageID: "workflow-owner", Target: "#agentteam-main", Content: "durable hidden workflow"},
		startSessionOptions{ephemeral: true, durableDelivery: true, requireProcessHandoff: true},
	)
	require.ErrorContains(t, err, "simulated parent directory fsync failure")
	require.Zero(t, spawnCalls.Load(), "runtime Spawn must remain after the complete directory durability boundary")
	_, statErr := os.Stat(sessionResumeStatePath(workDir))
	require.ErrorIs(t, statErr, os.ErrNotExist, "ownership Put/ACK durability must not run after a parent fsync failure")
}

func TestEnsureSessionWorkDirDurableRepairsAlreadyExistingPath(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	workDir := filepath.Join(configDir, "agents", "existing-agent")
	require.NoError(t, os.MkdirAll(workDir, 0o755))

	var synced []string
	d := mustNewDispatcher("http://server.test")
	d.sessionWorkDirSync = func(dir string) error {
		synced = append(synced, filepath.Clean(dir))
		return nil
	}

	require.NoError(t, d.ensureSessionWorkDirDurable(workDir))
	require.Equal(t, []string{
		filepath.Clean(workDir),
		filepath.Join(configDir, "agents"),
		filepath.Clean(configDir),
	}, synced, "an existing path must re-establish the full boundary after a prior failed fsync attempt")
}
