package streamgate

import (
	"context"
	"sort"
	"strings"
	"testing"
	"time"
)

// stubEvaluator implements FilterEvaluator for tests. It exposes the id and
// a return decision/error so test cases can drive readiness normalisation and
// outcome composition without touching the guardrail.
type stubEvaluator struct {
	id              string
	decision        FilterDecision
	err             error
	evaluateInvoked bool
}

func (s *stubEvaluator) ID() string { return s.id }
func (s *stubEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
	s.evaluateInvoked = true
	return s.decision, s.err
}

// newPassDecision returns a passing FilterDecision for use in readiness and
// outcome composition tests.
func newPassDecision(t *testing.T, filterID string) FilterDecision {
	t.Helper()
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)
	d, err := NewFilterDecision(FilterDecisionKindPass, "consumer.id", filterID, "rule.id", evidence, nil)
	if err != nil {
		t.Fatalf("newPassDecision: %v", err)
	}
	return d
}

// newViolationDecision returns a violating FilterDecision for enforcement
// composition tests.
func newViolationDecision(t *testing.T, filterID string) FilterDecision {
	t.Helper()
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)
	d, err := NewFilterDecision(FilterDecisionKindViolation, "consumer.id", filterID, "rule.id", evidence, nil)
	if err != nil {
		t.Fatalf("newViolationDecision: %v", err)
	}
	return d
}

// newFatalDecision returns a fatal FilterDecision for enforcement composition tests.
func newFatalDecision(t *testing.T, filterID string) FilterDecision {
	t.Helper()
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)
	d, err := NewFilterDecision(FilterDecisionKindFatal, "consumer.id", filterID, "rule.id", evidence, nil)
	if err != nil {
		t.Fatalf("newFatalDecision: %v", err)
	}
	return d
}

// mustEvalError creates a FilterOutcome of kind evaluation_error with a
// stable error code, failing the test on any error.
func mustEvalError(t *testing.T, code string) FilterOutcome {
	t.Helper()
	o, err := NewFilterOutcomeEvaluationError(code)
	if err != nil {
		t.Fatalf("NewFilterOutcomeEvaluationError: %v", err)
	}
	return o
}

// --- [API-1] Resolved epoch filter binding ---

// TestEpochFilterNormalizesApplicabilityAndRequirement verifies the readiness
// normalisation table across all four states. Each matrix entry is driven by
// a distinct stub evaluator so accidental coupling between tests is avoided.
func TestEpochFilterNormalizesApplicabilityAndRequirement(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")

	cases := []struct {
		name              string
		subscribed        bool
		triggerReady      bool
		blocksRelease     bool
		enforcement       FilterEnforcement
		wantForEpoch      bool
		wantNotApplicable bool
		wantDeferred      bool
	}{
		{
			name:              "subscribed_trigger_ready",
			subscribed:        true,
			triggerReady:      true,
			blocksRelease:     false,
			enforcement:       FilterEnforcementBlocking,
			wantForEpoch:      true,
			wantNotApplicable: false,
			wantDeferred:      false,
		},
		{
			name:              "subscribed_trigger_not_ready_blocks",
			subscribed:        true,
			triggerReady:      false,
			blocksRelease:     true,
			enforcement:       FilterEnforcementBlocking,
			wantForEpoch:      false,
			wantNotApplicable: false,
			wantDeferred:      true,
		},
		{
			name:              "subscribed_trigger_not_ready_nonblock",
			subscribed:        true,
			triggerReady:      false,
			blocksRelease:     false,
			enforcement:       FilterEnforcementObserveOnly,
			wantForEpoch:      false,
			wantNotApplicable: true,
			wantDeferred:      false,
		},
		{
			name:              "unsubscribed",
			subscribed:        false,
			triggerReady:      false,
			blocksRelease:     false,
			enforcement:       FilterEnforcementBlocking,
			wantForEpoch:      false,
			wantNotApplicable: true,
			wantDeferred:      false,
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			e := &stubEvaluator{id: "filter.id", decision: passDecision}
			f, err := NewEpochFilter(
				e, 100,
				tc.subscribed, tc.triggerReady, tc.blocksRelease,
				tc.enforcement,
				100*time.Millisecond,
			)
			if err != nil {
				t.Fatalf("NewEpochFilter: %v", err)
			}

			if f.EvaluatedForEpoch() != tc.wantForEpoch {
				t.Errorf("EvaluatedForEpoch() = %v, want %v", f.EvaluatedForEpoch(), tc.wantForEpoch)
			}

			oc := f.NormalizeOutcome()
			if tc.wantNotApplicable && oc.Kind() != FilterOutcomeKindNotApplicableForEpoch {
				t.Errorf("NormalizeOutcome() Kind() = %v, want not_applicable_for_epoch", oc.Kind())
			}
			if tc.wantDeferred && oc.Kind() != FilterOutcomeKindDeferredByRequirement {
				t.Errorf("NormalizeOutcome() Kind() = %v, want deferred_by_requirement", oc.Kind())
			}

			// Accessors must reflect construction inputs exactly.
			if f.SubscribedEventPresent() != tc.subscribed {
				t.Errorf("SubscribedEventPresent() = %v, want %v", f.SubscribedEventPresent(), tc.subscribed)
			}
			if f.TriggerReady() != tc.triggerReady {
				t.Errorf("TriggerReady() = %v, want %v", f.TriggerReady(), tc.triggerReady)
			}
			if f.BlocksRelease() != tc.blocksRelease {
				t.Errorf("BlocksRelease() = %v, want %v", f.BlocksRelease(), tc.blocksRelease)
			}
			if f.EnforcedAs() != tc.enforcement {
				t.Errorf("EnforcedAs() = %v, want %v", f.EnforcedAs(), tc.enforcement)
			}
		})
	}
}

