package workspace

import (
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

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

// These integration tests pin the workspace-layer half of the
// control-char wire-survival contract reviewer3 + codex6 required:
//
// Every Review entry function (Summary/Snapshot/Fingerprint/Files/Diff)
// that touches a go-git tree, status, or patch boundary MUST target the
// stable ErrReviewControlCharPath sentinel when — and ONLY when — the
// underlying go-git validator rejects a path for containing control
// characters. The original go-git error carries the offending path
// verbatim (e.g. `invalid path "line\nbreak.txt": contains control
// character`); each call site MUST route that through wrapReviewTreeError
// so callers only ever see the path-free sentinel.
//
// Two real-fixture flavors are exercised because the leak returns come
// from different go-git code paths:
//
//  1. Branch committed — a newline-named file COMMITTED to the repo.
//     Triggers object.DiffTreeWithOptions → pathutil.ValidTreePath
//     rejection (and change.Patch() during line-stats). Covers the
//     Summary/Snapshot/Fingerprint/branch-Files/branch-Diff chain.
//
//  2. Working uncommitted — a newline-named file in the worktree but NOT
//     committed. Triggers wt.StatusWithOptions → worktree_fs.validPath
//     rejection. Covers the working-Files / working-Diff chain.
//
// For each endpoint × fixture the test asserts:
//
//   - errors.Is(err, ErrReviewControlCharPath) — the typed sentinel
//     survives, so the wire/HTTP layers above can map to a stable outcome.
//   - err.Error() carries NONE of the offending path bytes — the leak
//     protection holds at the workspace boundary, before any wire/HTTP
//     serialization could carry it further.
//
// Skipped on Windows: newline-in-filename fixtures are unreliable there.
// The workspace-layer contract applies on every platform; the assertion
// is that the wrap fires, not that any specific platform accepts the
// fixture.
func TestReviewEntriesWireControlCharSentinel_BranchCommitted(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("newline-in-filename fixtures are unreliable on Windows")
	}
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	const (
		offendingFile = "line\nbreak.txt"
		leakMarker    = "line\nbreak"
	)

	repoDir := t.TempDir()
	setupReviewTestRepo(t, repoDir)
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, offendingFile), []byte("content\n"), 0o644))
	runGit(t, repoDir, "add", offendingFile)
	runGit(t, repoDir, "commit", "-m", "add control-char path")

	// Each entry function whose go-git call graph touches DiffTree or
	// change.Patch() for the branch side. The expectSentinel closure keeps
	// the leak assertion identical across endpoints so a regression in any
	// single handler stands out.
	expectSentinel := func(t *testing.T, err error) {
		t.Helper()
		require.Error(t, err, "endpoint MUST fail when the branch tree contains a control-char path")
		require.True(t, errors.Is(err, ErrReviewControlCharPath),
			"endpoint MUST return ErrReviewControlCharPath sentinel; got %v", err)
		require.False(t, strings.Contains(err.Error(), leakMarker),
			"endpoint leaked offending path bytes through sentinel Error(): %q", err.Error())
	}

	t.Run("Summary", func(t *testing.T) {
		_, err := ReviewSummary(repoDir, "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("Snapshot", func(t *testing.T) {
		_, err := ReviewSnapshot(repoDir, "branch", "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("Snapshot_working_scope", func(t *testing.T) {
		// Snapshot with working scope still computes branch changes first;
		// the branch DiffTree rejection fires before working scan.
		_, err := ReviewSnapshot(repoDir, "working", "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("Fingerprint", func(t *testing.T) {
		_, err := ReviewFingerprint(repoDir, "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("Files_branch", func(t *testing.T) {
		_, err := ReviewFiles(repoDir, "branch", "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("Diff_branch", func(t *testing.T) {
		_, err := ReviewDiff(repoDir, "branch", offendingFile, "", "", "", "")
		expectSentinel(t, err)
	})

	t.Run("DiffBatch_branch", func(t *testing.T) {
		results, err := ReviewDiffBatch(repoDir, "branch", []string{"tracked.txt", offendingFile}, "", "", "", "")
		require.Nil(t, results)
		expectSentinel(t, err)
	})
}

func TestReviewEntriesWireControlCharSentinel_WorkingUncommitted(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("newline-in-filename fixtures are unreliable on Windows")
	}
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	const (
		offendingFile = "line\nbreak.txt"
		leakMarker    = "line\nbreak"
	)

	repoDir := t.TempDir()
	setupReviewTestRepo(t, repoDir)
	// Place the file in the worktree AND `git add` it to the index, but do
	// NOT commit. This way the branch tree is clean (DiffTree won't trip),
	// but wt.StatusWithOptions scans the index/worktree and trips on
	// worktree_fs.validPath.
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, offendingFile), []byte("content\n"), 0o644))
	runGit(t, repoDir, "add", offendingFile)

	expectSentinel := func(t *testing.T, err error) {
		t.Helper()
		require.Error(t, err, "endpoint MUST fail when the worktree contains a control-char path")
		require.True(t, errors.Is(err, ErrReviewControlCharPath),
			"endpoint MUST return ErrReviewControlCharPath sentinel; got %v", err)
		require.False(t, strings.Contains(err.Error(), leakMarker),
			"endpoint leaked offending path bytes through sentinel Error(): %q", err.Error())
	}

	// Snapshot/Files both invoke reviewWorkingFiles → StatusWithOptions.
	t.Run("Snapshot", func(t *testing.T) {
		_, err := ReviewSnapshot(repoDir, "working", "", "", "", "")
		expectSentinel(t, err)
	})
	t.Run("Snapshot_branch_scope", func(t *testing.T) {
		// Snapshot computes working files regardless of scope (to feed the
		// working-tokens half of the aggregate token), so the working
		// scanner trips even when the user asked for branch scope.
		_, err := ReviewSnapshot(repoDir, "branch", "", "", "", "")
		expectSentinel(t, err)
	})
	t.Run("Files_working", func(t *testing.T) {
		_, err := ReviewFiles(repoDir, "working", "", "", "", "")
		expectSentinel(t, err)
	})
	t.Run("DiffBatch_working", func(t *testing.T) {
		results, err := ReviewDiffBatch(repoDir, "working", []string{"tracked.txt", offendingFile}, "", "", "", "")
		require.Nil(t, results)
		expectSentinel(t, err)
	})

	// Fingerprint invokes reviewWorkingFiles as part of its aggregate token
	// computation, so it trips on the same scan.
	t.Run("Fingerprint", func(t *testing.T) {
		_, err := ReviewFingerprint(repoDir, "", "", "", "")
		expectSentinel(t, err)
	})
}

func TestReviewEntriesWireControlCharSentinel_WorkingRenameOldPath(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("newline-in-filename fixtures are unreliable on Windows")
	}
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	const (
		offendingFile = "line\nbreak.txt"
		leakMarker    = "line\nbreak"
	)
	repoDir := t.TempDir()
	setupReviewTestRepo(t, repoDir)
	require.NoError(t, os.Rename(
		filepath.Join(repoDir, "tracked.txt"),
		filepath.Join(repoDir, offendingFile),
	))
	runGit(t, repoDir, "add", "-A")
	runGit(t, repoDir, "commit", "-m", "track control-char source path")
	runGit(t, repoDir, "mv", offendingFile, "renamed.txt")

	_, err := ReviewFiles(repoDir, "working", "", "", "", "")
	require.Error(t, err)
	require.ErrorIs(t, err, ErrReviewControlCharPath)
	require.NotContains(t, err.Error(), leakMarker)
}
