package streamgate

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

type mockFilter struct {
	id         string
	muID       bool
	appliesFn  func(FilterContext) bool
	holdReqFn  func(FilterContext) FilterHoldRequirement
	evaluateFn func(context.Context, FilterContext, EvidenceBatch) (FilterDecision, error)
}

func newMockFilter(id string) *mockFilter { return &mockFilter{id: id} }
func (m *mockFilter) SetID(id string)     { m.id = id }
func (m *mockFilter) ID() string          { return m.id }

func (m *mockFilter) Applies(c FilterContext) bool {
	if m.appliesFn != nil {
		return m.appliesFn(c)
	}
	return true
}

func (m *mockFilter) HoldRequirement(c FilterContext) FilterHoldRequirement {
	if m.holdReqFn != nil {
		return m.holdReqFn(c)
	}
	return mustHoldReqNone("test-channel")
}

func (m *mockFilter) Evaluate(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
	if m.evaluateFn != nil {
		return m.evaluateFn(ctx, fc, batch)
	}
	return fixedDecisionFor(m.id), nil
}

func mustHoldReqNone(channel string) FilterHoldRequirement {
	req, err := NewFilterHoldRequirementNone(channel, []EventKind{EventKindTextDelta})
	if err != nil {
		panic(err)
	}
	return req
}

func mustHoldReqRolling(channel string, evidenceRunes int) FilterHoldRequirement {
	req, err := NewFilterHoldRequirementRolling(channel, []EventKind{EventKindTextDelta}, evidenceRunes)
	if err != nil {
		panic(err)
	}
	return req
}

type capturedContext struct {
	configGeneration  string
	attemptID         string
	epochID           uint64
	hasEpoch          bool
	environment       string
	modelGroup        string
	actualModel       string
	actualProvider    string
	executionPath     string
	streamID          string
	commitState       CommitState
	hasToolSideEffect bool
	terminal          bool
	endpoint          string
	family            string
	stableCorrelation string
}

func captureContext(fc FilterContext) capturedContext {
	return capturedContext{
		configGeneration: fc.ConfigGeneration(), attemptID: fc.AttemptID(),
		epochID: fc.EpochID(), hasEpoch: fc.HasEpoch(),
		environment: fc.Environment(), modelGroup: fc.ModelGroup(),
		actualModel: fc.ActualModel(), actualProvider: fc.ActualProvider(),
		executionPath: fc.ExecutionPath(), streamID: fc.StreamID(),
		commitState: fc.CommitState(), hasToolSideEffect: fc.HasToolSideEffect(),
		terminal: fc.Terminal(),
		endpoint: fc.Endpoint(), family: fc.Family(),
		stableCorrelation: fc.StableCorrelation(),
	}
}

