package wire

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// TestRemoteReviewFingerprintEndpointReturnsAuthoritativeBranchTokenAndHintWorkingToken
// is the Desktop transport-adapter end-to-end regression reviewer3 required:
// the handleRemoteReviewFingerprint adapter MUST, when invoked through the
// remote-endpoint assignment path against a real git repo, surface a
// fingerprint whose BranchToken is full 64-hex SHA-256 (authoritative) and
// whose WorkingToken is 16-hex (hint). The ObservedAt / NextRefreshAt /
// BranchFileCount / StagedFileCount / UnstagedFileCount fields must also be
// populated — the frontend uses them to decide whether to re-fetch.
//
// The adapter sits at the desktop-side end of the elastic request link
// pipeline; this test exercises it via handleRemoteRequestAssignment, which
// is the same entrypoint used by production traffic. A regression that
// truncated the BranchToken, dropped a count field, or swapped the
// strengths would surface here.
func TestRemoteReviewFingerprintEndpointReturnsAuthoritativeBranchTokenAndHintWorkingToken(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}
	repoDir := t.TempDir()
	setupRemoteReviewTestRepo(t, repoDir)
	// Stage a working change so the working token is non-empty and the
	// unstaged file count is non-zero.
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "tracked.txt"), []byte("changed\n"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_fingerprint",
		RemoteRequestID:      "rrq_fingerprint",
		EndpointID:           remoteEndpointReviewFingerprint,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      5000,
		CancelToken:          "cancel_fingerprint",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewSummaryPayload{AgentID: "agent-review", TargetPath: repoDir}),
	}))

	result := nextRemoteRequestResult(t, ch)
	require.True(t, result.Success, "fingerprint endpoint must succeed against a real repo, got outcome=%q error=%q",
		result.Outcome, result.Error)
	require.Equal(t, "ok", result.Outcome)

	var response remoteReviewFingerprintResponse
	require.NoError(t, json.Unmarshal(result.Payload, &response))
	require.Equal(t, "agent-review", response.AgentID)
	require.NotNil(t, response.Fingerprint, "fingerprint must be populated on success")

	fp := *response.Fingerprint

	// Authoritative tokens: full SHA-256 hex (64 chars).
	require.Len(t, fp.RepositoryToken, sha256.Size*2,
		"RepositoryToken MUST be full SHA-256 hex (authoritative strength), got len=%d", len(fp.RepositoryToken))
	_, err := hex.DecodeString(fp.RepositoryToken)
	require.NoError(t, err, "RepositoryToken MUST be valid lowercase hex")
	require.Len(t, fp.BranchToken, sha256.Size*2,
		"BranchToken MUST be full SHA-256 hex (authoritative strength), got len=%d", len(fp.BranchToken))
	_, err = hex.DecodeString(fp.BranchToken)
	require.NoError(t, err, "BranchToken MUST be valid lowercase hex")

	// Hint token: 16 hex (truncated to reviewFingerprintTokenHexLen).
	require.Len(t, fp.WorkingToken, 16,
		"WorkingToken MUST be 16-hex (hint strength), got len=%d", len(fp.WorkingToken))
	_, err = hex.DecodeString(fp.WorkingToken)
	require.NoError(t, err, "WorkingToken MUST be valid lowercase hex")

	// Strength label: working is hint, never authoritative.
	require.Equal(t, "hint", fp.WorkingStrength,
		"WorkingStrength MUST be 'hint' — the aggregate working token is truncated so it cannot be authoritative")

	// Counts: the staged/unstaged counts must reflect the working change.
	require.NotZero(t, fp.UnstagedFileCount,
		"UnstagedFileCount must be non-zero — tracked.txt has unstaged bytes in this fixture")
	require.False(t, fp.ObservedAt.IsZero(), "ObservedAt must be populated")
	require.False(t, fp.NextRefreshAt.IsZero(), "NextRefreshAt must be populated")
	require.True(t, fp.NextRefreshAt.After(fp.ObservedAt),
		"NextRefreshAt MUST be strictly after ObservedAt so the frontend can pace its polls")
}

