package plan

import (
	"testing"

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

// TestParseSelectOneStrategyIsClosed proves the closed strategy validator
// accepts only omitted (""), "ordered", and "random"; any other value (unknown,
// case-mangled, whitespace-only) is rejected with a bounded error — an unknown
// value must never be normalized to the legacy ordered default.
func TestParseSelectOneStrategyIsClosed(t *testing.T) {
	for _, valid := range []string{"", "ordered", "random"} {
		strategy, err := ParseSelectOneStrategy(valid)
		require.NoError(t, err, "strategy %q must be valid", valid)
		require.Equal(t, SelectOneStrategy(valid), strategy)
	}
	for _, invalid := range []string{"weighted", "RANDOM", "Ordered", "unknown", "ordered:random"} {
		_, err := ParseSelectOneStrategy(invalid)
		require.Error(t, err, "strategy %q must be rejected, not normalized", invalid)
	}
	// Validated values normalize losslessly; unknown never maps to ordered.
	require.Equal(t, SelectOneStrategyOrdered, SelectOneStrategy("").Normalized())
	require.Equal(t, SelectOneStrategyOrdered, SelectOneStrategy("ordered").Normalized())
	require.Equal(t, SelectOneStrategyRandom, SelectOneStrategy("random").Normalized())
}

// TestParseArtifactSelectOneStrategyIsClosed proves the artifact-side closed
// validator accepts only omitted (""), "ordered", and "random".
func TestParseArtifactSelectOneStrategyIsClosed(t *testing.T) {
	for _, valid := range []string{"", "ordered", "random"} {
		strategy, err := ParseArtifactSelectOneStrategy(valid)
		require.NoError(t, err, "strategy %q must be valid", valid)
		require.Equal(t, ArtifactSelectOneStrategy(valid), strategy)
	}
	for _, invalid := range []string{"weighted", "RANDOM", "Ordered", "unknown", "ordered:random"} {
		_, err := ParseArtifactSelectOneStrategy(invalid)
		require.Error(t, err, "strategy %q must be rejected, not normalized", invalid)
	}
}

// TestArtifactDecodeRejectsUnknownSelectOneStrategy proves a tampered artifact
// whose selectOneStrategy is an unknown value (with the semantic digest
// recomputed so only the closed validator stands between it and dispatch) is
// rejected by decode — it is never normalized to the legacy ordered default.
func TestArtifactDecodeRejectsUnknownSelectOneStrategy(t *testing.T) {
	artifact := compiledRandomSelectOneArtifact(t)
	node := artifactNodeByID(artifact, "start")
	node.Config.SendMessage.SelectOneStrategy = ArtifactSelectOneStrategy("weighted")
	normalized := recompileAndNormalize(t, artifact)

	_, err := DecodeArtifact(normalized)
	require.ErrorContains(t, err, "invalid artifact selectOne strategy")
}

// TestArtifactDecodeRejectsExplicitEmptyAndVariantSelectOneStrategy proves a
// tampered artifact carrying an explicit whitespace-padded or case-variant
// selectOneStrategy wire value is rejected by decode (the field is present but
// must resolve to exactly "ordered" or "random"). The explicit-empty form is
// covered at the validator level (ParseArtifactSelectOneStrategyPresent rejects
// "") because the omitempty wire form cannot express a present-but-empty field.
func TestArtifactDecodeRejectsExplicitEmptyAndVariantSelectOneStrategy(t *testing.T) {
	for _, tampered := range []ArtifactSelectOneStrategy{" random ", "Random", "Ordered"} {
		artifact := compiledRandomSelectOneArtifact(t)
		node := artifactNodeByID(artifact, "start")
		node.Config.SendMessage.SelectOneStrategy = tampered
		normalized := recompileAndNormalize(t, artifact)

		_, err := DecodeArtifact(normalized)
		require.Error(t, err, "tampered strategy %q must be rejected by decode", tampered)
	}
}

// TestArtifactDecodeRejectsOrphanSelectOneStrategy proves a strategy field
// without a selectOne pool (orphan) is rejected by decode. The tampered artifact
// keeps a fixed responder (a v4 feature) so only the closed orphan guard stands
// between it and dispatch.
func TestArtifactDecodeRejectsOrphanSelectOneStrategy(t *testing.T) {
	artifact, diagnostics, err := Compile([]byte(selectOnePlan(`{value: agt_a}`)))
	require.NoError(t, err)
	require.Empty(t, diagnostics, "fixed-responder source must compile: %#v", diagnostics)
	node := artifactNodeByID(artifact, "start")
	require.NotNil(t, node.Config.SendMessage.Responder)
	node.Config.SendMessage.SelectOneStrategy = ArtifactSelectOneStrategy("random")
	normalized := recompileAndNormalize(t, artifact)

	_, err = DecodeArtifact(normalized)
	require.ErrorContains(t, err, "selectOneStrategy is only valid with a selectOne pool")
}

// TestArtifactDecodeRejectsRandomSingletonPool proves a random strategy whose
// const selectOne pool has fewer than two deduplicated candidates is rejected
// by decode, even when the digest is recomputed to cover the tampered pool.
func TestArtifactDecodeRejectsRandomSingletonPool(t *testing.T) {
	artifact := compiledRandomSelectOneArtifact(t)
	node := artifactNodeByID(artifact, "start")
	values := []any{"agt_a", "agt_a"} // deduplicates to one
	compiled, err := compileArtifactLiteral(values, nil)
	require.NoError(t, err)
	node.Config.SendMessage.SelectOne.Value = compiled
	normalized := recompileAndNormalize(t, artifact)

	_, err = DecodeArtifact(normalized)
	require.ErrorContains(t, err, "at least two deduplicated candidates")
}

// TestArtifactDecodeAcceptsOmittedOrderedAndRandom proves the closed artifact
// validator admits the three legal forms: omitted (legacy ordered), explicit
// ordered, and random.
func TestArtifactDecodeAcceptsOmittedOrderedAndRandom(t *testing.T) {
	for _, responder := range []string{
		`{selectOne: {value: [agt_a, agt_b]}}`,
		`{selectOne: {value: [agt_a, agt_b]}, strategy: ordered}`,
		`{selectOne: {value: [agt_a, agt_b]}, strategy: random}`,
	} {
		artifact, diagnostics, err := Compile([]byte(selectOnePlan(responder)))
		require.NoError(t, err)
		require.Empty(t, diagnostics, "source must compile: %s", responder)
		deterministic, err := artifact.DeterministicJSON()
		require.NoError(t, err)
		decoded, err := DecodeArtifact(deterministic)
		require.NoError(t, err, "artifact must decode: %s", responder)
		require.NotNil(t, decoded, "artifact must decode: %s", responder)
	}
}

// compiledRandomSelectOneArtifact compiles a valid random-strategy selectOne
// plan into an Artifact.
func compiledRandomSelectOneArtifact(t *testing.T) *Artifact {
	t.Helper()
	source := selectOnePlan(`{selectOne: {value: [agt_a, agt_b]}, strategy: random}`)
	artifact, diagnostics, err := Compile([]byte(source))
	require.NoError(t, err)
	require.Empty(t, diagnostics, "source must compile: %#v", diagnostics)
	return artifact
}

// recompileAndNormalize recomputes the semantic digest over the (possibly
// tampered) artifact and returns its deterministic JSON. The digest is recomputed so
// the only guard between the tampered bytes and dispatch is the closed decoder.
func recompileAndNormalize(t *testing.T, artifact *Artifact) []byte {
	t.Helper()
	digest, err := artifact.computeDigest()
	require.NoError(t, err)
	artifact.Digest = digest
	deterministic, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	return deterministic
}
