package converter

import (
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

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

// TestToAgentReviewSnapshotPreservesPerFileTokenByteExact pins reviewer3's
// acceptance contract for the SNAPSHOT path: the per-file branch token (full
// 64-hex SHA-256) and per-file working token (16-hex truncated) MUST survive
// the agent → dto conversion byte-exact, for both the BranchFiles array AND
// the WorkingFiles array. A regression that truncated, trimmed, or rewrote
// the Token field in ToAgentReviewFile would force the frontend to re-fetch
// every file diff on every snapshot poll, defeating the fingerprint
// optimization.
func TestToAgentReviewSnapshotPreservesPerFileTokenByteExact(t *testing.T) {
	t.Parallel()

	// Construct a 64-hex branch token (authoritative) and a 16-hex working
	// token (hint) — the two lengths the desktop encoder emits.
	branchToken := hex.EncodeToString(sha256.New().Sum(nil))[:sha256.Size*2] // 64 hex
	workingToken := branchToken[:16]                                         // 16 hex
	require.Len(t, branchToken, 64)
	require.Len(t, workingToken, 16)

	// Include tokens containing characters that look "trimmable" to verify
	// the converter does NOT apply any whitespace normalization. Hex tokens
	// cannot contain whitespace, but the contract is that Token is opaque —
	// any string value survives byte-exact.
	trailingToken := "abcdef0123456789" // 16 hex, no internal whitespace
	require.Len(t, trailingToken, 16)

	src := agent.AgentReviewSnapshot{
		Summary: agent.AgentReviewSummary{Status: "ready"},
		Files: []agent.AgentReviewFile{
			{Path: "branch/main.go", ChangeType: "M", Token: branchToken},
			{Path: "branch/renamed.go", OldPath: "branch/old.go", ChangeType: "R", Token: trailingToken},
		},
		WorkingFiles: []agent.AgentReviewFile{
			{Path: "working/main.go", ChangeType: "M", Token: workingToken},
			{Path: "working/unstaged.go", ChangeType: "M", Unstaged: true, Token: trailingToken},
		},
	}

	got := ToAgentReviewSnapshot(src)

	require.Equal(t, src.Files[0].Token, got.Files[0].Token,
		"snapshot branch per-file token MUST survive converter byte-exact (full 64-hex SHA-256)")
	require.Equal(t, src.Files[1].Token, got.Files[1].Token,
		"snapshot branch per-file token (with trailing chars) MUST survive byte-exact")
	require.Len(t, got.Files[0].Token, 64,
		"snapshot branch per-file token MUST be full 64-hex (authoritative strength)")

	require.Equal(t, src.WorkingFiles[0].Token, got.WorkingFiles[0].Token,
		"snapshot working per-file token MUST survive converter byte-exact (16-hex hint)")
	require.Equal(t, src.WorkingFiles[1].Token, got.WorkingFiles[1].Token,
		"snapshot working per-file token (Unstaged flag set) MUST survive byte-exact")
	require.Len(t, got.WorkingFiles[0].Token, 16,
		"snapshot working per-file token MUST be 16-hex (hint strength)")
}

// TestToAgentReviewFilesPreservesPerFileTokenByteExact pins the same contract
// for the standalone FILES RPC path. Snapshot and Files are INDEPENDENT RPCs:
// a regression in one converter path must not be masked by the other passing.
// This test exercises ToAgentReviewFiles directly so a snapshot-side fix does
// not mask a files-side regression.
func TestToAgentReviewFilesPreservesPerFileTokenByteExact(t *testing.T) {
	t.Parallel()

	branchToken := strings.Repeat("a", 64)
	workingToken := strings.Repeat("b", 16)

	src := []agent.AgentReviewFile{
		{Path: "branch/main.go", ChangeType: "M", Token: branchToken},
		{Path: "working/main.go", ChangeType: "M", Token: workingToken},
	}

	got := ToAgentReviewFiles(src)

	require.Len(t, got, 2, "converter must preserve file count")
	require.Equal(t, branchToken, got[0].Token,
		"files branch per-file token MUST survive byte-exact (full 64-hex)")
	require.Equal(t, workingToken, got[1].Token,
		"files working per-file token MUST survive byte-exact (16-hex hint)")
	require.Len(t, got[0].Token, 64, "branch per-file token length")
	require.Len(t, got[1].Token, 16, "working per-file token length")
}

// TestToAgentReviewFingerprintPreservesAggregateTokensByteExact pins the
// fingerprint converter: RepositoryToken (64-hex), BranchToken (64-hex), and
// WorkingToken (16-hex) MUST each survive byte-exact. These three are the
// frontend's change-detection signals — truncating any one would cause
// either a false positive (frontend re-fetches unnecessarily) or a false
// negative (frontend skips a re-fetch when it should not).
func TestToAgentReviewFingerprintPreservesAggregateTokensByteExact(t *testing.T) {
	t.Parallel()

	repositoryToken := strings.Repeat("c", 64)
	branchToken := strings.Repeat("d", 64)
	workingToken := strings.Repeat("e", 16)

	src := agent.AgentReviewFingerprint{
		HeadCommit:      "abc123",
		BaseRef:         "main",
		HeadRef:         "feature",
		ComparisonMode:  "direct",
		RepositoryToken: repositoryToken,
		BranchToken:     branchToken,
		WorkingToken:    workingToken,
	}

	got := ToAgentReviewFingerprint(src)

	require.Equal(t, repositoryToken, got.RepositoryToken,
		"RepositoryToken MUST survive byte-exact (authoritative 64-hex)")
	require.Equal(t, branchToken, got.BranchToken,
		"BranchToken MUST survive byte-exact (authoritative 64-hex)")
	require.Equal(t, workingToken, got.WorkingToken,
		"WorkingToken MUST survive byte-exact (hint 16-hex)")

	require.Len(t, got.RepositoryToken, 64,
		"RepositoryToken MUST be full SHA-256 hex (64 chars), got %d", len(got.RepositoryToken))
	require.Len(t, got.BranchToken, 64,
		"BranchToken MUST be full SHA-256 hex (64 chars), got %d", len(got.BranchToken))
	require.Len(t, got.WorkingToken, 16,
		"WorkingToken MUST be 16-hex (hint strength), got %d", len(got.WorkingToken))

	// Hex charset sanity: each token must decode cleanly. A regression that
	// accidentally uppercased or injected separators would fail this.
	for name, tok := range map[string]string{
		"RepositoryToken": got.RepositoryToken,
		"BranchToken":     got.BranchToken,
		"WorkingToken":    got.WorkingToken,
	} {
		_, err := hex.DecodeString(tok)
		require.NoError(t, err, "%s must be valid lowercase hex, got %q", name, tok)
	}
}

// TestToAgentReviewFingerprintPreservesStrengthAndFreshnessFields verifies
// the non-token fields that the frontend uses to decide whether to re-fetch:
// WorkingStrength (hint vs authoritative) and WorkingStateFresh (cache hit vs
// synchronous scan). A regression that dropped either field would force the
// frontend into a default-safe posture of always re-fetching, defeating the
// fingerprint optimization just like a corrupted token would.
func TestToAgentReviewFingerprintPreservesStrengthAndFreshnessFields(t *testing.T) {
	t.Parallel()

	src := agent.AgentReviewFingerprint{
		WorkingStrength:   "authoritative",
		WorkingStateFresh: true,
		BranchFileCount:   42,
		StagedFileCount:   3,
		UnstagedFileCount: 7,
	}
	got := ToAgentReviewFingerprint(src)
	require.Equal(t, src.WorkingStrength, got.WorkingStrength,
		"WorkingStrength MUST survive — frontend uses hint vs authoritative to gate re-fetch")
	require.Equal(t, src.WorkingStateFresh, got.WorkingStateFresh,
		"WorkingStateFresh MUST survive — frontend uses freshness to skip stale re-fetches")
	require.Equal(t, src.BranchFileCount, got.BranchFileCount)
	require.Equal(t, src.StagedFileCount, got.StagedFileCount)
	require.Equal(t, src.UnstagedFileCount, got.UnstagedFileCount)
}

// TestToAgentReviewSnapshotPreservesEmptyTokenSlice pins the boundary case:
// an empty Files or WorkingFiles slice MUST round-trip as an empty dto slice,
// not nil. The dto→JSON layer emits `[]` for an empty slice and `null` for
// nil — the frontend treats those differently, and a nil mistake would cause
// "cannot read property 'length' of null" crashes on the client.
func TestToAgentReviewSnapshotPreservesEmptyTokenSlice(t *testing.T) {
	t.Parallel()

	got := ToAgentReviewFiles(nil)
	require.NotNil(t, got, "nil input MUST produce non-nil empty slice (frontend expects [] not null)")
	require.Len(t, got, 0)

	got = ToAgentReviewFiles([]agent.AgentReviewFile{})
	require.NotNil(t, got, "empty input MUST produce non-nil empty slice")
	require.Len(t, got, 0)
}
