package agent

import (
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// TestValidateReviewParamsPreservesRootPathByteExact pins the canonical-root
// byte-exactness fix at the agent-layer validator: RootPath MUST survive
// validation unchanged, including trailing whitespace. A regression that
// reintroduces strings.TrimSpace(params.RootPath) here would propagate the
// trimmed value through the entire create/update/identity chain, silently
// collapsing two distinct worktrees onto one review_context row.
func TestValidateReviewParamsPreservesRootPathByteExact(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		root string
	}{
		{"trailing space", "/repo "},
		{"trailing tab", "/repo\t"},
		{"trailing CRLF", "/repo\r\n"},
		{"leading space (still absolute on Unix)", "/ repo"},
		{"interior space", "/var/repos/agentteam workspace"},
		{"plain absolute", "/repo"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			out, err := ValidateReviewParams(UpsertReviewContextParams{
				WorkspaceID: "ws_1",
				MachineID:   "machine_1",
				RootPath:    tc.root,
			})
			require.NoError(t, err, "ValidateReviewParams must not reject a non-empty absolute path")
			require.Equal(t, tc.root, out.RootPath,
				"RootPath must be preserved byte-exact — no TrimSpace on the canonical root")
		})
	}
}

// TestValidateReviewParamsRejectsEmptyRoot verifies that ONLY the empty string
// counts as "missing" — a pure-whitespace root is NOT auto-trimmed to empty.
// (Reviewer3: "pure-whitespace non-empty doesn't fallback".) Without this, a
// RootPath of " " would pass validation here and only fail later when the
// git layer tries to resolve it, masking the original input.
func TestValidateReviewParamsRejectsEmptyRoot(t *testing.T) {
	t.Parallel()

	_, err := ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_1",
		MachineID:   "machine_1",
		RootPath:    "",
	})
	require.Error(t, err, "empty RootPath must be rejected")
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"empty RootPath must surface as a 400 BadRequest, got %v", err)

	// A pure-whitespace root is NOT auto-trimmed to empty — it falls through
	// to the absoluteness check, which rejects it (whitespace is not absolute).
	_, err = ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_1",
		MachineID:   "machine_1",
		RootPath:    "   ",
	})
	require.Error(t, err,
		"pure-whitespace RootPath must NOT be silently trimmed to empty; it must be rejected by the absoluteness check")
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"pure-whitespace RootPath must surface as a 400 BadRequest, got %v", err)
}

// TestValidateReviewParamsTrimsControlFields verifies the control/enum fields
// (refs, IDs, PR URL) ARE still trim-normalized. That discipline predates this
// PR and is unrelated to the canonical-root byte-exactness fix — but the test
// pins both directions so a future "trim everything" cleanup doesn't
// accidentally re-introduce the regression.
func TestValidateReviewParamsTrimsControlFields(t *testing.T) {
	t.Parallel()

	out, err := ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID:   "  ws_1  ",
		MachineID:     "  machine_1  ",
		RootPath:      "/repo ",
		BaseRef:       "  main  ",
		HeadRef:       "  feature  ",
		PRURL:         "  https://x/y  ",
		DisplayName:   "  custom  ",
		ThreadID:      "  thd_1  ",
		SourceAgentID: "  agt_1  ",
	})
	require.NoError(t, err)
	require.Equal(t, "ws_1", out.WorkspaceID)
	require.Equal(t, "machine_1", out.MachineID)
	require.Equal(t, "/repo ", out.RootPath, "RootPath is the ONLY field that stays raw")
	require.Equal(t, "main", out.BaseRef)
	require.Equal(t, "feature", out.HeadRef)
	require.Equal(t, "https://x/y", out.PRURL)
	require.Equal(t, "custom", out.DisplayName)
	require.Equal(t, "thd_1", out.ThreadID)
	require.Equal(t, "agt_1", out.SourceAgentID)

	out, err = ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID:   "ws_1",
		MachineID:     "machine_1",
		RootPath:      "/repo ",
		TaskScopeID:   " tsk_1 ",
		SourceAgentID: " agt_1 ",
	})
	require.NoError(t, err)
	require.Equal(t, "tsk_1", out.TaskScopeID)
	require.Equal(t, "agt_1", out.SourceAgentID)
}

// TestIsAbsoluteTargetPathAcceptsNonTrimmedRoots pins the absoluteness check
// at the unit level: it operates on the RAW path. A trailing-whitespace root
// MUST still be recognized as absolute (filepath.IsAbs reports true for
// "/repo "), so the validator surfaces it downstream rather than rejecting it
// here as "non-absolute" and hiding the original input.
func TestValidateReviewParamsTaskScopeRequiresAgent(t *testing.T) {
	t.Parallel()

	_, err := ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_1",
		MachineID:   "machine_1",
		RootPath:    "/repo/task",
		TaskScopeID: "tsk_1",
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	_, err = ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID:   "ws_1",
		MachineID:     "machine_1",
		RootPath:      "/repo/task",
		TaskScopeID:   "scope_1",
		SourceAgentID: "agt_1",
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
}