func fixedDecisionFor(filterID string) FilterDecision {
	fp := mustFixedFingerprint()
	ev, err := NewSanitizedEvidence(EventKindTextDelta, "text", "rule1", "desc-1", fp, 1, 0, FilterOutcomeKindEvaluated, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
	if err != nil {
		panic(err)
	}
	d, err := NewFilterDecision(FilterDecisionKindPass, "consumer1", filterID, "rule1", ev, nil)
	if err != nil {
		panic(err)
	}
	return d
}

func fixedDecision() FilterDecision { return fixedDecisionFor("f-1") }

func mustFixedFingerprint() FixedFingerprint { var fp FixedFingerprint; fp[0] = 1; return fp }

func makeBatch() EvidenceBatch {
	ts := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	ev, err := NewTextDeltaEvent("ch-input", "hello", ts)
	if err != nil {
		panic(err)
	}
	b, err := NewEvidenceBatch([]NormalizedEvent{ev}, map[string][]NormalizedEvent{}, map[string][]NormalizedEvent{}, nil, false, CommitStateTransportUncommitted, ts)
	if err != nil {
		panic(err)
	}
	return b
}

func reqCtx(gen, attempt, env string) RequestFilterContext {
	rc, _ := NewRequestFilterContext(gen, attempt, env, "ep", "fam", "stream-1", CommitStateTransportUncommitted, false, false, "corr-1")
	return rc
}

func mustPolicy(fID string, st PolicySelectorType, key string, enabled bool, enf FilterEnforcement, timeout time.Duration, priority int) FilterPolicyLayer {
	p, err := NewFilterPolicyLayer(fID, st, key, enabled, enf, timeout, priority)
	if err != nil {
		panic(err)
	}
	return p
}

// --- API-1 tests ---

func TestFilterContextIsImmutable(t *testing.T) {
	base, err := NewFilterContextBuilder("gen-1", "attempt-1").
		SetEnvironment("staging").SetEndpoint("ep").SetFamily("fam").
		SetModelGroup("mg").SetActualModel("model-a").SetActualProvider("prov-x").
		SetExecutionPath("primary").SetStream("stream-1").
		SetCommitState(CommitStateTransportUncommitted).SetTerminal(false).
		SetHasToolSideEffect(true).SetStableCorrelation("corr-1").Build()
	if err != nil {
		t.Fatalf("build: %v", err)
	}
	if base.ConfigGeneration() != "gen-1" {
		t.Errorf("ConfigGeneration wrong")
	}
	if base.HasEpoch() {
		t.Errorf("HasEpoch should be false")
	}
	if base.Endpoint() != "ep" {
		t.Errorf("Endpoint wrong")
	}
	if base.Family() != "fam" {
		t.Errorf("Family wrong")
	}
	if base.StreamID() != "stream-1" {
		t.Errorf("StreamID wrong")
	}
	if base.StableCorrelation() != "corr-1" {
		t.Errorf("StableCorrelation wrong")
	}
	if base.HasToolSideEffect() != true {
		t.Errorf("HasToolSideEffect wrong")
	}
	_ = base.CommitState()
	epochB, err := base.WithEpoch(42)
	if err != nil {
		t.Fatalf("WithEpoch: %v", err)
	}
	if !epochB.HasEpoch() {
		t.Errorf("epochB should have epoch")
	}
	if epochB.EpochID() != 42 {
		t.Errorf("epochB EpochID = %d", epochB.EpochID())
	}
	if base.HasEpoch() {
		t.Errorf("WithEpoch mutated base")
	}
	_, err = epochB.WithEpoch(99)
	if err != nil {
		t.Fatalf("WithEpoch on epochB: %v", err)
	}
	if epochB.EpochID() != 42 {
		t.Errorf("WithEpoch mutated epochB")
	}
}

// TestFilterRegistryCandidateScopedActiveResolution verifies that provider-
// specific filter capabilities and Applies logic are evaluated per candidate
// target. A provider-a filter with cap-provider-a should only be eligible
// for candidates that have cap-provider-a, and provider-specific Applies
// should gate resolution correctly.
func TestFilterRegistryCandidateScopedActiveResolution(t *testing.T) {
	ts := time.Second

	// Provider-A filter: applies only to prov-a, requires cap-provider-a.
	fA := newMockFilter("f-prov-a")
	fA.appliesFn = func(c FilterContext) bool { return c.ActualProvider() == "prov-a" }
	regA, _ := NewFilterRegistration(fA, "cap-provider-a", true, FilterEnforcementBlocking, ts, 10)

	// Provider-B filter: applies only to prov-b, requires cap-provider-b.
	fB := newMockFilter("f-prov-b")
	fB.appliesFn = func(c FilterContext) bool { return c.ActualProvider() == "prov-b" }
	regB, _ := NewFilterRegistration(fB, "cap-provider-b", true, FilterEnforcementBlocking, ts, 10)

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

	t.Run("prov_a_candidate_needs_cap_provider_a", func(t *testing.T) {
		caps, err := rsnap.RequiredFilterCapabilities(mustTarget("mg", "model-a", "prov-a", "primary", "cap-provider-a"))
		if err != nil {
			t.Fatalf("RequiredFilterCapabilities: %v", err)
		}
		if len(caps) != 1 || caps[0] != "cap-provider-a" {
			t.Fatalf("expected [cap-provider-a], got %v", caps)
		}
	})

	t.Run("prov_b_candidate_needs_cap_provider_b", func(t *testing.T) {
		caps, err := rsnap.RequiredFilterCapabilities(mustTarget("mg", "model-b", "prov-b", "primary", "cap-provider-b"))
		if err != nil {
			t.Fatalf("RequiredFilterCapabilities: %v", err)
		}
		if len(caps) != 1 || caps[0] != "cap-provider-b" {
			t.Fatalf("expected [cap-provider-b], got %v", caps)
		}
	})

	t.Run("missing_capability_candidate_excluded", func(t *testing.T) {
		// Candidate with prov-a but missing cap-provider-a.
		cand, _ := NewAttemptTarget("mg", "model-a", "prov-a", "primary", []string{"cap-provider-b"})
		_, err := rsnap.EligibleCandidates([]AttemptTarget{cand})
		if !errors.Is(err, ErrNoEligibleCandidate) {
			t.Fatalf("expected ErrNoEligibleCandidate, got %v", err)
		}
	})

	t.Run("both_candidates_with_correct_caps_eligible", func(t *testing.T) {
		candA, _ := NewAttemptTarget("mg", "model-a", "prov-a", "primary", []string{"cap-provider-a"})
		candB, _ := NewAttemptTarget("mg", "model-b", "prov-b", "primary", []string{"cap-provider-b"})
		eligible, err := rsnap.EligibleCandidates([]AttemptTarget{candA, candB})
		if err != nil {
			t.Fatalf("EligibleCandidates: %v", err)
		}
		if len(eligible) != 2 {
			t.Fatalf("expected 2 eligible, got %d", len(eligible))
		}
	})

	t.Run("resolve_returns_provider_scoped_filters", func(t *testing.T) {
		target, _ := NewAttemptTarget("mg", "model-a", "prov-a", "primary", []string{"cap-provider-a"})
		resolved, err := rsnap.ResolveAttempt(target)
		if err != nil {
			t.Fatalf("ResolveAttempt: %v", err)
		}
		if len(resolved) != 1 || resolved[0].FilterID() != "f-prov-a" {
			t.Fatalf("expected [f-prov-a], got %d/%s", len(resolved), filterIDs(resolved))
		}
		ctx := resolved[0].FilterContextForBindEpoch()
		if ctx.ActualProvider() != "prov-a" {
			t.Errorf("provider = %q", ctx.ActualProvider())
		}
	})
}

// TestFilterRegistryDisabledFilterSkipsContractCalls verifies that disabled
// filters do not have Applies or HoldRequirement called, and that an invalid
// HoldRequirement on a disabled filter does not cause request or attempt
// failure.
func TestFilterRegistryDisabledFilterSkipsContractCalls(t *testing.T) {
	ts := time.Second

	// Disabled filter: has invalid HoldRequirement (empty), but is disabled
	// via policy so Applies/HoldRequirement must never be called.
	fD := newMockFilter("f-disabled")
	fD.holdReqFn = func(FilterContext) FilterHoldRequirement {
		t.Error("HoldRequirement must not be called for disabled filter")
		return FilterHoldRequirement{}
	}
	fD.appliesFn = func(FilterContext) bool {
		t.Error("Applies must not be called for disabled filter")
		return true
	}
	regD, _ := NewFilterRegistration(fD, "cap-disabled", true, FilterEnforcementBlocking, ts, 10)

	// Enabled filter with valid hold.
	fE := newMockFilter("f-enabled")
	fE.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch-enabled") }
	regE, _ := NewFilterRegistration(fE, "cap-enabled", true, FilterEnforcementObserveOnly, ts, 20)

	pDisabled := mustPolicy("f-disabled", PolicySelectorDefault, "", false, FilterEnforcementObserveOnly, ts, 0)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{regD, regE}, []FilterPolicyLayer{pDisabled})
	rc := reqCtx("gen-1", "attempt-1", "staging")

	// BeginRequest must not fail even though f-disabled has invalid HoldRequirement.
	rsnap, err := snap.BeginRequest(rc)
	if err != nil {
		t.Fatalf("BeginRequest: %v", err)
	}

	// ResolveAttempt must not fail either.
	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-enabled"})
	resolved, err := rsnap.ResolveAttempt(target)
	if err != nil {
		t.Fatalf("ResolveAttempt: %v", err)
	}
	if len(resolved) != 1 || resolved[0].FilterID() != "f-enabled" {
		t.Fatalf("expected [f-enabled], got %d/%s", len(resolved), filterIDs(resolved))
	}
}

