package workspace

import (
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

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

// TestReviewFingerprintHandlesRepoWithWhitespacePaths pins the raw-path
// discipline end-to-end: a real git repo whose filenames contain leading
// or trailing whitespace must round-trip through ReviewFingerprint and
// ReviewFiles without truncation, normalization, or panic. The fingerprint
// and file list output must include the special paths verbatim — they are
// legitimate git paths and any TrimSpace/Clean in the chain would silently
// rewrite them to different filenames.
//
// Embedded newlines in filenames are a separate case: go-git's underlying
// merkletrie validator rejects paths containing control characters, so
// those do not round-trip through ReviewDiff even though git itself stores
// them. That constraint lives in the third-party library, not our code; we
// document it here so future readers know whitespace-only is the contract.
func TestReviewFingerprintHandlesRepoWithWhitespacePaths(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	// Isolate the working-state cache so this test's repo root does not
	// collide with other tests' entries.
	origWorkingCache := defaultWorkingStateCache
	t.Cleanup(func() { defaultWorkingStateCache = origWorkingCache })
	defaultWorkingStateCache = newWorkingStateCache(defaultWorkingStateCacheCapacity, defaultWorkingStateCacheTTL, func() time.Time { return time.Now().UTC() })

	repoDir := t.TempDir()
	setupReviewTestRepo(t, repoDir)

	// Create a feature branch with the whitespace-named files. The branch
	// diff (main vs feature) will then include them, exercising the
	// raw-path chain through ReviewFingerprint's branch signature path and
	// ReviewFiles' branch scope.
	runGit(t, repoDir, "checkout", "-b", "feature")

	// Files with whitespace-only differences from a "normal" name. Each pair
	// is designed so TrimSpace would conflate the two names; the test asserts
	// both survive as distinct paths in the fingerprint pipeline.
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, " leading.txt"), []byte("leading space\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "leading.txt"), []byte("no leading space\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "trailing.txt "), []byte("trailing space\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "trailing.txt"), []byte("no trailing space\n"), 0o644))
	runGit(t, repoDir, "add", "-A")
	runGit(t, repoDir, "commit", "-m", "add whitespace-named files")
	runGit(t, repoDir, "checkout", "main")

	// First call computes the fingerprint; second call should be stable.
	fp1, err := ReviewFingerprint(repoDir, "main", "feature", "common_base", "")
	require.NoError(t, err, "ReviewFingerprint must succeed on a repo with whitespace paths")
	require.NotEmpty(t, fp1.RepositoryToken, "RepositoryToken must be populated")
	require.NotEmpty(t, fp1.BranchToken, "BranchToken must be populated")
	require.Equal(t, 4, fp1.BranchFileCount, "exactly four whitespace-named files should differ between main and feature")

	fp2, err := ReviewFingerprint(repoDir, "main", "feature", "common_base", "")
	require.NoError(t, err)
	// Tokens and counts must be byte-equal across calls. ObservedAt and
	// NextRefreshAt are time-derived and may drift by sub-milliseconds
	// between calls — compare only the stable fields.
	require.Equal(t, fp1.RepositoryToken, fp2.RepositoryToken, "RepositoryToken must be stable")
	require.Equal(t, fp1.BranchToken, fp2.BranchToken, "BranchToken must be stable")
	require.Equal(t, fp1.WorkingToken, fp2.WorkingToken, "WorkingToken must be stable")
	require.Equal(t, fp1.WorkingStrength, fp2.WorkingStrength, "WorkingStrength must be stable")
	require.Equal(t, fp1.BranchFileCount, fp2.BranchFileCount, "BranchFileCount must be stable")

	// The branch file list must include both members of each whitespace
	// pair verbatim. TrimSpace in the chain would collapse each pair to a
	// single entry and silently lose a real file.
	files, err := ReviewFiles(repoDir, "branch", "main", "feature", "common_base", "")
	require.NoError(t, err)

	paths := make(map[string]bool, len(files))
	for _, f := range files {
		paths[f.Path] = true
	}
	for _, want := range []string{" leading.txt", "leading.txt", "trailing.txt ", "trailing.txt"} {
		require.Truef(t, paths[want],
			"expected branch file path %q to survive the pipeline verbatim; TrimSpace would have collapsed it. got paths: %v",
			want, paths)
	}
}

// TestReviewDiffHandlesLeadingSpacePath confirms ReviewDiff returns the diff
// for a file whose path contains a leading space without trimming it. This
// is the most pointed demonstration of why the chain cannot apply
// TrimSpace/Clean on path values: doing so would silently point the diff at
// a different file.
func TestReviewDiffHandlesLeadingSpacePath(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	repoDir := t.TempDir()
	setupReviewTestRepo(t, repoDir)

	require.NoError(t, os.WriteFile(filepath.Join(repoDir, " leading.txt"), []byte("with space\n"), 0o644))
	runGit(t, repoDir, "add", "-A")
	runGit(t, repoDir, "commit", "-m", "add leading-space file")

	// The current HEAD has the file; HEAD~1 (the initial commit from
	// setupReviewTestRepo) does not. ReviewDiff on the branch scope must
	// return the file's diff with the path intact.
	diff, err := ReviewDiff(repoDir, "branch", " leading.txt", "HEAD~1", "HEAD", "direct", "")
	require.NoError(t, err, "ReviewDiff must succeed for a path containing a leading space")
	require.NotEmpty(t, diff.Sections, "diff must have at least one section")
}