func TestIsAbsoluteTargetPathAcceptsNonTrimmedRoots(t *testing.T) {
	t.Parallel()

	require.True(t, isAbsoluteTargetPath("/repo "), "trailing space must not break absoluteness on Unix")
	require.True(t, isAbsoluteTargetPath("/repo"), "plain absolute path accepted")
	require.True(t, isAbsoluteTargetPath(`C:\repo`), "Windows drive-letter path accepted")
	require.True(t, isAbsoluteTargetPath(`C:/repo`), "Windows drive-letter with forward slash accepted")
	require.True(t, isAbsoluteTargetPath(`\\host\share`), "Windows UNC path accepted")

	require.False(t, isAbsoluteTargetPath(""), "empty path rejected")
	require.False(t, isAbsoluteTargetPath("relative/path"), "relative path rejected")
	require.False(t, isAbsoluteTargetPath("   "), "pure-whitespace path rejected")
}

// TestValidateReviewParamsRequiresWorkspaceID pins the replace-target
// contract: WorkspaceID is core to target_hash. An empty workspace after
// trim makes the identity hash ambiguous (two distinct missing-workspace
// calls would hash to the SAME key and silently dedup), so the validator
// MUST surface typed storeerr.BadRequest. CreateReviewContextForAgent and
// UpdateReviewContextForUser both fill WorkspaceID BEFORE calling validate
// (one from authenticatedAgent, the other carried forward from the existing
// row); reaching validate with an empty workspace is therefore a real bug,
// not an "omit means default" case.
func TestValidateReviewParamsRequiresWorkspaceID(t *testing.T) {
	t.Parallel()

	_, err := ValidateReviewParams(UpsertReviewContextParams{
		MachineID: "machine_1",
		RootPath:  "/repo/ws_required",
	})
	require.Error(t, err)
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"empty WorkspaceID MUST surface as storeerr.ErrBadRequest (HTTP 400); got %v", err)

	// Pure-whitespace workspace is trimmed to empty and hits the same check.
	_, err = ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "   ",
		MachineID:   "machine_1",
		RootPath:    "/repo/ws_required",
	})
	require.Error(t, err)
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"whitespace-only WorkspaceID MUST trim to empty and surface as BadRequest; got %v", err)
}

// TestValidateReviewParamsRequiresMachineID pins the parallel contract for
// MachineID. UpdateReviewContextForUser is REPLACE-TARGET: omitting MachineID
// is NOT "preserve existing machine", it is a 400 — the caller MUST re-supply
// the same value to keep the existing machine. Without this check, an omitted
// MachineID would hash to a target with an empty machine component and
// silently create a new ambiguous identity.
func TestValidateReviewParamsRequiresMachineID(t *testing.T) {
	t.Parallel()

	_, err := ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_1",
		RootPath:    "/repo/machine_required",
	})
	require.Error(t, err)
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"empty MachineID MUST surface as storeerr.ErrBadRequest (HTTP 400); got %v", err)

	// Pure-whitespace machine is trimmed to empty and hits the same check.
	_, err = ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_1",
		MachineID:   "   ",
		RootPath:    "/repo/machine_required",
	})
	require.Error(t, err)
	require.True(t, errors.Is(err, storeerr.ErrBadRequest),
		"whitespace-only MachineID MUST trim to empty and surface as BadRequest; got %v", err)
}

// TestDefaultReviewDisplayNamePreservesTrailingWhitespaceBasename pins the
// byte-exact basename contract for the auto-generated display name. Two
// worktrees whose canonical roots differ only by trailing whitespace
// ("/repo" vs "/repo ") are DISTINCT targets (see ValidateReviewParams
// RootPath contract); their auto-generated display names MUST also differ,
// otherwise the UI would show identical labels for distinct review contexts.
// The earlier revision trimmed the basename, collapsing the two — this test
// pins the fix.
//
// The default is also the CREATE-path default: when the caller omits
// DisplayName, validate fills it from rootPath via this function. So the
// byte-exact contract here covers BOTH the default-name derivation and the
// create-when-omitted path.
func TestDefaultReviewDisplayNamePreservesTrailingWhitespaceBasename(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		root string
		want string
	}{
		{"plain absolute", "/repo", "repo"},
		{"trailing space preserved", "/repo ", "repo "},
		{"trailing tab preserved", "/repo\t", "repo\t"},
		{"nested path basename only", "/var/repos/agentteam", "agentteam"},
		{"nested trailing space", "/var/repos/agentteam ", "agentteam "},
		{"root only fallback", "/", "/"},
		{"dot-only fallback", ".", "."},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			got := defaultReviewDisplayName(tc.root)
			require.Equal(t, tc.want, got,
				"defaultReviewDisplayName MUST preserve basename bytes including trailing whitespace")
		})
	}

	// End-to-end through ValidateReviewParams: an omitted DisplayName is
	// filled from defaultReviewDisplayName(rootPath), and the trailing-
	// whitespace basename survives the round trip.
	out, err := ValidateReviewParams(UpsertReviewContextParams{
		WorkspaceID: "ws_dn",
		MachineID:   "machine_dn",
		RootPath:    "/repo ",
	})
	require.NoError(t, err)
	require.Equal(t, "repo ", out.DisplayName,
		"omitted DisplayName MUST default to the byte-exact basename (no TrimSpace)")
}