// TestFilterRegistryRejectsIdentityMutationAtBoundaries verifies that
// ErrRegistrationFilterIDMismatch is returned at every boundary:
// snapshot construction, BeginRequest, ResolveAttempt, and BindEpoch.
func TestFilterRegistryRejectsIdentityMutationAtBoundaries(t *testing.T) {
	ts := time.Second

	t.Run("snapshot_construction", func(t *testing.T) {
		f := newMockFilter("f-stable")
		f.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch") }
		reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 1)
		f.SetID("f-mutated")
		_, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
		if !errors.Is(err, ErrRegistrationFilterIDMismatch) {
			t.Fatalf("expected ErrRegistrationFilterIDMismatch, got %v", err)
		}
	})

	t.Run("begin_request", func(t *testing.T) {
		f := newMockFilter("f-stable")
		f.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch") }
		reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 1)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
		f.SetID("f-mutated")
		_, err := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		if !errors.Is(err, ErrRegistrationFilterIDMismatch) {
			t.Fatalf("expected ErrRegistrationFilterIDMismatch, got %v", err)
		}
	})

	t.Run("resolve_attempt", func(t *testing.T) {
		f := newMockFilter("f-stable")
		f.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch") }
		reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 1)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		f.SetID("f-mutated")
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-test"})
		_, err := rsnap.ResolveAttempt(target)
		if !errors.Is(err, ErrRegistrationFilterIDMismatch) {
			t.Fatalf("expected ErrRegistrationFilterIDMismatch, got %v", err)
		}
	})

	t.Run("bind_epoch", func(t *testing.T) {
		f := newMockFilter("f-stable")
		f.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch") }
		reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 1)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-test"})
		resolved, _ := rsnap.ResolveAttempt(target)
		f.SetID("f-mutated")
		app, _ := NewFilterApplicability("f-stable", true, true)
		_, err := resolved[0].BindEpoch(1, app)
		if !errors.Is(err, ErrRegistrationFilterIDMismatch) {
			t.Fatalf("expected ErrRegistrationFilterIDMismatch, got %v", err)
		}
	})
}