// TestEpochFilterRejectsInvalidExecutionContract verifies that NewEpochFilter
// rejects every invalid constructor combination: nil evaluator, empty id,
// invalid id grammar, unknown enforcement, and non-positive timeout.
func TestEpochFilterRejectsInvalidExecutionContract(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")

	// --- nil evaluator ---
	t.Run("nil_evaluator", func(t *testing.T) {
		_, err := NewEpochFilter(
			nil, 100,
			true, true, false,
			FilterEnforcementBlocking,
			100*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(nil, ...) should return error")
		}
	})

	// --- negative priority ---
	t.Run("negative_priority", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "filter.id", decision: passDecision}, -1,
			true, true, false,
			FilterEnforcementBlocking,
			100*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(negative priority, ...) should return error")
		}
	})

	// --- empty evaluator id ---
	t.Run("empty_evaluator_id", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "", decision: passDecision}, 100,
			true, true, false,
			FilterEnforcementBlocking,
			100*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(empty id, ...) should return error")
		}
	})

	// --- invalid evaluator id grammar ---
	t.Run("invalid_evaluator_id_grammar", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "INVALID-ID!", decision: passDecision}, 100,
			true, true, false,
			FilterEnforcementBlocking,
			100*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(invalid id grammar, ...) should return error")
		}
	})

	// --- unknown enforcement ---
	t.Run("unknown_enforcement", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
			true, true, false,
			"unknown",
			100*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(unknown enforcement, ...) should return error")
		}
	})

	// --- zero timeout ---
	t.Run("zero_timeout", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
			true, true, false,
			FilterEnforcementBlocking,
			0,
		)
		if err == nil {
			t.Error("NewEpochFilter(zero timeout, ...) should return error")
		}
	})

	// --- negative timeout ---
	t.Run("negative_timeout", func(t *testing.T) {
		_, err := NewEpochFilter(
			&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
			true, true, false,
			FilterEnforcementBlocking,
			-1*time.Millisecond,
		)
		if err == nil {
			t.Error("NewEpochFilter(negative timeout, ...) should return error")
		}
	})
}

// TestEpochFilterTimeoutAndAccessorImmutability verifies that the timeout is
// stored exactly and that accessor reads do not leak mutable state.
func TestEpochFilterTimeoutAndAccessorImmutability(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")
	timeout := 42 * time.Millisecond
	f, err := NewEpochFilter(
		&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
		true, true, false,
		FilterEnforcementObserveOnly,
		timeout,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter: %v", err)
	}
	if f.EvaluationTimeout() != timeout {
		t.Errorf("EvaluationTimeout() = %v, want %v", f.EvaluationTimeout(), timeout)
	}

	// The evaluator is returned by reference; verify that it is the same
	// instance but that the accessors do not expose internal mutation paths.
	e := f.Evaluator()
	if e.ID() != "filter.id" {
		t.Errorf("Evaluator().ID() = %q, want %q", e.ID(), "filter.id")
	}
}

// --- [API-2] Immutable evaluation outcome set ---

// TestEvaluationSetIsStableAndImmutable verifies that the set sorts by filter
// id regardless of input order, rejects duplicates, and returns defensive
// copies on every accessor path.
func TestEvaluationSetIsStableAndImmutable(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)

	// Build outcomes in reverse id order to exercise sort stability.
	d1, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "beta.id", "rule.id", evidence, nil)
	d2, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "alpha.id", "rule.id", evidence, nil)
	d3, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "gamma.id", "rule.id", evidence, nil)

	ocBeta, _ := NewFilterOutcomeEvaluated(d1)
	ocAlpha, _ := NewFilterOutcomeEvaluated(d2)
	ocGamma, _ := NewFilterOutcomeEvaluated(d3)

	betaO, _ := NewEpochFilterOutcome("beta.id", 100, ocBeta, FilterEnforcementBlocking)
	alphaO, _ := NewEpochFilterOutcome("alpha.id", 100, ocAlpha, FilterEnforcementBlocking)
	gammaO, _ := NewEpochFilterOutcome("gamma.id", 100, ocGamma, FilterEnforcementBlocking)

	// Input in reverse id order: gamma, beta, alpha.
	inputs := []EpochFilterOutcome{
		gammaO,
		betaO,
		alphaO,
	}

	set, err := NewEvaluationSet(inputs)
	if err != nil {
		t.Fatalf("NewEvaluationSet: %v", err)
	}

	// Stable order: alpha < beta < gamma.
	wantOrder := []string{"alpha.id", "beta.id", "gamma.id"}
	for i, w := range wantOrder {
		o, err := set.At(i)
		if err != nil {
			t.Fatalf("At(%d): %v", i, err)
		}
		if o.FilterID() != w {
			t.Errorf("At(%d).FilterID() = %q, want %q", i, o.FilterID(), w)
		}
	}

	// --- ByID lookup succeeds for sorted ids ---
	if _, err := set.ByID("alpha.id"); err != nil {
		t.Errorf("ByID('alpha.id'): %v", err)
	}
	if _, err := set.ByID("nonexistent"); err == nil {
		t.Error("ByID('nonexistent') should return error")
	}

	// --- All() returns defensive copy in stable order ---
	all := set.All()
	if len(all) != 3 {
		t.Fatalf("All() len = %d, want 3", len(all))
	}
	if all[0].FilterID() != "alpha.id" {
		t.Errorf("All()[0].FilterID() = %q, want %q", all[0].FilterID(), "alpha.id")
	}

	// --- Mutating All() does not affect stored ---
	all[0] = EpochFilterOutcome{}
	if set.Len() != 3 {
		t.Errorf("Len() after All() mutation = %d, want 3", set.Len())
	}

	// --- Duplicate filter id is rejected ---
	dupInputs := []EpochFilterOutcome{
		alphaO,
		alphaO,
	}
	if _, err := NewEvaluationSet(dupInputs); err == nil {
		t.Error("NewEvaluationSet(duplicate id) should return error")
	}
}

