package workspace

import (
	"errors"
	"strings"
	"testing"
)

// TestWrapReviewTreeErrorConvertsControlCharToSentinel pins the controlled-
// error contract for go-git's DiffTree rejection of control-character paths.
// The original go-git error carries the offending path verbatim (e.g.
// `invalid path "line\nbreak.txt": contains control character`); the wrapper
// MUST discard that and return a fixed sentinel so neither Error() text nor
// downstream logs can leak the filesystem path. Callers identify the category
// via errors.Is(err, ErrReviewControlCharPath).
func TestWrapReviewTreeErrorConvertsControlCharToSentinel(t *testing.T) {
	t.Parallel()

	original := errors.New(`invalid path "line\nbreak.txt": contains control character`)
	wrapped := wrapReviewTreeError(original)

	if !errors.Is(wrapped, ErrReviewControlCharPath) {
		t.Fatalf("wrapReviewTreeError(control-char err) = %v, want errors.Is(_, ErrReviewControlCharPath)", wrapped)
	}
	if wrapped.Error() != ErrReviewControlCharPath.Error() {
		t.Fatalf("wrapReviewTreeError changed the error text: got %q, want sentinel text %q", wrapped.Error(), ErrReviewControlCharPath.Error())
	}
	// The offending path MUST NOT appear anywhere in the wrapped error text.
	if strings.Contains(wrapped.Error(), "line") || strings.Contains(wrapped.Error(), "break") {
		t.Fatalf("wrapReviewTreeError leaked the path in error text: %q", wrapped.Error())
	}
}

// TestWrapReviewTreeErrorPassesThroughNonControlCharErrors verifies that
// unrelated DiffTree / scan errors are NOT converted to the sentinel. Only
// the control-character category is mapped; everything else surfaces
// unchanged so legitimate failures keep their full diagnostic text.
func TestWrapReviewTreeErrorPassesThroughNonControlCharErrors(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		err  error
	}{
		{"generic error", errors.New("object not found")},
		{"nil", nil},
		{"wrap of plain error", errors.New("DiffTree: some other failure")},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := wrapReviewTreeError(tc.err)
			if tc.err == nil {
				if got != nil {
					t.Fatalf("wrapReviewTreeError(nil) = %v, want nil", got)
				}
				return
			}
			if errors.Is(got, ErrReviewControlCharPath) {
				t.Fatalf("wrapReviewTreeError(%v) was incorrectly converted to sentinel", tc.err)
			}
			if got.Error() != tc.err.Error() {
				t.Fatalf("wrapReviewTreeError(%v) changed text: got %q", tc.err, got.Error())
			}
		})
	}
}

// TestErrReviewControlCharPathIsStableSentinel verifies the sentinel is a
// package-level variable that callers can compare against with errors.Is —
// it must not be allocated per-call or the identity check would fail across
// different wrapReviewTreeError invocations.
func TestErrReviewControlCharPathIsStableSentinel(t *testing.T) {
	t.Parallel()

	// Inputs MUST match both anchored substrings (`invalid path` + `: contains
	// control character`) — the matcher is intentionally narrow to avoid
	// misclassifying unrelated errors that happen to mention control chars.
	first := wrapReviewTreeError(errors.New(`invalid path "line\nbreak.txt": contains control character`))
	second := wrapReviewTreeError(errors.New(`invalid path "foo\rbar": contains control character`))
	if !errors.Is(first, ErrReviewControlCharPath) || !errors.Is(second, ErrReviewControlCharPath) {
		t.Fatalf("sentinel must match across independent wrapReviewTreeError calls")
	}
}
