package acp_test

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

func TestResolveSafePath_InsideWorktree(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()

	target := filepath.Join(tmp, "sub", "a.txt")
	require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
	require.NoError(t, os.WriteFile(target, []byte("x"), 0o644))

	got, err := acp.ResolveSafePath(tmp, target)
	require.NoError(t, err)
	// EvalSymlinks may resolve macOS / temp paths via /private prefix; on
	// Windows the path may be returned as-is. Compare via filepath.Clean.
	assert.Equal(t, filepath.Clean(target), got)

	got, err = acp.ResolveSafePath(tmp, "sub/a.txt")
	require.NoError(t, err)
	assert.Equal(t, filepath.Clean(target), got)
}

func TestResolveSafePath_AbsoluteOutside(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()

	// On Windows /etc/passwd is interpreted relative to the current drive;
	// use a guaranteed-outside path.
	outsideAbs := filepath.Join(filepath.VolumeName(tmp)+string(filepath.Separator), "etc", "passwd")

	_, err := acp.ResolveSafePath(tmp, outsideAbs)
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}

func TestResolveSafePath_RelativeWithDotDot(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()

	_, err := acp.ResolveSafePath(tmp, "../etc/passwd")
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}

func TestResolveSafePath_PrefixSiblingTrap(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()
	sibling := tmp + "_sibling"
	require.NoError(t, os.MkdirAll(sibling, 0o755))
	t.Cleanup(func() { _ = os.RemoveAll(sibling) })

	_, err := acp.ResolveSafePath(tmp, sibling+string(filepath.Separator)+"file")
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}

func TestResolveSafePath_SymlinkEscape(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("symlink test requires admin on Windows")
	}
	t.Parallel()
	tmp := t.TempDir()
	outside := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o644))

	link := filepath.Join(tmp, "link-to-secret")
	require.NoError(t, os.Symlink(filepath.Join(outside, "secret"), link))

	_, err := acp.ResolveSafePath(tmp, link)
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}

func TestResolveSafePath_NonexistentInsideOK(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()
	want := filepath.Join(tmp, "newfile.txt")
	got, err := acp.ResolveSafePath(tmp, want)
	require.NoError(t, err)
	assert.Equal(t, want, got)

	// 多级不存在的中间目录（祖先均为 cwd 内真实目录）同样合法，不能误杀
	want = filepath.Join(tmp, "newdir", "sub", "newfile.txt")
	got, err = acp.ResolveSafePath(tmp, want)
	require.NoError(t, err)
	assert.Equal(t, want, got)
}

// 父目录是指向外部的 symlink 且叶子文件不存在 → 必须被拒，
// 否则 fs/write_text_file 会跟随 symlink 把新建文件写出 worktree。
func TestResolveSafePath_SymlinkDirEscapeNonexistentLeaf(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()
	outside := t.TempDir()

	link := filepath.Join(tmp, "link-dir")
	if err := os.Symlink(outside, link); err != nil {
		t.Skipf("symlink not supported on this platform: %v", err)
	}

	// 叶子不存在
	_, err := acp.ResolveSafePath(tmp, filepath.Join(link, "newfile.txt"))
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)

	// 叶子与中间目录都不存在（symlink 是更远的祖先）同样被拒
	_, err = acp.ResolveSafePath(tmp, filepath.Join(link, "sub", "newfile.txt"))
	ae, ok = errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}

func TestResolveSafePath_RelativeCwdRejected(t *testing.T) {
	t.Parallel()
	_, err := acp.ResolveSafePath("relative-cwd", "x")
	ae, ok := errs.As(err)
	require.True(t, ok)
	assert.Equal(t, errs.CodeAcpFsPathOutsideWorktree, ae.Code)
}