// TestEpochFilterOutcomeValidatesFailurePolicy verifies that the failure
// disposition is derived correctly from the outcome kind, enforcement mode,
// and decision kind. It also verifies that invalid combinations are rejected.
func TestEpochFilterOutcomeValidatesFailurePolicy(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	_ = ts // reserved for future expansion

	cases := []struct {
		name        string
		filterID    string
		enforcement FilterEnforcement
		wantKind    FilterOutcomeKind
		desiredDisp EvaluationFailureDisposition
	}{
		{
			name:        "pass_blocking",
			filterID:    "f1",
			enforcement: FilterEnforcementBlocking,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionNone,
		},
		{
			name:        "pass_observe",
			filterID:    "f2",
			enforcement: FilterEnforcementObserveOnly,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionNone,
		},
		{
			name:        "violation_blocking",
			filterID:    "f3",
			enforcement: FilterEnforcementBlocking,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionBlockingFatal,
		},
		{
			name:        "violation_observe",
			filterID:    "f4",
			enforcement: FilterEnforcementObserveOnly,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionObserveError,
		},
		{
			name:        "fatal_blocking",
			filterID:    "f5",
			enforcement: FilterEnforcementBlocking,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionBlockingFatal,
		},
		{
			name:        "fatal_observe",
			filterID:    "f6",
			enforcement: FilterEnforcementObserveOnly,
			wantKind:    FilterOutcomeKindEvaluated,
			desiredDisp: EvaluationFailureDispositionObserveError,
		},
		{
			name:        "not_applicable_blocking",
			filterID:    "f9",
			enforcement: FilterEnforcementBlocking,
			wantKind:    FilterOutcomeKindNotApplicableForEpoch,
			desiredDisp: EvaluationFailureDispositionNone,
		},
		{
			name:        "deferred",
			filterID:    "f10",
			enforcement: FilterEnforcementObserveOnly,
			wantKind:    FilterOutcomeKindDeferredByRequirement,
			desiredDisp: EvaluationFailureDispositionNone,
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			var oc FilterOutcome
			switch tc.name {
			case "violation_blocking", "violation_observe":
				oc = mustEvalOutcome(t, newViolationDecision(t, tc.filterID))
			case "fatal_blocking", "fatal_observe":
				oc = mustEvalOutcome(t, newFatalDecision(t, tc.filterID))
			case "not_applicable_blocking":
				oc = NewFilterOutcomeNotApplicableForEpoch()
			case "deferred":
				oc = NewFilterOutcomeDeferredByRequirement()
			default:
				oc = mustEvalOutcome(t, newPassDecision(t, tc.filterID))
			}
			o, err := NewEpochFilterOutcome(tc.filterID, 100, oc, tc.enforcement)
			if err != nil {
				t.Fatalf("NewEpochFilterOutcome: %v", err)
			}
			if o.FailureDisposition() != tc.desiredDisp {
				t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), tc.desiredDisp)
			}
			if o.Outcome().Kind() != tc.wantKind {
				t.Errorf("Outcome().Kind() = %v, want %v", o.Outcome().Kind(), tc.wantKind)
			}
			if o.FilterID() != tc.filterID {
				t.Errorf("FilterID() = %q, want %q", o.FilterID(), tc.filterID)
			}
			if o.Enforcement() != tc.enforcement {
				t.Errorf("Enforcement() = %v, want %v", o.Enforcement(), tc.enforcement)
			}
		})
	}

	// --- evaluation_error with blocking/observe_only ---
	t.Run("eval_error_blocking", func(t *testing.T) {
		eoc := mustEvalError(t, "err.code")
		o, err := NewEpochFilterOutcome("f7", 100, eoc, FilterEnforcementBlocking)
		if err != nil {
			t.Fatalf("NewEpochFilterOutcome: %v", err)
		}
		if o.Outcome().Kind() != FilterOutcomeKindEvaluationError {
			t.Errorf("Outcome().Kind() = %v, want evaluation_error", o.Outcome().Kind())
		}
		if o.FailureDisposition() != EvaluationFailureDispositionBlockingFatal {
			t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), EvaluationFailureDispositionBlockingFatal)
		}
	})

	t.Run("eval_error_observe", func(t *testing.T) {
		eoc := mustEvalError(t, "err.code")
		o, err := NewEpochFilterOutcome("f8", 100, eoc, FilterEnforcementObserveOnly)
		if err != nil {
			t.Fatalf("NewEpochFilterOutcome: %v", err)
		}
		if o.Outcome().Kind() != FilterOutcomeKindEvaluationError {
			t.Errorf("Outcome().Kind() = %v, want evaluation_error", o.Outcome().Kind())
		}
		if o.FailureDisposition() != EvaluationFailureDispositionObserveError {
			t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), EvaluationFailureDispositionObserveError)
		}
	})

	// --- Invalid combinations are rejected ---
	t.Run("invalid_empty_filter_id", func(t *testing.T) {
		_, err := NewEpochFilterOutcome("", 100, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
		if err == nil {
			t.Error("NewEpochFilterOutcome(empty id) should return error")
		}
	})

	t.Run("invalid_bad_id_grammar", func(t *testing.T) {
		_, err := NewEpochFilterOutcome("BAD!ID", 100, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
		if err == nil {
			t.Error("NewEpochFilterOutcome(bad id grammar) should return error")
		}
	})

	t.Run("invalid_unknown_enforcement", func(t *testing.T) {
		_, err := NewEpochFilterOutcome("f.id", 100, NewFilterOutcomeNotApplicableForEpoch(), "unknown")
		if err == nil {
			t.Error("NewEpochFilterOutcome(unknown enforcement) should return error")
		}
	})

	t.Run("invalid_negative_priority", func(t *testing.T) {
		_, err := NewEpochFilterOutcome("f.id", -1, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
		if err == nil {
			t.Error("NewEpochFilterOutcome(negative priority) should return error")
		}
	})
}

// TestEvaluationSetRejectsInvalidEntries verifies that invalid EpochFilterOutcome
// entries (empty filter id, bad grammar, invalid outcome) are rejected at
// set construction.
func TestEvaluationSetRejectsInvalidEntries(t *testing.T) {
	// Empty filter id is rejected at NewEpochFilterOutcome level.
	goodOutcome := NewFilterOutcomeNotApplicableForEpoch()
	badO, err := NewEpochFilterOutcome("", 100, goodOutcome, FilterEnforcementBlocking)
	if err == nil {
		t.Error("NewEpochFilterOutcome(empty id) should return error")
	}
	if badO.FilterID() != "" {
		t.Errorf("badO.FilterID() = %q, want empty", badO.FilterID())
	}
}

// TestEvaluationSetAccessorReturnsDefensiveCopy verifies that mutating the
// return value of All() does not affect the set's stored outcomes.
func TestEvaluationSetAccessorReturnsDefensiveCopy(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)
	d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "filter.id", "rule.id", evidence, nil)
	oc, _ := NewFilterOutcomeEvaluated(d)
	o, _ := NewEpochFilterOutcome("filter.id", 100, oc, FilterEnforcementBlocking)
	set, err := NewEvaluationSet([]EpochFilterOutcome{o})
	if err != nil {
		t.Fatalf("NewEvaluationSet: %v", err)
	}

	// --- All() mutation ---
	all := set.All()
	all[0] = EpochFilterOutcome{}
	if set.Len() != 1 {
		t.Error("All() replacement leaked to set length")
	}

	// --- At() element ---
	atO, err := set.At(0)
	if err != nil {
		t.Fatalf("At(0): %v", err)
	}
	if atO.FilterID() != "filter.id" {
		t.Errorf("At(0).FilterID() = %q, want %q", atO.FilterID(), "filter.id")
	}

	// --- ByID() lookup ---
	byID, err := set.ByID("filter.id")
	if err != nil {
		t.Fatalf("ByID: %v", err)
	}
	if byID.FilterID() != "filter.id" {
		t.Errorf("ByID.FilterID() = %q, want %q", byID.FilterID(), "filter.id")
	}
}

