package policy

import "testing"

// The escalation ladder is the load-bearing invariant of the policy layer:
// computed risk may only escalate, never de-escalate, against the caller's
// declaration. These pin the rank order and the unknown-input defaults that
// ClassifyCommand relies on (riskRank/normalizeRisk were only 66% covered).
func TestRiskRankOrder(t *testing.T) {
	cases := []struct {
		a, b string
		want bool // want riskRank(a) < riskRank(b)
	}{
		{RiskReadOnly, RiskReversibleLow, true},
		{RiskReversibleLow, RiskConfigMutation, true},
		{RiskConfigMutation, RiskDestructive, true},
		{RiskReadOnly, RiskDestructive, true},
		{RiskDestructive, RiskReadOnly, false},
		{RiskConfigMutation, RiskConfigMutation, false},
	}
	for _, c := range cases {
		if got := riskRank(c.a) < riskRank(c.b); got != c.want {
			t.Errorf("riskRank(%q) < riskRank(%q) = %v, want %v", c.a, c.b, got, c.want)
		}
	}
}

func TestRiskRankUnknownDefaultsToConfigMutation(t *testing.T) {
	// An unrecognized declared risk is treated as config_mutation — the
	// safer-to-gate default — not as the lowest tier.
	if r := riskRank("totally_made_up"); r != riskRank(RiskConfigMutation) {
		t.Errorf("riskRank(unknown) = %d, want %d (config_mutation)", r, riskRank(RiskConfigMutation))
	}
	// It therefore outranks read_only and reversible_low...
	if riskRank("made_up") <= riskRank(RiskReadOnly) {
		t.Error("unknown risk should outrank read_only")
	}
	if riskRank("made_up") <= riskRank(RiskReversibleLow) {
		t.Error("unknown risk should outrank reversible_low")
	}
	// ...but never outranks destructive.
	if riskRank("made_up") >= riskRank(RiskDestructive) {
		t.Error("unknown risk must not outrank destructive")
	}
}

func TestNormalizeRisk(t *testing.T) {
	cases := []struct {
		in   string
		want string
	}{
		{RiskReadOnly, RiskReadOnly},
		{RiskReversibleLow, RiskReversibleLow},
		{RiskConfigMutation, RiskConfigMutation},
		{RiskDestructive, RiskDestructive},
		// Unknown / empty / malformed declared risks collapse to the gated
		// default rather than the most-permissive tier.
		{"", RiskConfigMutation},
		{"bogus", RiskConfigMutation},
		{"READ_ONLY", RiskConfigMutation}, // case-sensitive: not normalized
		{"read-only", RiskConfigMutation}, // hyphen, not underscore
	}
	for _, c := range cases {
		if got := normalizeRisk(c.in); got != c.want {
			t.Errorf("normalizeRisk(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

// Escalation property: ClassifyCommand returns max(rank(computed), rank(declared)).
// Over a read-only command (computed rank 0) the declared risk passes through
// (undeclared → read_only; bogus → config_mutation); over a destructive command
// (computed rank 3) the result is always destructive.
func TestClassifyCommandEscalationIsMaxOfRanks(t *testing.T) {
	readOnlyExpected := []struct {
		declared, want string
	}{
		{"", RiskReadOnly},
		{RiskReadOnly, RiskReadOnly},
		{RiskReversibleLow, RiskReversibleLow},
		{RiskConfigMutation, RiskConfigMutation},
		{RiskDestructive, RiskDestructive},
		{"bogus", RiskConfigMutation}, // unknown declared → config_mutation rank
	}
	for _, c := range readOnlyExpected {
		if got := ClassifyCommand("uptime", c.declared); got != c.want {
			t.Errorf("read-only cmd + declared %q = %q, want %q", c.declared, got, c.want)
		}
	}
	for _, d := range []string{"", RiskReadOnly, RiskReversibleLow, RiskConfigMutation, RiskDestructive, "bogus"} {
		if got := ClassifyCommand("rm -rf /var/lib/x", d); got != RiskDestructive {
			t.Errorf("destructive cmd + declared %q = %q, want destructive", d, got)
		}
	}
}