// TestFilterRegistryUsesValidatedHoldRequirement verifies that validated
// HoldRequirement fields are preserved in the ResolvedFilter and that an
// invalid HoldRequirement on an enabled filter causes rejection.
func TestFilterRegistryUsesValidatedHoldRequirement(t *testing.T) {
	ts := time.Second
	fV := newMockFilter("f-valid")
	fV.holdReqFn = func(FilterContext) FilterHoldRequirement {
		req, _ := NewFilterHoldRequirementRollingWithMaxBuffer("ch1", []EventKind{EventKindTextDelta}, 100, 200)
		return req
	}
	regV, _ := NewFilterRegistration(fV, "cap-rolling", true, FilterEnforcementBlocking, ts, 1)
	snapV, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{regV}, nil)
	if err != nil {
		t.Fatalf("snapValid: %v", err)
	}
	rc := reqCtx("gen-1", "attempt-1", "staging")
	rsnap, err := snapV.BeginRequest(rc)
	if err != nil {
		t.Fatalf("BeginRequest valid: %v", err)
	}
	if rsnap.Generation() != "gen-1" {
		t.Errorf("Generation = %q", rsnap.Generation())
	}

	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-rolling"})
	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]
	req := rf.HoldRequirement()
	if req.Channel() != "ch1" {
		t.Errorf("channel = %q", req.Channel())
	}
	if req.Mode() != FilterHoldModeRolling {
		t.Errorf("mode = %q", req.Mode())
	}
	if req.EvidenceRunes() != 100 {
		t.Errorf("evidenceRunes = %d", req.EvidenceRunes())
	}
	if req.MaxBufferRunes() != 200 {
		t.Errorf("maxBufferRunes = %d", req.MaxBufferRunes())
	}
	if len(req.SubscribedKinds()) != 1 || req.SubscribedKinds()[0] != EventKindTextDelta {
		t.Errorf("subscribedKinds = %v", req.SubscribedKinds())
	}

	// Invalid hold requirement on an enabled filter should reject at ResolveAttempt.
	fI := newMockFilter("f-invalid")
	fI.holdReqFn = func(FilterContext) FilterHoldRequirement { return FilterHoldRequirement{} }
	regI, _ := NewFilterRegistration(fI, "cap-invalid", true, FilterEnforcementObserveOnly, ts, 1)
	snapI, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{regI}, nil)
	rsnapI, err := snapI.BeginRequest(rc)
	if err != nil {
		t.Fatalf("BeginRequest: %v", err)
	}
	targetI, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-invalid"})
	if _, err := rsnapI.ResolveAttempt(targetI); err == nil {
		t.Errorf("expected rejection of invalid requirement at ResolveAttempt")
	}
}

func TestFilterBaseDoesNotOwnRuntimePolicy(t *testing.T) {
	b, _ := NewFilterBase("fb-test")
	if b.ID() != "fb-test" {
		t.Errorf("ID = %q", b.ID())
	}
	if b.StableTokenValue().String() != "fb-test" {
		t.Errorf("Token = %q", b.StableTokenValue().String())
	}
}

// --- API-2 tests ---

func TestFilterRegistryRequestSnapshotIsolatesReload(t *testing.T) {
	f1 := newMockFilter("f-1")
	reg1, _ := NewFilterRegistration(f1, "cap-observe", true, FilterEnforcementBlocking, time.Second, 1)
	snap1, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg1}, nil)
	rc1 := reqCtx("gen-1", "attempt-1", "staging")
	rsnap1, _ := snap1.BeginRequest(rc1)
	f2 := newMockFilter("f-2")
	reg2, _ := NewFilterRegistration(f2, "cap-observe", true, FilterEnforcementBlocking, time.Second, 1)
	snap2, _ := NewFilterRegistrySnapshot("gen-2", []FilterRegistration{reg2}, nil)
	if rsnap1.Generation() != "gen-1" {
		t.Errorf("gen1 = %q", rsnap1.Generation())
	}
	rc2 := reqCtx("gen-2", "attempt-1", "staging")
	rsnap2, _ := snap2.BeginRequest(rc2)
	if rsnap2.Generation() != "gen-2" {
		t.Errorf("gen2 = %q", rsnap2.Generation())
	}
}

// TestFilterRegistryPolicyIsFilterScopedAndSnapshotImmutable verifies that
// mutating the caller-owned policy slice after snapshot construction does
// not affect the snapshot's resolution results.
func TestFilterRegistryPolicyIsFilterScopedAndSnapshotImmutable(t *testing.T) {
	ts := time.Second
	f1 := newMockFilter("f-1")
	f2 := newMockFilter("f-2")
	reg1, _ := NewFilterRegistration(f1, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
	reg2, _ := NewFilterRegistration(f2, "cap-observe", true, FilterEnforcementObserveOnly, ts, 20)
	p1 := mustPolicy("f-1", PolicySelectorDefault, "", false, FilterEnforcementObserveOnly, ts, 50)
	p2 := mustPolicy("f-2", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 60)
	policies := []FilterPolicyLayer{p1, p2}
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg1, reg2}, policies)
	rc := reqCtx("gen-1", "attempt-1", "staging")
	rsnap, _ := snap.BeginRequest(rc)
	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})

	// First resolution: f-2 only (f-1 is disabled by p1), f-2 enabled.
	resolved, _ := rsnap.ResolveAttempt(target)
	if len(resolved) != 1 || resolved[0].FilterID() != "f-2" {
		t.Fatalf("expected [f-2], got %d items", len(resolved))
	}
	if !resolved[0].Enabled() {
		t.Errorf("f-2 should be enabled by original policy")
	}

	// Mutate the actual slice elements passed to the snapshot.
	replacement1 := mustPolicy("f-1", PolicySelectorDefault, "", false, FilterEnforcementBlocking, ts, 99)
	replacement2 := mustPolicy("f-2", PolicySelectorDefault, "", false, FilterEnforcementObserveOnly, ts, 1)
	policies[0] = replacement1
	policies[1] = replacement2

	// Second resolution with same snapshot should still reflect old policy.
	rsnap2, _ := snap.BeginRequest(rc)
	resolved2, _ := rsnap2.ResolveAttempt(target)
	if len(resolved2) != 1 || resolved2[0].FilterID() != "f-2" {
		t.Fatalf("post-mutation: expected [f-2], got %d/%s", len(resolved2), filterIDs(resolved2))
	}
	if !resolved2[0].Enabled() {
		t.Errorf("f-2 should still be enabled by original snapshot policy")
	}

	// A brand new snapshot from the mutated slice reflects the new policy.
	snap3, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg1, reg2}, policies)
	rsnap3, _ := snap3.BeginRequest(rc)
	resolved3, _ := rsnap3.ResolveAttempt(target)
	if len(resolved3) != 0 {
		t.Errorf("with both disabled in new snapshot, expected empty resolved, got %d", len(resolved3))
	}
}