// TestEvaluationSetStableOrderAcrossInputOrders verifies that regardless of
// the order the inputs are provided, the resulting EvaluationSet always
// orders outcomes by ascending stable-token id. This ensures Arbiter inputs
// do not depend on completion order.
func TestEvaluationSetStableOrderAcrossInputOrders(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)

	makeOutcome := func(id string) EpochFilterOutcome {
		d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", id, "rule.id", evidence, nil)
		oc, _ := NewFilterOutcomeEvaluated(d)
		o, _ := NewEpochFilterOutcome(id, 100, oc, FilterEnforcementBlocking)
		return o
	}

	// Generate a non-sorted order of ids.
	ids := []string{"z.id", "a.id", "m.id", "c.id", "y.id", "b.id"}
	// Sort them to know the expected order.
	sort.Strings(ids)

	// Build inputs in random order.
	inputs := make([]EpochFilterOutcome, len(ids))
	for i, id := range ids {
		inputs[i] = makeOutcome(id)
	}

	set, err := NewEvaluationSet(inputs)
	if err != nil {
		t.Fatalf("NewEvaluationSet: %v", err)
	}

	for i, wantID := range ids {
		o, err := set.At(i)
		if err != nil {
			t.Fatalf("At(%d): %v", i, err)
		}
		if o.FilterID() != wantID {
			t.Errorf("At(%d).FilterID() = %q, want %q", i, o.FilterID(), wantID)
		}
	}
}

// TestEpochFilterOutcomeNoRawErrorPreserved verifies that the outcome stored
// in EpochFilterOutcome does not contain raw error messages; only the
// sanitized stable error code is retained.
func TestEpochFilterOutcomeNoRawErrorPreserved(t *testing.T) {
	// Build an evaluation_error outcome with a stable error code.
	oc, _ := NewFilterOutcomeEvaluationError("runtime.guardrail.timeout")
	o, err := NewEpochFilterOutcome("f.id", 100, oc, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome: %v", err)
	}
	if o.Outcome().ErrorCode() != "runtime.guardrail.timeout" {
		t.Errorf("ErrorCode() = %q, want %q", o.Outcome().ErrorCode(), "runtime.guardrail.timeout")
	}
	if o.Outcome().Decision() != nil {
		t.Error("Evaluation error outcome must not carry a decision")
	}
}

// TestEpochFilterNormalizeOutcomeForReadyFilter verifies that a ready filter
// (subscribed + triggerReady) produces an empty FilterOutcome from
// NormalizeOutcome. The coordinator is responsible for wrapping the actual
// evaluation result.
func TestEpochFilterNormalizeOutcomeForReadyFilter(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")
	f, err := NewEpochFilter(
		&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
		true, true, false,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter: %v", err)
	}
	oc := f.NormalizeOutcome()
	// Ready filter has no pre-evaluation outcome; the kind is the zero value.
	if oc.Kind() != "" {
		t.Errorf("NormalizeOutcome().Kind() = %q, want empty (ready filter has no pre-evaluation outcome)", oc.Kind())
	}
}

// TestEvaluationSetEmptyIsValid verifies that an empty EvaluationSet is valid
// and that its accessors behave safely.
func TestEvaluationSetEmptyIsValid(t *testing.T) {
	set, err := NewEvaluationSet(nil)
	if err != nil {
		t.Fatalf("NewEvaluationSet(nil): %v", err)
	}
	if set.Len() != 0 {
		t.Errorf("Len() = %d, want 0", set.Len())
	}
	if set.All() != nil {
		t.Error("All() of empty set should return nil")
	}
	if _, err := set.At(0); err == nil {
		t.Error("At(0) on empty set should return error")
	}
	if _, err := set.ByID("any"); err == nil {
		t.Error("ByID on empty set should return error")
	}
}

