package run

import (
	"strings"
	"testing"

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

// TestPublicFailureCodeRoundTripsValidHyphenatedCodes proves valid hyphenated
// authored codes (dot-separated segments with hyphens) round-trip unchanged
// through the shared normalized producer validator.
func TestPublicFailureCodeRoundTripsValidHyphenatedCodes(t *testing.T) {
	valid := []string{
		"work-item.failed",
		"message.delivery-failed",
		"callback.responder-changed",
		"run.plan-version.invalid",
		"plan.node-unreachable",
		"approval.delivery_failed",
		"effect.kind_unsupported",
		"a",
	}
	for _, code := range valid {
		normalized, ok := PublicFailureCode(code)
		require.True(t, ok, "valid hyphenated code %q must round-trip", code)
		require.Equal(t, code, normalized)
	}
}

// TestPublicFailureCodeRejectsInvalidAndSecretBearing proves malformed or
// secret-bearing codes fail closed instead of being accepted or truncated.
func TestPublicFailureCodeRejectsInvalidAndSecretBearing(t *testing.T) {
	invalid := []string{
		"",
		"  ",
		"SELECT * FROM users",
		"password=hunter2",
		"token_abc secret",
		"work item.failed",
		"work_item..failed",
		".leading",
		"trailing.",
		"has_Unicode_文",
		"has@at",
		"has:colon",
		strings.Repeat("x", PublicFailureCodeRunes+1),
	}
	for _, code := range invalid {
		_, ok := PublicFailureCode(code)
		require.False(t, ok, "invalid/secret-bearing code %q must fail closed", code)
	}
}

// TestPublicFailureStageClosedVocabulary proves only the closed producer
// vocabulary is accepted; secret-bearing or unknown stage prose fails closed.
func TestPublicFailureStageClosedVocabulary(t *testing.T) {
	for _, stage := range []string{"dispatch", "reducer", "work_item", "callback", "material", "storage", "recovery", "approval"} {
		normalized, ok := PublicFailureStage(stage)
		require.True(t, ok)
		require.Equal(t, stage, normalized)
	}
	for _, stage := range []string{"", "SELECT secret", "reducer password=hunter2", "not-a-stage", "Work Item"} {
		_, ok := PublicFailureStage(stage)
		require.False(t, ok, "invalid stage %q must fail closed", stage)
	}
}

// TestPublicFailurePathRoundTripsActualNodeJSONWorkItemPaths proves the
// actual node/JSON/WorkItem paths (node ids, $.refs, [index], dotted keys)
// round-trip unchanged.
func TestPublicFailurePathRoundTripsActualNodeJSONWorkItemPaths(t *testing.T) {
	valid := []string{
		"notify",
		"$.nodes.entry",
		"$.input.reviewers[0]",
		"$.nodes.notify",
		"callback.workItems[2].key",
		"wnd_1",
		"dep/path/to/item",
		"$.input.value",
	}
	for _, path := range valid {
		normalized, ok := PublicFailurePath(path)
		require.True(t, ok, "valid path %q must round-trip", path)
		require.Equal(t, path, normalized)
	}
}

// TestPublicFailurePathRejectsProseAndSecrets proves free-form prose and
// secret-bearing paths fail closed rather than truncating into a different
// identity.
func TestPublicFailurePathRejectsProseAndSecrets(t *testing.T) {
	invalid := []string{
		"",
		"SELECT * FROM users",
		"path with spaces",
		"password=hunter2 tenant=acme",
		"stack at main.go:42",
		"line\nbreak",
		strings.Repeat("p", PublicFailurePathRunes+1),
	}
	for _, path := range invalid {
		_, ok := PublicFailurePath(path)
		require.False(t, ok, "invalid/secret path %q must fail closed", path)
	}
}

// TestPublicWorkItemIdentityRoundTrips proves WorkItem key/dependencyPath
// identities round-trip through the shared grammar.
func TestPublicWorkItemIdentityRoundTrips(t *testing.T) {
	key, ok := PublicWorkItemKey("child-item")
	require.True(t, ok)
	require.Equal(t, "child-item", key)

	path, ok := ValidDependencyPath("$.input.items[0].value")
	require.True(t, ok)
	require.Equal(t, "$.input.items[0].value", path)

	_, ok = PublicWorkItemKey("secret value")
	require.False(t, ok)
	_, ok = ValidDependencyPath("SELECT * FROM x")
	require.False(t, ok)
}

// TestPublicWorkItemKeyAuthoritative256ByteGrammar proves the WorkItem key
// uses the authoritative 256-byte ^[a-z][a-z0-9_-]{0,255}$ grammar: the max-valid
// 256-byte key round-trips, a leading-digit or dot-bearing key fails closed, and
// an oversized key fails closed (never truncated into a different valid identity).
func TestPublicWorkItemKeyAuthoritative256ByteGrammar(t *testing.T) {
	// Max-valid: 1 leading lowercase letter + 255 word/dash chars = 256 bytes.
	maxValid := "a" + strings.Repeat("b", 255)
	key, ok := PublicWorkItemKey(maxValid)
	require.True(t, ok, "the max-valid 256-byte WorkItem key must round-trip")
	require.Equal(t, maxValid, key)

	// Leading digit fails closed (must start with a lowercase letter).
	_, ok = PublicWorkItemKey("1leading-digit")
	require.False(t, ok, "a leading-digit WorkItem key must fail closed")

	// Dot-bearing key fails closed (the WorkItem grammar has no dots).
	_, ok = PublicWorkItemKey("child.item")
	require.False(t, ok, "a dot-bearing WorkItem key must fail closed")

	// Oversized (256 bytes + 1) fails closed and is never truncated.
	_, ok = PublicWorkItemKey("a" + strings.Repeat("b", 256))
	require.False(t, ok, "an oversized WorkItem key must fail closed")

	// Uppercase start fails closed.
	_, ok = PublicWorkItemKey("BadKey")
	require.False(t, ok, "an uppercase-leading WorkItem key must fail closed")
}

// TestMergeFailureTruncationTrueUnionDeduplicatesAndSorts proves the prior+fresh
// merge true-unions TruncatedFields (deduplicated) and sorts them, and carries
// DroppedCount/MinimalFallback deterministically.
func TestMergeFailureTruncationTrueUnionDeduplicatesAndSorts(t *testing.T) {
	prior := &FailureTruncation{
		DiagnosticsTruncated: true,
		OmittedCount:         2,
		DroppedCount:         1,
		TruncatedFields:      []string{"message", "path"},
		SerializedBytes:      500,
		MinimalFallback:      false,
	}
	fresh := &FailureTruncation{
		DiagnosticsTruncated: true,
		OmittedCount:         1,
		DroppedCount:         3,
		TruncatedFields:      []string{"path", "code", "message"},
		SerializedBytes:      700,
		MinimalFallback:      true,
	}
	merged := mergeFailureTruncation(prior, fresh)
	require.NotNil(t, merged)
	require.Equal(t, 3, merged.OmittedCount, "prior + fresh omitted counts must accumulate")
	require.Equal(t, 4, merged.DroppedCount, "prior + fresh dropped counts must accumulate")
	require.Equal(t, 700, merged.SerializedBytes, "serialized bytes come from the latest bound")
	require.True(t, merged.MinimalFallback, "minimal fallback is OR-ed")
	// True-union deduplicated and sorted.
	require.Equal(t, []string{"code", "message", "path"}, merged.TruncatedFields)
}

// TestMergeFailureTruncationClosesFieldNames proves an unknown/arbitrary field
// name in either receipt is dropped rather than entering the merged receipt.
func TestMergeFailureTruncationClosesFieldNames(t *testing.T) {
	prior := &FailureTruncation{
		DiagnosticsTruncated: true,
		OmittedCount:         1,
		TruncatedFields:      []string{"message", "SELECT * FROM users", "password=hunter2"},
	}
	fresh := &FailureTruncation{
		DiagnosticsTruncated: true,
		OmittedCount:         0,
		TruncatedFields:      []string{"code", "message", "tenant=acme"},
	}
	merged := mergeFailureTruncation(prior, fresh)
	require.NotNil(t, merged)
	require.Equal(t, []string{"code", "message"}, merged.TruncatedFields)
}

// TestMergeFailureTruncationNilHandling proves the merge is total for nil
// operands.
func TestMergeFailureTruncationNilHandling(t *testing.T) {
	require.Nil(t, mergeFailureTruncation(nil, nil))
	mergedFresh := mergeFailureTruncation(nil, &FailureTruncation{OmittedCount: 1})
	require.NotNil(t, mergedFresh)
	require.Equal(t, 1, mergedFresh.OmittedCount)
	prior := &FailureTruncation{DiagnosticsTruncated: true, OmittedCount: 5}
	merged := mergeFailureTruncation(prior, nil)
	require.Same(t, prior, merged, "a nil fresh cut must preserve the prior receipt")
}

// TestPublicFailureIdentityRejectsSurroundingWhitespaceAndInvalidUTF8 proves
// the identity validators are byte-for-byte: a persisted/remote identity with
// leading/trailing whitespace or malformed UTF-8 is never TrimSpace'd or
// UTF-8-sanitized into a different valid identity — it fails closed exactly.
func TestPublicFailureIdentityRejectsSurroundingWhitespaceAndInvalidUTF8(t *testing.T) {
	invalid := map[string]func(string) (string, bool){
		"code":         PublicFailureCode,
		"stage":        PublicFailureStage,
		"path":         PublicFailurePath,
		"workitem key": PublicWorkItemKey,
		"plan port":    PublicPlanPort,
	}
	for name, validate := range invalid {
		for _, value := range []string{
			" child ",
			" child",
			"child ",
			"\tchild\n",
			" child-item ",
			"child-item ",
			" child-item",
		} {
			_, ok := validate(value)
			require.False(t, ok, "%s %q must fail closed (surrounding whitespace is never trimmed)", name, value)
		}
		// Malformed UTF-8 byte sequence must fail closed (never replacement-char
		// sanitized into an accepted identity).
		_, ok := validate("child\xffitem")
		require.False(t, ok, "%s malformed-UTF-8 must fail closed", name)
	}
}

// TestPublicFailureIdentitySurvivesExactValidValues proves valid identities
// survive byte-for-byte (never altered by validation).
func TestPublicFailureIdentitySurvivesExactValidValues(t *testing.T) {
	cases := map[string]struct {
		value    string
		validate func(string) (string, bool)
	}{
		"code":         {"child-item.failed", PublicFailureCode},
		"stage":        {"reducer", PublicFailureStage},
		"path":         {"$.input.reviewers[0]", PublicFailurePath},
		"workitem key": {"child-item", PublicWorkItemKey},
		"plan port":    {"complete", PublicPlanPort},
	}
	for name, tc := range cases {
		normalized, ok := tc.validate(tc.value)
		require.True(t, ok, "%s %q must round-trip", name, tc.value)
		require.Equal(t, tc.value, normalized, "%s must survive unchanged", name)
	}
}

// TestPublicPlanPortRejectsMalformedBoundaries proves the authoritative Plan
// port grammar fails closed on leading-digit/uppercase/punctuation/oversize and
// accepts the exact valid boundary.
func TestPublicPlanPortRejectsMalformedBoundaries(t *testing.T) {
	for _, value := range []string{
		"", "1abc", "Abc", "a b", "a.b", "a$b", "a-b c",
		strings.Repeat("a", PublicPlanPortRunes+1),
	} {
		_, ok := PublicPlanPort(value)
		require.False(t, ok, "invalid plan port %q must fail closed", value)
	}
	maxValid := "a" + strings.Repeat("b", PublicPlanPortRunes-1)
	normalized, ok := PublicPlanPort(maxValid)
	require.True(t, ok, "the max-valid plan port must round-trip")
	require.Equal(t, maxValid, normalized)
}