func TestFilterRegistryRejectsDuplicatePolicySelector(t *testing.T) {
	ts := time.Second
	f := newMockFilter("f-1")
	reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
	p1 := mustPolicy("f-1", PolicySelectorModel, "model-a", true, FilterEnforcementBlocking, ts, 10)
	p2 := mustPolicy("f-1", PolicySelectorModel, "model-a", false, FilterEnforcementObserveOnly, 2*ts, 20)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{p1, p2}); err == nil {
		t.Errorf("expected dup rejection")
	}
	p3 := mustPolicy("f-1", PolicySelectorModel, "model-b", true, FilterEnforcementBlocking, ts, 10)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{p1, p3}); err != nil {
		t.Errorf("different keys: %v", err)
	}
	f2 := newMockFilter("f-2")
	reg2, _ := NewFilterRegistration(f2, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
	p4 := mustPolicy("f-2", PolicySelectorModel, "model-a", true, FilterEnforcementBlocking, ts, 10)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg, reg2}, []FilterPolicyLayer{p1, p4}); err != nil {
		t.Errorf("different filters: %v", err)
	}
	pBad := mustPolicy("unknown-filter", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 10)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{pBad}); err == nil {
		t.Errorf("expected unknown id rejection")
	}
	pBad2, _ := NewFilterPolicyLayer("f-1", PolicySelectorDefault, "non-empty", true, FilterEnforcementBlocking, ts, 10)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{pBad2}); err == nil {
		t.Errorf("expected default non-empty key rejection")
	}
	pBad3, _ := NewFilterPolicyLayer("f-1", PolicySelectorModel, "", true, FilterEnforcementBlocking, ts, 10)
	if _, err := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{pBad3}); err == nil {
		t.Errorf("expected non-default empty key rejection")
	}
}

func TestFilterRegistryRejectsRequestGenerationMismatch(t *testing.T) {
	f := newMockFilter("f-1")
	reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, time.Second, 1)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
	badRC, _ := NewRequestFilterContext("gen-999", "attempt-1", "staging", "ep", "fam", "stream-1", CommitStateTransportUncommitted, false, false, "corr-1")
	if _, err := snap.BeginRequest(badRC); err == nil {
		t.Errorf("expected generation mismatch")
	}
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	if rsnap.Generation() != "gen-1" {
		t.Errorf("matching gen should succeed")
	}
}

func TestFilterRegistryPolicyPrecedence(t *testing.T) {
	ts := time.Second
	t.Run("default_only", func(t *testing.T) {
		f := newMockFilter("f-1")
		reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{mustPolicy("f-1", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 10)})
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})
		resolved, _ := rsnap.ResolveAttempt(target)
		rf := resolved[0]
		if rf.Enabled() != true || rf.Enforcement() != FilterEnforcementBlocking || rf.Timeout() != ts {
			t.Errorf("unexpected: %+v", rf)
		}
	})
	t.Run("env_overrides_default_resolve", func(t *testing.T) {
		f := newMockFilter("f-1")
		f.appliesFn = func(c FilterContext) bool { return c.Environment() != "dev" }
		reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{
			mustPolicy("f-1", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 10),
			mustPolicy("f-1", PolicySelectorEnvironment, "staging", false, FilterEnforcementObserveOnly, 2*ts, 20),
		})
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		// Disabled at target level means filter not in resolved set.
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})
		resolved, _ := rsnap.ResolveAttempt(target)
		if len(resolved) != 0 {
			t.Errorf("expected empty resolved at target (disabled), got %d", len(resolved))
		}
	})
	t.Run("model_overrides_env", func(t *testing.T) {
		f := newMockFilter("f-1")
		f.appliesFn = func(c FilterContext) bool { return c.ActualModel() != "ignored" }
		reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{
			mustPolicy("f-1", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 10),
			mustPolicy("f-1", PolicySelectorEnvironment, "staging", false, FilterEnforcementObserveOnly, 2*ts, 20),
			mustPolicy("f-1", PolicySelectorModel, "model-a", true, FilterEnforcementBlocking, 3*ts, 30),
		})
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})
		resolved, _ := rsnap.ResolveAttempt(target)
		rf := resolved[0]
		if rf.Enabled() != true || rf.Timeout() != 3*ts {
			t.Errorf("unexpected: %+v", rf)
		}
	})
	t.Run("provider_overrides_model_resolve", func(t *testing.T) {
		f := newMockFilter("f-1")
		f.appliesFn = func(c FilterContext) bool { return c.ActualProvider() != "ignored" }
		reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
		snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, []FilterPolicyLayer{
			mustPolicy("f-1", PolicySelectorDefault, "", true, FilterEnforcementBlocking, ts, 10),
			mustPolicy("f-1", PolicySelectorProvider, "prov-x", false, FilterEnforcementObserveOnly, 4*ts, 40),
		})
		rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
		// Provider override disables the filter at the target level.
		target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})
		resolved, _ := rsnap.ResolveAttempt(target)
		if len(resolved) != 0 {
			t.Errorf("expected empty resolved at target (provider disabled), got %d", len(resolved))
		}
		// RequiredFilterCapabilities also reflects the disabling.
		caps, _ := rsnap.RequiredFilterCapabilities(target)
		if len(caps) != 0 {
			t.Errorf("expected 0 required caps (provider disabled), got %d", len(caps))
		}
	})
}