// TestEpochFilterNoPartialConstructionLeak verifies that the filter
// contract is also invariant under concurrent access — the constructor does
// not produce a filter that can be observed in a partially constructed state.
// This is enforced by the immutable design of the returned value.
func TestEpochFilterNoPartialConstructionLeak(t *testing.T) {
	// The constructor validates all fields before returning. Since all
	// fields are stored in a struct value (not pointers to shared state),
	// a partially constructed EpochFilter can never be observed. This test
	// simply verifies that a fully valid filter has stable accessors.
	passDecision := newPassDecision(t, "filter.id")
	f, err := NewEpochFilter(
		&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
		true, true, false,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter: %v", err)
	}

	// All accessors should return consistent values.
	if f.ID() != "filter.id" {
		t.Errorf("ID() = %q, want %q", f.ID(), "filter.id")
	}
	if f.EnforcedAs() != FilterEnforcementBlocking {
		t.Errorf("EnforcedAs() = %v, want %v", f.EnforcedAs(), FilterEnforcementBlocking)
	}
	if !f.SubscribedEventPresent() {
		t.Error("SubscribedEventPresent() should be true")
	}
	if !f.TriggerReady() {
		t.Error("TriggerReady() should be true")
	}
	if !f.EvaluatedForEpoch() {
		t.Error("EvaluatedForEpoch() should be true for subscribed+triggered filter")
	}
}

