package review

import (
	"strings"
	"testing"

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

// TestReviewContextTargetHashTreatsTrailingWhitespaceRootAsDistinct pins the
// canonical-root byte-exactness invariant reviewer3 required: two review
// targets whose RootPath differs ONLY by trailing whitespace ("/repo" vs
// "/repo ") MUST hash to distinct target_hash values.
//
// Why: target_hash is the dedup key for review_contexts. If the hash were to
// trim the rootPath, a second upsert with a trailing-whitespace variant would
// collide with the first row and silently remap one worktree onto the other's
// thread / source_agent / display_name. That is exactly the regression that
// re-introducing strings.TrimSpace(rootPath) in this function would trigger.
//
// The refs (baseRef/headRef) and machineID remain trim-normalized because they
// are control strings (git refs, IDs) — that discipline pre-dates this PR and
// is unrelated to the canonical-root byte-exactness fix.
func TestReviewContextTargetHashTreatsTrailingWhitespaceRootAsDistinct(t *testing.T) {

	const (
		machine  = "machine_1"
		baseRef  = "main"
		headRef  = "feature"
		rootA    = "/repo"
		rootB    = "/repo "
		rootTab  = "/repo\t"
		rootCRLF = "/repo\r\n"
	)

	hashA := reviewContextTargetHash(machine, rootA, baseRef, headRef)
	hashB := reviewContextTargetHash(machine, rootB, baseRef, headRef)
	hashTab := reviewContextTargetHash(machine, rootTab, baseRef, headRef)
	hashCRLF := reviewContextTargetHash(machine, rootCRLF, baseRef, headRef)

	require.NotEqual(t, hashA, hashB,
		"roots '/repo' and '/repo ' MUST hash to distinct values — trailing space is part of the canonical path")
	require.NotEqual(t, hashA, hashTab,
		"roots '/repo' and '/repo\\t' MUST hash to distinct values")
	require.NotEqual(t, hashA, hashCRLF,
		"roots '/repo' and '/repo\\r\\n' MUST hash to distinct values")
	require.NotEqual(t, hashB, hashTab,
		"distinct trailing-whitespace variants MUST hash to distinct values")
}

// TestReviewContextTargetHashIsStableForSameRoot verifies the inverse: the
// same input tuple always maps to the same hash. This is the load-bearing
// contract that lets the upsert ON DUPLICATE KEY UPDATE path find the existing
// row and refresh its metadata instead of creating a second one.
func TestReviewContextTargetHashIsStableForSameRoot(t *testing.T) {

	const (
		machine = "machine_1"
		baseRef = "main"
		headRef = "feature"
		root    = "/var/repos/agentteam "
	)

	h1 := reviewContextTargetHash(machine, root, baseRef, headRef)
	h2 := reviewContextTargetHash(machine, root, baseRef, headRef)
	require.Equal(t, h1, h2, "same tuple must hash to same value")
}

// TestReviewContextTargetHashDistinguishesRefAndMachineChanges verifies the
// hash is still sensitive to the OTHER inputs (machineID, refs) — the
// trailing-whitespace fix must NOT have flattened the rest of the tuple.
func TestReviewContextTargetHashDistinguishesRefAndMachineChanges(t *testing.T) {

	const root = "/repo"

	require.NotEqual(t,
		reviewContextTargetHash("machine_A", root, "main", "feature"),
		reviewContextTargetHash("machine_B", root, "main", "feature"),
		"different machineID must produce a different hash")

	require.NotEqual(t,
		reviewContextTargetHash("machine_A", root, "main", "feature"),
		reviewContextTargetHash("machine_A", root, "main", "release"),
		"different headRef must produce a different hash")

	require.NotEqual(t,
		reviewContextTargetHash("machine_A", root, "main", "feature"),
		reviewContextTargetHash("machine_A", root, "trunk", "feature"),
		"different baseRef must produce a different hash")
}

// TestReviewContextTargetHashIsHexSha256 verifies the output is a 64-char
// lowercase-hex SHA-256 digest. The schema (migration 00240) declares
// target_hash as a fixed-width hex column; a non-hex or wrong-length hash
// would either fail to insert or collide in the index.
func TestReviewContextTargetHashIsHexSha256(t *testing.T) {

	h := reviewContextTargetHash("machine_1", "/repo", "main", "feature")
	require.Len(t, h, 64, "target_hash must be a full SHA-256 hex digest")
	for _, c := range h {
		require.True(t, strings.ContainsRune("0123456789abcdef", c),
			"target_hash must be lowercase hex, got %q", h)
	}
}

// TestReviewContextTargetHashEmptyRefsParticipateAsEmpty verifies that empty
// base/head refs hash distinctly from non-empty ones — they participate in the
// tuple as empty strings rather than being skipped. Without this, a
// default-ref fallback applied at one layer but not another would let two
// different inputs collapse onto the same row.
func TestReviewContextTargetHashEmptyRefsParticipateAsEmpty(t *testing.T) {

	const (
		machine = "machine_1"
		root    = "/repo"
	)

	require.NotEqual(t,
		reviewContextTargetHash(machine, root, "", ""),
		reviewContextTargetHash(machine, root, "main", "feature"),
		"empty refs must NOT collide with non-empty refs")

	require.NotEqual(t,
		reviewContextTargetHash(machine, root, "", ""),
		reviewContextTargetHash(machine, root, "main", ""),
		"empty baseRef must NOT collide with non-empty baseRef when headRef is empty in both")
}