// TestFilterRegistryReresolvesProviderSpecificApplies verifies that provider-
// specific Applies functions are evaluated per target and produce different
// resolved sets for different providers.
func TestFilterRegistryReresolvesProviderSpecificApplies(t *testing.T) {
	ts := time.Second
	f := newMockFilter("f-1")
	f.appliesFn = func(c FilterContext) bool {
		return c.ActualProvider() == "prov-x"
	}
	reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	targetA, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-observe"})
	targetB, _ := NewAttemptTarget("mg", "model-b", "prov-y", "failover", []string{"cap-observe"})
	rA, _ := rsnap.ResolveAttempt(targetA)
	rB, _ := rsnap.ResolveAttempt(targetB)
	if len(rA) != 1 || len(rB) != 0 {
		t.Fatalf("expected [1, 0], got [%d, %d]", len(rA), len(rB))
	}
	// Provider-specific Applies must also produce different required capability sets.
	capsA, _ := rsnap.RequiredFilterCapabilities(targetA)
	capsB, _ := rsnap.RequiredFilterCapabilities(targetB)
	if len(capsA) != 1 || len(capsB) != 0 {
		t.Fatalf("expected caps [1, 0], got [%d, %d]", len(capsA), len(capsB))
	}
	ctxA := rA[0].FilterContextForBindEpoch()
	if ctxA.ActualModel() != "model-a" || ctxA.ActualProvider() != "prov-x" {
		t.Errorf("resolve A wrong: model=%s provider=%s", ctxA.ActualModel(), ctxA.ActualProvider())
	}
	if ctxA.ExecutionPath() != "primary" {
		t.Errorf("executionPath A = %q", ctxA.ExecutionPath())
	}
}

func TestFilterRegistryDisabledFilterDoesNotRequireCapability(t *testing.T) {
	ts := time.Second
	fD := newMockFilter("f-disabled")
	regD, _ := NewFilterRegistration(fD, "cap-disabled", true, FilterEnforcementBlocking, ts, 10)
	fE := newMockFilter("f-enabled")
	regE, _ := NewFilterRegistration(fE, "cap-enabled", true, FilterEnforcementObserveOnly, ts, 20)
	p := mustPolicy("f-disabled", PolicySelectorDefault, "", false, FilterEnforcementObserveOnly, ts, 0)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{regD, regE}, []FilterPolicyLayer{p})
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-enabled"})
	resolved, _ := rsnap.ResolveAttempt(target)
	if len(resolved) != 1 || resolved[0].FilterID() != "f-enabled" {
		t.Errorf("expected only f-enabled")
	}
	reqCaps, err := rsnap.RequiredFilterCapabilities(target)
	if err != nil {
		t.Fatalf("RequiredFilterCapabilities: %v", err)
	}
	if len(reqCaps) != 1 || reqCaps[0] != "cap-enabled" {
		t.Errorf("RequiredFilterCapabilities = %v", reqCaps)
	}
	candBad, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-disabled"})
	if _, err := rsnap.EligibleCandidates([]AttemptTarget{candBad}); !errors.Is(err, ErrNoEligibleCandidate) {
		t.Errorf("expected exclusion")
	}
	candGood, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-enabled"})
	eligible, _ := rsnap.EligibleCandidates([]AttemptTarget{candGood})
	if len(eligible) != 1 {
		t.Errorf("expected 1 eligible")
	}
}