// TestRemoteReviewFingerprintEndpointPreservesTrailingSpaceTargetPath is the
// Desktop transport-adapter half of the trailing-whitespace isolation
// contract: a TargetPath whose canonical root ends in a trailing space MUST
// flow from the remote-endpoint wire payload through reviewBasePath into
// workspace.ReviewFingerprint byte-exact. A regression that reintroduces
// strings.TrimSpace(reviewBasePath) would rewrite "/repo " to "/repo",
// silently pointing the fingerprint at a different worktree.
//
// The test sets up a real git repo at a trailing-space path on disk and
// invokes the fingerprint adapter with that path. If reviewBasePath trims,
// the workspace layer cannot find the repo and the adapter returns
// "endpoint_error"; if it does not trim, the adapter succeeds.
func TestRemoteReviewFingerprintEndpointPreservesTrailingSpaceTargetPath(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	parent := t.TempDir()
	repoDir := filepath.Join(parent, "repo ")
	require.NotEqual(t, filepath.Join(parent, "repo"), repoDir)
	require.NoError(t, os.MkdirAll(repoDir, 0o755))
	setupRemoteReviewTestRepo(t, repoDir)

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_fingerprint_ws",
		RemoteRequestID:      "rrq_fingerprint_ws",
		EndpointID:           remoteEndpointReviewFingerprint,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      5000,
		CancelToken:          "cancel_fingerprint_ws",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewSummaryPayload{AgentID: "agent-review", TargetPath: repoDir}),
	}))

	result := nextRemoteRequestResult(t, ch)
	require.True(t, result.Success,
		"fingerprint endpoint MUST succeed against the trailing-space repo — an error means reviewBasePath trimmed it. "+
			"got outcome=%q error=%q", result.Outcome, result.Error)
	require.Equal(t, "ok", result.Outcome)

	var response remoteReviewFingerprintResponse
	require.NoError(t, json.Unmarshal(result.Payload, &response))
	require.NotNil(t, response.Fingerprint)
	require.Len(t, response.Fingerprint.BranchToken, sha256.Size*2,
		"BranchToken MUST still be full SHA-256 hex (authoritative) for the trailing-space repo")
}

// TestReviewBasePathPreservesTrailingAndControlChars is the unit-level pin
// for reviewBasePath, the helper used by all five review remote endpoints to
// resolve the workdir. It MUST treat only the literal empty string as
// "use agent workdir" — anything else, including pure whitespace and paths
// with embedded control characters, flows through byte-exact.
func TestReviewBasePathPreservesTrailingAndControlChars(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name       string
		targetPath string
		want       string // empty want means "expect WorkDir(agentID)"
	}{
		{"empty", "", ""},
		{"trailing space", "/repo ", "/repo "},
		{"trailing tab", "/repo\t", "/repo\t"},
		{"trailing newline", "/repo\n", "/repo\n"},
		{"interior space", "/var/repos/agentteam workspace", "/var/repos/agentteam workspace"},
		{"pure whitespace (NOT trimmed to empty)", "   ", "   "},
		{"single space (NOT trimmed to empty)", " ", " "},
		{"NUL byte", "/repo\x00", "/repo\x00"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := reviewBasePath("agt_test", tc.targetPath)
			if tc.want == "" {
				// Empty targetPath triggers the workspacefs.WorkDir fallback.
				// We don't assert on the exact WorkDir value (it depends on
				// profile.ConfigDir), only that the fallback branched — i.e.
				// the returned path is NOT the empty input.
				require.NotEqual(t, tc.targetPath, got,
					"empty TargetPath MUST trigger the WorkDir fallback, not pass through as empty")
				require.Contains(t, got, "agt_test",
					"WorkDir fallback MUST include the agentID — the agent's workspace dir")
			} else {
				require.Equal(t, tc.want, got,
					"reviewBasePath MUST preserve TargetPath byte-exact — got %q want %q", got, tc.want)
				require.True(t, strings.HasSuffix(got, tc.want) || got == tc.want,
					"TargetPath MUST survive without trimming")
			}
		})
	}
}