// TestEpochFilterOutcomeValidateRejection verifies that EpochFilterOutcome.Validate()
// rejects entries with invalid filter ids, invalid outcomes, invalid
// enforcement, and invalid dispositions.
func TestEpochFilterOutcomeValidateRejection(t *testing.T) {
	// --- Empty filter id ---
	o := EpochFilterOutcome{
		filterID:           StableToken{},
		outcome:            NewFilterOutcomeNotApplicableForEpoch(),
		enforcement:        FilterEnforcementBlocking,
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := o.Validate(); err == nil {
		t.Error("Validate() with empty filter id should return error")
	}

	// --- Invalid filter id grammar ---
	badToken, _ := NewStableTokenRequired("filterID", "BAD-ID!")
	o2 := EpochFilterOutcome{
		filterID:           badToken,
		outcome:            NewFilterOutcomeNotApplicableForEpoch(),
		enforcement:        FilterEnforcementBlocking,
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := o2.Validate(); err == nil {
		t.Error("Validate() with bad id grammar should return error")
	}

	// --- Invalid outcome ---
	o3 := EpochFilterOutcome{
		filterID:           mustToken("filter.id"),
		outcome:            FilterOutcome{},
		enforcement:        FilterEnforcementBlocking,
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := o3.Validate(); err == nil {
		t.Error("Validate() with invalid outcome should return error")
	}

	// --- Invalid enforcement ---
	o4 := EpochFilterOutcome{
		filterID:           mustToken("filter.id"),
		outcome:            NewFilterOutcomeNotApplicableForEpoch(),
		enforcement:        "unknown",
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := o4.Validate(); err == nil {
		t.Error("Validate() with invalid enforcement should return error")
	}
}

// TestEpochFilterOutcomeStableTokenGrammar verifies that EpochFilterOutcome
// constructor enforces the stable-token grammar on the filter id.
func TestEpochFilterOutcomeStableTokenGrammar(t *testing.T) {
	goodOutcome := NewFilterOutcomeNotApplicableForEpoch()

	// --- Uppercase rejected ---
	_, err := NewEpochFilterOutcome("Filter.ID", 100, goodOutcome, FilterEnforcementBlocking)
	if err == nil {
		t.Error("NewEpochFilterOutcome(uppercase id) should return error")
	}

	// --- Special chars rejected ---
	_, err = NewEpochFilterOutcome("filter@id", 100, goodOutcome, FilterEnforcementBlocking)
	if err == nil {
		t.Error("NewEpochFilterOutcome(special chars id) should return error")
	}

	// --- Empty rejected ---
	_, err = NewEpochFilterOutcome("", 100, goodOutcome, FilterEnforcementBlocking)
	if err == nil {
		t.Error("NewEpochFilterOutcome(empty id) should return error")
	}

	// --- Valid grammar accepted ---
	_, err = NewEpochFilterOutcome("filter.id-01", 100, goodOutcome, FilterEnforcementBlocking)
	if err != nil {
		t.Errorf("NewEpochFilterOutcome(valid id) = %v, want nil", err)
	}
}

// TestEpochFilterPrioritySnapshotReadyAndNonReady verifies that EpochFilter
// preserves the priority value for both ready and non-ready paths, and that
// the priority is accessible via the Priority() accessor regardless of
// EvaluatedForEpoch() state.
func TestEpochFilterPrioritySnapshotReadyAndNonReady(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")

	// Ready path: subscribed + triggerReady.
	fReady, err := NewEpochFilter(
		&stubEvaluator{id: "ready.filter", decision: passDecision}, 42,
		true, true, false,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter ready: %v", err)
	}
	if fReady.Priority() != 42 {
		t.Errorf("Ready filter Priority() = %d, want 42", fReady.Priority())
	}
	if !fReady.EvaluatedForEpoch() {
		t.Error("Ready filter should be evaluated for epoch")
	}

	// Non-ready path (deferred): subscribed + trigger not ready + blocks release.
	fDeferred, err := NewEpochFilter(
		&stubEvaluator{id: "defer.filter", decision: passDecision}, 77,
		true, false, true,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter deferred: %v", err)
	}
	if fDeferred.Priority() != 77 {
		t.Errorf("Deferred filter Priority() = %d, want 77", fDeferred.Priority())
	}
	if fDeferred.EvaluatedForEpoch() {
		t.Error("Deferred filter should NOT be evaluated for epoch")
	}

	// Non-ready path (not_applicable): subscribed + trigger not ready + !blocksRelease.
	fNotApplicable, err := NewEpochFilter(
		&stubEvaluator{id: "na.filter", decision: passDecision}, 99,
		true, false, false,
		FilterEnforcementObserveOnly,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter not_applicable: %v", err)
	}
	if fNotApplicable.Priority() != 99 {
		t.Errorf("Not-applicable filter Priority() = %d, want 99", fNotApplicable.Priority())
	}
	if fNotApplicable.EvaluatedForEpoch() {
		t.Error("Not-applicable filter should NOT be evaluated for epoch")
	}

	// Unsubscribed path.
	fUnsub, err := NewEpochFilter(
		&stubEvaluator{id: "unsub.filter", decision: passDecision}, 5,
		false, false, false,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter unsubscribed: %v", err)
	}
	if fUnsub.Priority() != 5 {
		t.Errorf("Unsubscribed filter Priority() = %d, want 5", fUnsub.Priority())
	}
	if fUnsub.EvaluatedForEpoch() {
		t.Error("Unsubscribed filter should NOT be evaluated for epoch")
	}
}

// TestEpochFilterOutcomePrioritySnapshotReadyAndNonReady verifies that
// EpochFilterOutcome preserves the priority value for both evaluated and
// non-evaluated outcome kinds.
func TestEpochFilterOutcomePrioritySnapshotReadyAndNonReady(t *testing.T) {
	passDecision := newPassDecision(t, "f.ready")

	// Evaluated outcome with priority.
	ocEval, _ := NewFilterOutcomeEvaluated(passDecision)
	oEval, err := NewEpochFilterOutcome("f.ready", 150, ocEval, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome evaluated: %v", err)
	}
	if oEval.Priority() != 150 {
		t.Errorf("Evaluated outcome Priority() = %d, want 150", oEval.Priority())
	}

	// Not-applicable outcome with priority.
	ocNA := NewFilterOutcomeNotApplicableForEpoch()
	oNA, err := NewEpochFilterOutcome("f.na", 200, ocNA, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome not_applicable: %v", err)
	}
	if oNA.Priority() != 200 {
		t.Errorf("Not-applicable outcome Priority() = %d, want 200", oNA.Priority())
	}

	// Deferred outcome with priority.
	ocDeferred := NewFilterOutcomeDeferredByRequirement()
	oDeferred, err := NewEpochFilterOutcome("f.defer", 250, ocDeferred, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome deferred: %v", err)
	}
	if oDeferred.Priority() != 250 {
		t.Errorf("Deferred outcome Priority() = %d, want 250", oDeferred.Priority())
	}

	// Evaluation error outcome with priority.
	evalError, _ := NewFilterOutcomeEvaluationError("err.code")
	oErr, err := NewEpochFilterOutcome("f.err", 300, evalError, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome eval_error: %v", err)
	}
	if oErr.Priority() != 300 {
		t.Errorf("Eval-error outcome Priority() = %d, want 300", oErr.Priority())
	}
}

// TestResolvedFilterPriorityPreservedFromRegistry verifies that ResolvedFilter
// preserves the priority from the registration's effective policy for both
// ready and non-ready paths.
func TestResolvedFilterPriorityPreservedFromRegistry(t *testing.T) {
	ts := time.Second

	f := newMockFilter("f-priority")
	reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 55)
	_ = reg

	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
	rc := reqCtx("gen-1", "attempt-1", "staging")
	rsnap, _ := snap.BeginRequest(rc)

	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-test"})
	resolved, err := rsnap.ResolveAttempt(target)
	if err != nil {
		t.Fatalf("ResolveAttempt: %v", err)
	}
	if len(resolved) != 1 {
		t.Fatalf("expected 1 resolved, got %d", len(resolved))
	}

	rf := resolved[0]
	if rf.Priority() != 55 {
		t.Errorf("ResolvedFilter.Priority() = %d, want 55", rf.Priority())
	}

	// BindEpoch with ready=true should preserve the registry priority.
	appReady, _ := NewFilterApplicability(rf.FilterID(), true, true)
	efReady, err := rf.BindEpoch(1, appReady)
	if err != nil {
		t.Fatalf("BindEpoch ready: %v", err)
	}
	if efReady.Priority() != 55 {
		t.Errorf("BindEpoch ready Priority() = %d, want 55", efReady.Priority())
	}

	// BindEpoch with trigger not ready should also preserve.
	appDeferred, _ := NewFilterApplicability(rf.FilterID(), true, false)
	efDeferred, err := rf.BindEpoch(1, appDeferred)
	if err != nil {
		t.Fatalf("BindEpoch deferred: %v", err)
	}
	if efDeferred.Priority() != 55 {
		t.Errorf("BindEpoch deferred Priority() = %d, want 55", efDeferred.Priority())
	}

	// BindEpoch with not-applicable.
	appNotApp, _ := NewFilterApplicability(rf.FilterID(), true, false)
	efNotApp, err := rf.BindEpoch(1, appNotApp)
	if err != nil {
		t.Fatalf("BindEpoch not_applicable: %v", err)
	}
	if efNotApp.Priority() != 55 {
		t.Errorf("BindEpoch not_applicable Priority() = %d, want 55", efNotApp.Priority())
	}

	// BindEpoch with unsubscribed.
	appUnsub, _ := NewFilterApplicability(rf.FilterID(), false, false)
	efUnsub, err := rf.BindEpoch(1, appUnsub)
	if err != nil {
		t.Fatalf("BindEpoch unsubscribed: %v", err)
	}
	if efUnsub.Priority() != 55 {
		t.Errorf("BindEpoch unsubscribed Priority() = %d, want 55", efUnsub.Priority())
	}
}

// TestGateCoordinatorEvaluateReturnsArbiterResult verifies that GateCoordinator
// Evaluate returns the exact ArbitrationResult produced by the arbiter, and
// that Submit also returns the same result.
func TestGateCoordinatorEvaluateReturnsArbiterResult(t *testing.T) {
	batch := makeTestBatch(t)

	passDecision := makeTestPassDecision(t, "pass.filter")
	passEval := &stubEvaluator{id: "pass.filter", decision: passDecision}

	fPass := mustMakeEpochFilter(t, passEval, true, true, false, FilterEnforcementBlocking)

	// Custom arbiter that captures the result.
	var capturedResult ArbitrationResult
	capturingArb := &capturingArbiterForProp{
		capture: &capturedResult,
	}

	coord, cleanup := mustNewCoordinator(t, capturingArb, 0)
	defer cleanup()

	set, evalResult, err := coord.Evaluate(context.Background(), batch, []EpochFilter{fPass})
	if err != nil {
		t.Fatalf("Evaluate error: %v", err)
	}

	// Evaluate must return the same result the arbiter produced.
	if evalResult.Action() != capturedResult.Action() {
		t.Errorf("Evaluate result Action = %v, arbiter result Action = %v", evalResult.Action(), capturedResult.Action())
	}
	if evalResult.FilterID() != capturedResult.FilterID() {
		t.Errorf("Evaluate result FilterID = %s, arbiter result FilterID = %s", evalResult.FilterID(), capturedResult.FilterID())
	}
	if evalResult.RuleID() != capturedResult.RuleID() {
		t.Errorf("Evaluate result RuleID = %s, arbiter result RuleID = %s", evalResult.RuleID(), capturedResult.RuleID())
	}
	if evalResult.BaseDisposition() != capturedResult.BaseDisposition() {
		t.Errorf("Evaluate result BaseDisposition = %v, arbiter result BaseDisposition = %v", evalResult.BaseDisposition(), capturedResult.BaseDisposition())
	}

	// The set must be non-empty and consistent.
	if set.Len() == 0 {
		t.Error("Evaluate returned empty set")
	}

	// Submit must also return the same result.
	submitResult, err := coord.Submit(context.Background(), batch, []EpochFilter{fPass})
	if err != nil {
		t.Fatalf("Submit error: %v", err)
	}
	if submitResult.Action() != capturedResult.Action() {
		t.Errorf("Submit result Action = %v, expected %v", submitResult.Action(), capturedResult.Action())
	}
	if submitResult.FilterID() != capturedResult.FilterID() {
		t.Errorf("Submit result FilterID = %s, expected %s", submitResult.FilterID(), capturedResult.FilterID())
	}
	if submitResult.RuleID() != capturedResult.RuleID() {
		t.Errorf("Submit result RuleID = %s, expected %s", submitResult.RuleID(), capturedResult.RuleID())
	}
	if submitResult.BaseDisposition() != capturedResult.BaseDisposition() {
		t.Errorf("Submit result BaseDisposition = %v, expected %v", submitResult.BaseDisposition(), capturedResult.BaseDisposition())
	}
}

// violationReturningEvaluator is a FilterEvaluator that always returns a
// specific FilterDecision.
type violationReturningEvaluator struct {
	id       string
	decision FilterDecision
}

func (v *violationReturningEvaluator) ID() string { return v.id }
func (v *violationReturningEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
	return v.decision, nil
}

// capturingArbiterForProp is an EpochArbiter that captures the result for
// propagation verification tests.
type capturingArbiterForProp struct {
	capture *ArbitrationResult
}

func (c *capturingArbiterForProp) Arbitrate(_ context.Context, batch EvidenceBatch, set EvaluationSet) (ArbitrationResult, error) {
	baseDisp, _ := batch.BaseDisposition()
	res, err := NewArbitrationResult(ArbitrationActionRelease, baseDisp, "", "", nil, nil)
	if err != nil {
		return ArbitrationResult{}, err
	}
	if c.capture != nil {
		*c.capture = res
	}
	return res, nil
}

// ensure unused imports are consumed.
var _ = context.Background
var _ = sort.Strings

// TestEpochFilterSnapshotsEvaluatorID verifies that EpochFilter.ID() returns
// the evaluator id snapshot taken at construction time, not the evaluator's
// current id after mutation.
func TestEpochFilterSnapshotsEvaluatorID(t *testing.T) {
	passDecision := newPassDecision(t, "filter.id")

	mutable := &mutableIDEvaluator{id: "filter.id", decision: passDecision}
	f, err := NewEpochFilter(
		mutable, 100,
		true, true, false,
		FilterEnforcementBlocking,
		100*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("NewEpochFilter: %v", err)
	}

	// Snapshot must match the id at construction time.
	if got := f.ID(); got != "filter.id" {
		t.Errorf("ID() = %q, want %q", got, "filter.id")
	}

	// Mutate the evaluator id.
	mutable.id = "mutated.id"

	// ID() must still return the original snapshot.
	if got := f.ID(); got != "filter.id" {
		t.Errorf("ID() after mutation = %q, want %q", got, "filter.id")
	}

	// Evaluator().ID() must reflect the mutation (seam is preserved).
	if got := f.Evaluator().ID(); got != "mutated.id" {
		t.Errorf("Evaluator().ID() = %q, want %q", got, "mutated.id")
	}
}

// mutableIDEvaluator is a FilterEvaluator whose id can be changed at runtime
// to test that EpochFilter snapshot is not affected by evaluator mutation.
type mutableIDEvaluator struct {
	id       string
	decision FilterDecision
}

func (m *mutableIDEvaluator) ID() string { return m.id }
func (m *mutableIDEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
	return m.decision, nil
}

// TestEpochFilterOutcomeRejectsDecisionIDMismatch verifies that NewEpochFilterOutcome
// rejects an evaluated outcome whose decision carries a different filter id
// than the constructor's wrapper filter id.
func TestEpochFilterOutcomeRejectsDecisionIDMismatch(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)

	// Build a decision with a different filter id from the wrapper.
	d, err := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "other.id", "rule.id", evidence, nil)
	if err != nil {
		t.Fatalf("NewFilterDecision: %v", err)
	}

	_, err = NewEpochFilterOutcome("wrapper.id", 100, mustEvalOutcome(t, d), FilterEnforcementBlocking)
	if err == nil {
		t.Error("NewEpochFilterOutcome(mismatched decision) should return error")
	} else if !strings.Contains(err.Error(), "filter id mismatch") {
		t.Errorf("error = %v, want to contain 'filter id mismatch'", err)
	}
}

// TestEvaluationSetRejectsForgedFailureDispositionMismatch verifies that a package-local
// forged EpochFilterOutcome (constructed directly without NewEpochFilterOutcome)
// whose failure disposition does not match the outcome kind and enforcement is
// rejected by both Validate() and NewEvaluationSet. Covers representative
// cases from the evaluated, evaluation-error, not-applicable, and deferred families.
func TestEvaluationSetRejectsForgedFailureDispositionMismatch(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)

	forgedToken, err := NewStableTokenRequired("filterID", "forged.id")
	if err != nil {
		t.Fatalf("NewStableTokenRequired: %v", err)
	}

	// Case 1: evaluated pass + blocking → should be none, but forged as blocking_fatal.
	t.Run("evaluated_pass_with_blocking_fatal", func(t *testing.T) {
		d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "forged.id", "rule.id", evidence, nil)
		oc, _ := NewFilterOutcomeEvaluated(d)
		forged := EpochFilterOutcome{
			filterID:           forgedToken,
			outcome:            oc,
			enforcement:        FilterEnforcementBlocking,
			failureDisposition: EvaluationFailureDispositionBlockingFatal,
		}
		if err := forged.Validate(); err == nil {
			t.Error("Validate() should reject forged evaluated+pass with blocking_fatal disposition")
		} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
			t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
		}
		if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
			t.Error("NewEvaluationSet should reject forged evaluated+pass with blocking_fatal")
		}
	})

	// Case 2: evaluated violation + observe_only → should be observe_error, but forged as blocking_fatal.
	t.Run("evaluated_violation_with_observe_blocking_fatal", func(t *testing.T) {
		d, _ := NewFilterDecision(FilterDecisionKindViolation, "consumer.id", "forged.id", "rule.id", evidence, nil)
		oc, _ := NewFilterOutcomeEvaluated(d)
		forged := EpochFilterOutcome{
			filterID:           forgedToken,
			outcome:            oc,
			enforcement:        FilterEnforcementObserveOnly,
			failureDisposition: EvaluationFailureDispositionBlockingFatal,
		}
		if err := forged.Validate(); err == nil {
			t.Error("Validate() should reject forged evaluated+violation with blocking_fatal disposition under observe_only")
		} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
			t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
		}
		if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
			t.Error("NewEvaluationSet should reject forged evaluated+violation with blocking_fatal under observe_only")
		}
	})

	// Case 3: evaluation_error + blocking → should be blocking_fatal, but forged as observe_error.
	t.Run("eval_error_with_observe_error", func(t *testing.T) {
		eoc := mustEvalError(t, "err.code")
		forged := EpochFilterOutcome{
			filterID:           forgedToken,
			outcome:            eoc,
			enforcement:        FilterEnforcementBlocking,
			failureDisposition: EvaluationFailureDispositionObserveError,
		}
		if err := forged.Validate(); err == nil {
			t.Error("Validate() should reject forged eval_error with observe_error under blocking")
		} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
			t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
		}
		if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
			t.Error("NewEvaluationSet should reject forged eval_error with observe_error under blocking")
		}
	})

	// Case 4: not_applicable → should be none, but forged as blocking_fatal.
	t.Run("not_applicable_with_blocking_fatal", func(t *testing.T) {
		oc := NewFilterOutcomeNotApplicableForEpoch()
		forged := EpochFilterOutcome{
			filterID:           forgedToken,
			outcome:            oc,
			enforcement:        FilterEnforcementBlocking,
			failureDisposition: EvaluationFailureDispositionBlockingFatal,
		}
		if err := forged.Validate(); err == nil {
			t.Error("Validate() should reject forged not_applicable with blocking_fatal disposition")
		} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
			t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
		}
		if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
			t.Error("NewEvaluationSet should reject forged not_applicable with blocking_fatal")
		}
	})

	// Case 5: deferred → should be none, but forged as observe_error.
	t.Run("deferred_with_observe_error", func(t *testing.T) {
		oc := NewFilterOutcomeDeferredByRequirement()
		forged := EpochFilterOutcome{
			filterID:           forgedToken,
			outcome:            oc,
			enforcement:        FilterEnforcementObserveOnly,
			failureDisposition: EvaluationFailureDispositionObserveError,
		}
		if err := forged.Validate(); err == nil {
			t.Error("Validate() should reject forged deferred with observe_error disposition")
		} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
			t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
		}
		if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
			t.Error("NewEvaluationSet should reject forged deferred with observe_error")
		}
	})
}