func TestFilterRegistryRequiredCapabilityPreflightIgnoresEnforcement(t *testing.T) {
	ts := time.Second
	fB, _ := NewFilterRegistration(newMockFilter("f-block"), "cap-blocking", true, FilterEnforcementBlocking, ts, 10)
	fO, _ := NewFilterRegistration(newMockFilter("f-observe"), "cap-observe", true, FilterEnforcementObserveOnly, ts, 20)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{fB, fO}, nil)
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-blocking", "cap-observe"})
	reqCaps, err := rsnap.RequiredFilterCapabilities(target)
	if err != nil {
		t.Fatalf("RequiredFilterCapabilities: %v", err)
	}
	sort.Strings(reqCaps)
	want := []string{"cap-blocking", "cap-observe"}
	if len(reqCaps) != len(want) {
		t.Fatalf("RequiredFilterCapabilities = %v", reqCaps)
	}
	for i, c := range reqCaps {
		if c != want[i] {
			t.Errorf("[%d] = %q", i, c)
		}
	}
	candAll, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-blocking", "cap-observe"})
	eligible, _ := rsnap.EligibleCandidates([]AttemptTarget{candAll})
	if len(eligible) != 1 {
		t.Fatalf("candidate with both caps should be eligible")
	}
}

// --- API-3 tests ---

type captureSet struct {
	set   EvaluationSet
	batch EvidenceBatch
}
type capturingArbiter struct{ capture *captureSet }

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

// TestResolvedFilterAdapterPassesFullContextThroughCoordinator verifies that
// the resolved filter adapter passes the full FilterContext scalar through
// the coordinator to the filter's Evaluate, that the batch's CapturedAt and
// events are identical to the input, and that the coordinator receives the
// correct endpoint/family/correlation.
func TestResolvedFilterAdapterPassesFullContextThroughCoordinator(t *testing.T) {
	ts := time.Second
	var captured *capturedContext
	var capturedBatch EvidenceBatch
	f := newMockFilter("f-1")
	f.evaluateFn = func(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
		cp := captureContext(fc)
		captured = &cp
		capturedBatch = batch
		return fixedDecision(), nil
	}
	reg, _ := NewFilterRegistration(f, "cap-observe", true, FilterEnforcementBlocking, ts, 10)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	target, _ := NewAttemptTarget("mg-test", "model-x", "prov-y", "primary", []string{"cap-observe"})
	resolved, _ := rsnap.ResolveAttempt(target)
	if len(resolved) != 1 {
		t.Fatalf("expected 1 resolved")
	}
	app, _ := NewFilterApplicability(resolved[0].FilterID(), true, true)
	ef, err := resolved[0].BindEpoch(7, app)
	if err != nil {
		t.Fatalf("BindEpoch: %v", err)
	}
	batch := makeBatch()
	capturedSet := &captureSet{}
	arbiter := &capturingArbiter{capture: capturedSet}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	coord, err := NewGateCoordinator(ctx, GateCoordinatorOptions{MaxQueuedEpochs: 1}, arbiter)
	if err != nil {
		t.Fatalf("NewGateCoordinator: %v", err)
	}
	defer coord.Close()
	if _, err := coord.Submit(ctx, batch, []EpochFilter{ef}); err != nil {
		t.Fatalf("Submit: %v", err)
	}
	if captured == nil {
		t.Fatalf("filter not called")
	}
	assertBatchMatches(t, "filter", capturedBatch, batch)
	// Full context scalar assertions captured inside Evaluate.
	if captured.configGeneration != "gen-1" {
		t.Errorf("configGeneration = %q", captured.configGeneration)
	}
	if captured.attemptID != "attempt-1" {
		t.Errorf("attemptID = %q", captured.attemptID)
	}
	if !captured.hasEpoch || captured.epochID != 7 {
		t.Errorf("epoch: hasEpoch=%v epochID=%d", captured.hasEpoch, captured.epochID)
	}
	if captured.environment != "staging" {
		t.Errorf("environment = %q", captured.environment)
	}
	if captured.modelGroup != "mg-test" {
		t.Errorf("modelGroup = %q", captured.modelGroup)
	}
	if captured.actualModel != "model-x" {
		t.Errorf("actualModel = %q", captured.actualModel)
	}
	if captured.actualProvider != "prov-y" {
		t.Errorf("actualProvider = %q", captured.actualProvider)
	}
	if captured.executionPath != "primary" {
		t.Errorf("executionPath = %q", captured.executionPath)
	}
	// Coordinator-seam scalars (endpoint/family/stableCorrelation) also reach the filter's Evaluate.
	if captured.endpoint != "ep" {
		t.Errorf("endpoint = %q", captured.endpoint)
	}
	if captured.family != "fam" {
		t.Errorf("family = %q", captured.family)
	}
	if captured.stableCorrelation != "corr-1" {
		t.Errorf("stableCorrelation = %q", captured.stableCorrelation)
	}

	// Verify input batch identity: CapturedAt, event snapshot, terminal/commit state.
	fixedTs := batch.CapturedAt()
	if fixedTs.IsZero() {
		t.Fatalf("batch CapturedAt is zero")
	}
	if batch.TerminalFlag() != false {
		t.Errorf("batch TerminalFlag = %v", batch.TerminalFlag())
	}
	if batch.CommitState() != CommitStateTransportUncommitted {
		t.Errorf("batch CommitState = %v", batch.CommitState())
	}
	events := batch.Events()
	if len(events) != 1 {
		t.Fatalf("batch event count = %d, want 1", len(events))
	}
	event := events[0]
	if event.Kind() != EventKindTextDelta {
		t.Errorf("event[0] kind = %q, want %q", event.Kind(), EventKindTextDelta)
	}
	if event.Channel() != "ch-input" {
		t.Errorf("event[0] channel = %q, want %q", event.Channel(), "ch-input")
	}
	if !event.Timestamp().Equal(fixedTs) {
		t.Errorf("event[0] timestamp = %v, want %v", event.Timestamp(), fixedTs)
	}
	text, err := event.AsTextDelta()
	if err != nil {
		t.Fatalf("event[0] AsTextDelta: %v", err)
	}
	if text != "hello" {
		t.Errorf("event[0] text = %q, want %q", text, "hello")
	}

	// Verify arbiter received the same batch content.
	assertBatchMatches(t, "arbiter", capturedSet.batch, batch)

	// Eval set outcome still targets the same filter id.
	if capturedSet.set.Len() != 1 {
		t.Fatalf("eval set len = %d", capturedSet.set.Len())
	}
	oc, _ := capturedSet.set.At(0)
	if oc.FilterID() != "f-1" {
		t.Errorf("outcome filter id = %q", oc.FilterID())
	}
	if ef.ID() != "f-1" {
		t.Errorf("EpochFilter.ID() = %q", ef.ID())
	}
}

