package plan

import (
	"testing"

	"gopkg.in/yaml.v3"
)

// TestResponderSelectOneStrategyYAMLNormalizedMatrix pins the exact strategy
// presence/value semantics at the YAML authoring boundary: an absent field alone
// is the legacy ordered default; a present field must resolve to exactly one
// string scalar "ordered" or "random" with no trimming/coercion. Duplicate
// keys, aliases to invalid kinds/values, null/bool/number/sequence/mapping, and
// whitespace/case variants all fail closed.
func TestResponderSelectOneStrategyYAMLNormalizedMatrix(t *testing.T) {
	valid := []struct {
		name string
		yaml string
		want SelectOneStrategy
	}{
		{"omitted stays ordered", "selectOne: {value: ['a', 'b']}", SelectOneStrategyOrdered},
		{"explicit ordered", "selectOne: {value: ['a', 'b']}\nstrategy: ordered", SelectOneStrategyOrdered},
		{"random", "selectOne: {value: ['a', 'b']}\nstrategy: random", SelectOneStrategyRandom},
	}
	for _, tc := range valid {
		t.Run("valid/"+tc.name, func(t *testing.T) {
			var selection ResponderSelection
			if err := yaml.Unmarshal([]byte(tc.yaml), &selection); err != nil {
				t.Fatalf("unmarshal: %v", err)
			}
			if selection.SelectOne == nil {
				t.Fatal("expected selectOne pool")
			}
			if got := selection.Strategy.Normalized(); got != tc.want {
				t.Fatalf("strategy = %q, want %q", got, tc.want)
			}
		})
	}

	invalid := []struct {
		name string
		yaml string
	}{
		{"whitespace padded", "selectOne: {value: ['a', 'b']}\nstrategy: ' random '"},
		{"case variant", "selectOne: {value: ['a', 'b']}\nstrategy: Random"},
		{"unknown", "selectOne: {value: ['a', 'b']}\nstrategy: weighted"},
		{"explicit empty scalar", "selectOne: {value: ['a', 'b']}\nstrategy: ''"},
		{"null", "selectOne: {value: ['a', 'b']}\nstrategy: null"},
		{"bool", "selectOne: {value: ['a', 'b']}\nstrategy: true"},
		{"number", "selectOne: {value: ['a', 'b']}\nstrategy: 1"},
		{"sequence", "selectOne: {value: ['a', 'b']}\nstrategy: [random]"},
		{"mapping", "selectOne: {value: ['a', 'b']}\nstrategy: {ordered: true}"},
		{"duplicate strategy key", "selectOne: {value: ['a', 'b']}\nstrategy: random\nstrategy: ordered"},
		{"duplicate selectOne key", "selectOne: {value: ['a', 'b']}\nselectOne: {value: ['c', 'd']}"},
		{"strategy without selectOne", "strategy: random"},
	}
	for _, tc := range invalid {
		t.Run("invalid/"+tc.name, func(t *testing.T) {
			var selection ResponderSelection
			if err := yaml.Unmarshal([]byte(tc.yaml), &selection); err == nil {
				t.Fatalf("expected %q to be rejected", tc.yaml)
			}
		})
	}
}

// TestResponderSelectOneStrategyYAMLAliasMatrix proves aliases are resolved
// safely: an alias to a valid string scalar "random" is accepted, while an alias
// to a mapping, a non-string scalar, or an unknown value fails closed.
func TestResponderSelectOneStrategyYAMLAliasMatrix(t *testing.T) {
	var selection ResponderSelection
	// Alias to a valid string scalar.
	if err := yaml.Unmarshal([]byte("strategy: &s random\nselectOne: {value: ['a', 'b']}\nstrategy: *s"), &selection); err == nil {
		t.Fatal("duplicate strategy key (even via alias) must be rejected")
	}
	selection = ResponderSelection{}
	// Alias to a mapping value.
	if err := yaml.Unmarshal([]byte("strategy: &m {ordered: true}\nselectOne: {value: ['a', 'b']}\nstrategy: *m"), &selection); err == nil {
		t.Fatal("alias to a mapping must be rejected")
	}
	selection = ResponderSelection{}
	// Alias to a number.
	if err := yaml.Unmarshal([]byte("strategy: &n 7\nselectOne: {value: ['a', 'b']}\nstrategy: *n"), &selection); err == nil {
		t.Fatal("alias to a number must be rejected")
	}
	selection = ResponderSelection{}
	// Alias to an unknown string.
	if err := yaml.Unmarshal([]byte("strategy: &u weighted\nselectOne: {value: ['a', 'b']}\nstrategy: *u"), &selection); err == nil {
		t.Fatal("alias to an unknown value must be rejected")
	}
}

// TestArtifactSelectOneStrategyJSONNormalizedMatrix pins the strict presence/value
// semantics at the artifact JSON decode boundary. An absent selectOneStrategy
// field alone is the legacy ordered default; a present field must resolve to
// exactly "ordered" or "random" (explicit empty/whitespace/case/unknown fails
// closed).
func TestArtifactSelectOneStrategyJSONNormalizedMatrix(t *testing.T) {
	valid := []struct {
		name     string
		wire     string
		wantNorm ArtifactSelectOneStrategy
	}{
		{"ordered", "ordered", ArtifactSelectOneStrategyOrdered},
		{"random", "random", ArtifactSelectOneStrategyRandom},
	}
	for _, tc := range valid {
		t.Run("valid/"+tc.name, func(t *testing.T) {
			parsed, err := ParseArtifactSelectOneStrategyPresent(tc.wire)
			if err != nil {
				t.Fatalf("unexpected error for %s: %v", tc.name, err)
			}
			if got := parsed.Normalized(); got != tc.wantNorm {
				t.Fatalf("normalized = %q, want %q", got, tc.wantNorm)
			}
		})
	}

	invalid := []string{
		"",
		" random ",
		"Random",
		"weighted",
		"null",
		"true",
		"7",
	}
	for _, wire := range invalid {
		t.Run("invalid/"+quoteOrEmpty(wire), func(t *testing.T) {
			if _, err := ParseArtifactSelectOneStrategyPresent(wire); err == nil {
				t.Fatalf("expected %q to be rejected", wire)
			}
		})
	}
}

// TestSelectOneStrategyPresentValidatorsStrict proves the present-field
// validators reject explicit empty while the internal parse accepts the omitted
// sentinel.
func TestSelectOneStrategyPresentValidatorsStrict(t *testing.T) {
	if _, err := ParseSelectOneStrategyPresent(""); err == nil {
		t.Fatal("explicit empty must be rejected when the field is present")
	}
	if _, err := ParseSelectOneStrategyPresent(" ordered "); err == nil {
		t.Fatal("whitespace-padded must be rejected when the field is present")
	}
	if _, err := ParseSelectOneStrategyPresent("Random"); err == nil {
		t.Fatal("case variant must be rejected when the field is present")
	}
	for _, ok := range []string{"ordered", "random"} {
		if _, err := ParseSelectOneStrategyPresent(ok); err != nil {
			t.Fatalf("present %q must be accepted: %v", ok, err)
		}
	}
	// The omitted sentinel stays valid for the internal/programmatic path.
	if _, err := ParseSelectOneStrategy(""); err != nil {
		t.Fatalf("omitted sentinel must stay valid internally: %v", err)
	}
}

func quoteOrEmpty(raw string) string {
	if raw == "" {
		return `""`
	}
	return raw
}