// TestEvaluationSetRejectsForgedDecisionIDMismatch verifies that a package-local
// forged EpochFilterOutcome (constructed directly without NewEpochFilterOutcome)
// whose wrapper filter id does not match the evaluated decision's filter id is
// rejected by both Validate() and NewEvaluationSet(). This is the set-boundary
// regression complement to TestEpochFilterOutcomeRejectsDecisionIDMismatch,
// which validates the constructor boundary.
func TestEvaluationSetRejectsForgedDecisionIDMismatch(t *testing.T) {
	forged := EpochFilterOutcome{
		filterID:           mustToken("wrapper.id"),
		outcome:            mustEvalOutcome(t, newPassDecision(t, "other.id")),
		enforcement:        FilterEnforcementBlocking,
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := forged.Validate(); err == nil {
		t.Fatal("Validate() should reject forged decision id mismatch")
	} else if !strings.Contains(err.Error(), "filter id mismatch") {
		t.Errorf("Validate error = %v, want to contain 'filter id mismatch'", err)
	}
	if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
		t.Fatal("NewEvaluationSet should reject forged decision id mismatch")
	}
}

// mustEvalOutcome wraps a FilterDecision into a FilterOutcome of kind
// evaluated, failing the test on any error.
func mustEvalOutcome(t *testing.T, d FilterDecision) FilterOutcome {
	t.Helper()
	o, err := NewFilterOutcomeEvaluated(d)
	if err != nil {
		t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
	}
	return o
}

// ensure unused imports are consumed.
var _ = context.Background
var _ = sort.Strings