// filterIDs returns the FilterID values of resolved filters for error messages.
func filterIDs(resolved []ResolvedFilter) []string {
	out := make([]string, len(resolved))
	for i, r := range resolved {
		out[i] = r.FilterID()
	}
	return out
}

// mustTarget constructs a validated AttemptTarget for tests.
func mustTarget(mg, model, prov, path, cap string) AttemptTarget {
	t, err := NewAttemptTarget(mg, model, prov, path, []string{cap})
	if err != nil {
		panic(err)
	}
	return t
}

// assertBatchMatches compares a captured batch against the input batch for
// CapturedAt, terminal flag, commit state, and the first text event's kind,
// channel, timestamp, and payload. It is used to verify that the filter and
// Arbiter both receive the same EvidenceBatch that the coordinator was given.
func assertBatchMatches(t *testing.T, who string, got, want EvidenceBatch) {
	t.Helper()
	if got.CapturedAt().IsZero() {
		t.Fatalf("%s batch not captured", who)
	}
	if !got.CapturedAt().Equal(want.CapturedAt()) {
		t.Errorf("%s batch CapturedAt = %v, want %v", who, got.CapturedAt(), want.CapturedAt())
	}
	if got.TerminalFlag() != want.TerminalFlag() {
		t.Errorf("%s batch TerminalFlag = %v, want %v", who, got.TerminalFlag(), want.TerminalFlag())
	}
	if got.CommitState() != want.CommitState() {
		t.Errorf("%s batch CommitState = %v, want %v", who, got.CommitState(), want.CommitState())
	}
	gotEvents := got.Events()
	wantEvents := want.Events()
	if len(gotEvents) != len(wantEvents) {
		t.Fatalf("%s batch event count = %d, want %d", who, len(gotEvents), len(wantEvents))
	}
	if len(gotEvents) == 0 {
		t.Fatalf("%s batch has no events", who)
	}
	gotEv := gotEvents[0]
	wantEv := wantEvents[0]
	if gotEv.Kind() != wantEv.Kind() {
		t.Errorf("%s event[0] kind = %q, want %q", who, gotEv.Kind(), wantEv.Kind())
	}
	if gotEv.Channel() != wantEv.Channel() {
		t.Errorf("%s event[0] channel = %q, want %q", who, gotEv.Channel(), wantEv.Channel())
	}
	if !gotEv.Timestamp().Equal(wantEv.Timestamp()) {
		t.Errorf("%s event[0] timestamp = %v, want %v", who, gotEv.Timestamp(), wantEv.Timestamp())
	}
	gotText, err := gotEv.AsTextDelta()
	if err != nil {
		t.Fatalf("%s event[0] AsTextDelta: %v", who, err)
	}
	wantText, err := wantEv.AsTextDelta()
	if err != nil {
		t.Fatalf("%s event[0] AsTextDelta: %v", who, err)
	}
	if gotText != wantText {
		t.Errorf("%s event[0] text = %q, want %q", who, gotText, wantText)
	}
}

func TestResolvedFilterBindEpochRejectsApplicabilityIDMismatch(t *testing.T) {
	ts := time.Second
	f := newMockFilter("f-alpha")
	f.holdReqFn = func(FilterContext) FilterHoldRequirement { return mustHoldReqNone("ch") }
	reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 1)
	snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
	rsnap, _ := snap.BeginRequest(reqCtx("gen-1", "attempt-1", "staging"))
	target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-test"})
	resolved, _ := rsnap.ResolveAttempt(target)
	mismatchedApp, _ := NewFilterApplicability("f-other", true, true)
	_, err := resolved[0].BindEpoch(1, mismatchedApp)
	if err == nil {
		t.Fatal("expected error on applicability filter ID mismatch, got nil")
	}
}
