package agent

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

// recordingValidator is a stub ReviewContextValidator that captures the
// exact arguments its methods were called with. Used by the byte-exactness
// regression tests below to prove that the resolved RootPath flows from
// validateReviewContext through the validator target byte-exact — no
// TrimSpace anywhere in the chain.
type recordingValidator struct {
	validateTargetPath string
	validateKind       string
	validateErr        error

	summaryTargetPath string
	summaryBaseRef    string
	summaryHeadRef    string
	summaryErr        error
	summaryResult     agent.AgentReviewSummary
}

func (r *recordingValidator) ValidateAgentOutputTarget(_ context.Context, _ string, _ string, kind string, targetPath string) error {
	r.validateTargetPath = targetPath
	r.validateKind = kind
	return r.validateErr
}

func (r *recordingValidator) GetAgentReviewSummary(_ context.Context, _ string, _ string, targetPath string, baseRef string, headRef string, _ string) (agent.AgentReviewSummary, error) {
	r.summaryTargetPath = targetPath
	r.summaryBaseRef = baseRef
	r.summaryHeadRef = headRef
	return r.summaryResult, r.summaryErr
}

// TestValidateReviewContextRoutesRootPathByteExactThroughReviewSummary pins
// the validateReviewContext fix: validation MUST call GetAgentReviewSummary
// (NOT ValidateAgentOutputTarget) with the RAW RootPath. Reviewer3 asked for
// this routing because ValidateAgentOutputTarget's desktop-side validator
// trims the resolved rootPath before Stat — that trim would rewrite
// "/repo " to "/repo" and silently remap a trailing-whitespace worktree to
// its non-whitespace twin, breaking the cache-key isolation the Review
// fingerprint depends on.
//
// The test feeds "/repo " (with trailing space) into validateReviewContext
// and asserts: (a) the recordingValidator received "/repo " byte-exact,
// and (b) ValidateAgentOutputTarget was NEVER called (the field stays at
// its zero value "").
func TestValidateReviewContextRoutesRootPathByteExactThroughReviewSummary(t *testing.T) {
	t.Parallel()

	const rawRoot = "/repo "
	rec := &recordingValidator{
		summaryResult: agent.AgentReviewSummary{Status: "ready"},
	}
	h := &ReviewContextHandler{validator: rec}

	aa := agent.AuthenticatedAgent{
		WorkspaceID: "wsp_1",
		AgentID:     "agt_00000000000001",
		MachineID:   "mac_1",
		MemberID:    "000000000001",
	}
	params := agent.UpsertReviewContextParams{
		WorkspaceID: "wsp_1",
		MachineID:   "mac_1",
		RootPath:    rawRoot,
		BaseRef:     "main",
		HeadRef:     "feature",
	}

	require.NoError(t, h.validateReviewContext(context.Background(), aa, params),
		"validateReviewContext must succeed when the validator returns a ready summary")

	// (a) GetAgentReviewSummary received the RAW RootPath byte-exact.
	require.Equal(t, rawRoot, rec.summaryTargetPath,
		"RootPath MUST be forwarded to GetAgentReviewSummary byte-exact — got %q, want %q",
		rec.summaryTargetPath, rawRoot)
	require.True(t, strings.HasSuffix(rec.summaryTargetPath, " "),
		"trailing space MUST survive — a TrimSpace here would silently remap '/repo ' to '/repo'")

	// (b) ValidateAgentOutputTarget was NEVER called. Its recording fields
	// stay at zero, proving validateReviewContext routed through the
	// Review Summary RPC instead.
	require.Empty(t, rec.validateTargetPath,
		"ValidateAgentOutputTarget MUST NOT be called — its desktop-side validator trims the resolved rootPath")
	require.Empty(t, rec.validateKind,
		"ValidateAgentOutputTarget MUST NOT be called")
}

// TestValidateReviewContextPreservesControlCharacterRoots verifies the
// byte-exact contract extends to control-character RootPaths too. The
// transport layer (JSON wire) preserves control chars byte-exact (pinned
// by TestReviewWirePayloadPreservesControlCharTargetPathByteExact); the
// validation layer must not silently strip them at the agent-runtime
// boundary, otherwise the workspace would receive a different path than
// the one the user submitted.
func TestValidateReviewContextPreservesControlCharacterRoots(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		root string
	}{
		{"newline in middle", "/var/repo\nname"},
		{"trailing tab", "/repo\t"},
		{"NUL byte", "/repo\x00"},
		{"interior space", "/var/repos/agentteam workspace"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			rec := &recordingValidator{
				summaryResult: agent.AgentReviewSummary{Status: "ready"},
			}
			h := &ReviewContextHandler{validator: rec}

			params := agent.UpsertReviewContextParams{
				WorkspaceID: "wsp_1",
				MachineID:   "mac_1",
				RootPath:    tc.root,
			}
			require.NoError(t, h.validateReviewContext(context.Background(),
				agent.AuthenticatedAgent{WorkspaceID: "wsp_1", AgentID: "agt_00000000000001", MachineID: "mac_1", MemberID: "000000000001"},
				params))
			require.Equal(t, tc.root, rec.summaryTargetPath,
				"control-char RootPath MUST survive byte-exact through validateReviewContext")
		})
	}
}

// TestValidateReviewContextReturnsMachineOfflineAsIs verifies the offline
// sentinel is propagated verbatim — validateReviewContext MUST NOT wrap it
// as a 400 BadRequest, because "machine offline" is a 503 (retry later),
// not a client input problem.
func TestValidateReviewContextReturnsMachineOfflineAsIs(t *testing.T) {
	t.Parallel()

	rec := &recordingValidator{
		summaryErr: runtimecontrol.ErrMachineOffline,
	}
	h := &ReviewContextHandler{validator: rec}

	err := h.validateReviewContext(context.Background(),
		agent.AuthenticatedAgent{WorkspaceID: "wsp_1", AgentID: "agt_00000000000001", MachineID: "mac_1", MemberID: "000000000001"},
		agent.UpsertReviewContextParams{WorkspaceID: "wsp_1", MachineID: "mac_1", RootPath: "/repo"})
	require.ErrorIs(t, err, runtimecontrol.ErrMachineOffline,
		"ErrMachineOffline MUST propagate as-is so the caller can target a 503 retry signal — "+
			"not get wrapped as a 400 BadRequest")
}

// TestValidateReviewContextTreatsNonReadyStatusAsBadRequest verifies the
// status→error mapping: a summary that returns "not_git" (or any non-ready
// status) MUST target as a 400 BadRequest so the create handler rejects
// the context rather than persisting an invalid review target.
func TestValidateReviewContextTreatsNonReadyStatusAsBadRequest(t *testing.T) {
	t.Parallel()

	rec := &recordingValidator{
		summaryResult: agent.AgentReviewSummary{
			Status:  "not_git",
			Message: "This workspace is not in a git repository yet.",
		},
	}
	h := &ReviewContextHandler{validator: rec}

	err := h.validateReviewContext(context.Background(),
		agent.AuthenticatedAgent{WorkspaceID: "wsp_1", AgentID: "agt_00000000000001", MachineID: "mac_1", MemberID: "000000000001"},
		agent.UpsertReviewContextParams{WorkspaceID: "wsp_1", MachineID: "mac_1", RootPath: "/repo"})
	require.Error(t, err, "non-ready summary MUST target as a validation error")
	require.False(t, errors.Is(err, runtimecontrol.ErrMachineOffline),
		"non-ready summary is NOT a machine-offline condition")
}
