package openai

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

	"iop/packages/go/config"
	"iop/packages/go/streamgate"
)

func repeatGuardFilterContext(t *testing.T, attemptID string, hasToolSideEffect bool) streamgate.FilterContext {
	t.Helper()
	fctx, err := streamgate.NewFilterContextBuilder(streamGateConfigGeneration, attemptID).
		SetEndpoint(openAIRebuildEndpointChat).
		SetExecutionPath("normalized").
		SetCommitState(streamgate.CommitStateStreamOpen).
		SetHasToolSideEffect(hasToolSideEffect).
		Build()
	if err != nil {
		t.Fatalf("build repeat filter context: %v", err)
	}
	return fctx
}

func repeatTextEvent(t *testing.T, kind streamgate.EventKind, value string) streamgate.NormalizedEvent {
	t.Helper()
	var (
		event streamgate.NormalizedEvent
		err   error
	)
	switch kind {
	case streamgate.EventKindTextDelta:
		event, err = streamgate.NewTextDeltaEvent(streamGateChannelDefault, value, time.Now())
	case streamgate.EventKindReasoningDelta:
		event, err = streamgate.NewReasoningDeltaEvent(streamGateChannelDefault, value, time.Now())
	default:
		t.Fatalf("unsupported repeat test event kind %q", kind)
	}
	if err != nil {
		t.Fatalf("build repeat event: %v", err)
	}
	return event
}

func repeatToolEvent(t *testing.T, id, name, arguments string) streamgate.NormalizedEvent {
	t.Helper()
	event, err := streamgate.NewToolCallFragmentEvent(
		streamGateChannelDefault,
		id,
		name,
		arguments,
		time.Now(),
	)
	if err != nil {
		t.Fatalf("build repeat tool event: %v", err)
	}
	return event
}

func repeatTerminalEvent(t *testing.T) streamgate.NormalizedEvent {
	t.Helper()
	event, err := streamgate.NewTerminalEvent(streamGateChannelDefault, time.Now())
	if err != nil {
		t.Fatalf("build repeat terminal event: %v", err)
	}
	return event
}

func repeatEvidenceBatch(
	t *testing.T,
	current streamgate.NormalizedEvent,
	pending, lookBehind []streamgate.NormalizedEvent,
) streamgate.EvidenceBatch {
	t.Helper()
	pendingByChannel := map[string][]streamgate.NormalizedEvent{}
	if len(pending) > 0 {
		pendingByChannel[streamGateChannelDefault] = pending
	}
	lookBehindByChannel := map[string][]streamgate.NormalizedEvent{}
	if len(lookBehind) > 0 {
		lookBehindByChannel[streamGateChannelDefault] = lookBehind
	}
	batch, err := streamgate.NewEvidenceBatch(
		[]streamgate.NormalizedEvent{current},
		pendingByChannel,
		lookBehindByChannel,
		nil,
		current.Kind() == streamgate.EventKindTerminal || current.Kind() == streamgate.EventKindProviderError,
		streamgate.CommitStateStreamOpen,
		time.Now(),
	)
	if err != nil {
		t.Fatalf("NewEvidenceBatch: %v", err)
	}
	return batch
}

func uniqueKoreanRunes(count int) string {
	runes := make([]rune, count)
	for i := range runes {
		runes[i] = rune(0xAC00 + i)
	}
	return string(runes)
}

func koreanRepeatParagraphFixture(t *testing.T) string {
	t.Helper()
	paragraphs := []string{
		"첫 번째 문단은 새벽의 관제실에서 시작된다. 운영자는 천천히 갱신되는 지표를 살피며 각 요청의 시작 시각과 종료 상태를 구분한다. 화면에 나타난 문장은 서로 다른 어휘와 리듬을 사용하고, 다음 문단과 혼동되지 않도록 고유한 맥락을 유지한다.",
		"두 번째 문단은 긴 응답을 전송하는 동안 바이트 경계가 글자 경계와 다를 수 있음을 설명한다. 한글 음절이 여러 조각으로 나뉘어 도착하더라도 검사기는 유효한 유니코드 순서를 보존해야 하며, 안전한 위치 이전의 내용만 사용자에게 전달해야 한다.",
		"세 번째 문단은 이미 전달된 접두사와 아직 보류 중인 꼬리를 분리한다. 이전 시도에서 확인한 문장을 다시 받으면 새 시작 이벤트를 노출하지 않고, 커서 뒤의 중복 구간을 제거한 다음 복구된 후속 내용을 정확히 한 번 이어 붙인다.",
		"네 번째 문단은 도구 호출의 책임 경계를 다룬다. 호출 이름과 인자가 여러 델타로 흩어질 수 있으므로 동일한 호출 식별자끼리 순서대로 조립하고, 완전한 구조가 확인되기 전에는 부분 인자를 독립된 행동으로 판정하거나 외부로 방출하지 않는다.",
		"다섯 번째 문단은 요청 이력의 출처를 구별한다. 사용자 문장에 존재하는 표현은 보조자의 반복 근거로 사용하지 않고, 일반 텍스트와 추론 별칭은 허용된 역할과 채널에서만 지문을 만들며 암호화되거나 서명된 값은 그대로 보호한다.",
		"여섯 번째 문단은 종료 조건을 확인한다. 복구 후보가 남아 있으면 정해진 온도를 순서대로 사용하지만 호출자가 값을 지정했다면 보존하고, 부작용 경계나 후보 소진을 만나면 자동 실행을 멈춘 뒤 하나의 안전한 종료 신호만 전달한다.",
	}
	if len(paragraphs) != 6 {
		t.Fatalf("Korean repeat fixture paragraphs = %d, want 6", len(paragraphs))
	}
	fixture := strings.Join(paragraphs, "\n\n")
	if utf8.RuneCountInString(fixture) <= 500 {
		t.Fatalf("Korean repeat fixture runes = %d, want more than 500", utf8.RuneCountInString(fixture))
	}
	return fixture
}

func splitRepeatFixtureAtRuneOffsets(t *testing.T, value string, offsets []int) []string {
	t.Helper()
	runes := []rune(value)
	chunks := make([]string, 0, len(offsets)+1)
	start := 0
	for _, offset := range offsets {
		if offset <= start || offset >= len(runes) {
			t.Fatalf("invalid repeat fixture split offset %d for range (%d,%d)", offset, start, len(runes))
		}
		chunks = append(chunks, string(runes[start:offset]))
		start = offset
	}
	chunks = append(chunks, string(runes[start:]))
	return chunks
}

func repeatTextChunks(t *testing.T, chunks []string) []streamgate.NormalizedEvent {
	t.Helper()
	events := make([]streamgate.NormalizedEvent, 0, len(chunks))
	for _, chunk := range chunks {
		events = append(events, repeatTextEvent(t, streamgate.EventKindTextDelta, chunk))
	}
	return events
}

func TestRepeatGuardSixParagraphKoreanRolling(t *testing.T) {
	fixture := koreanRepeatParagraphFixture(t)
	fixtureRunes := utf8.RuneCountInString(fixture)

	for _, threshold := range []int{200, 500} {
		t.Run(fmt.Sprintf("pending_%d", threshold), func(t *testing.T) {
			repeated := fixture + fixture
			offsets := []int{
				1,
				threshold - 1,
				threshold + 7,
				fixtureRunes - 3,
				fixtureRunes + 1,
				fixtureRunes + threshold - 2,
				fixtureRunes + threshold + 11,
			}
			events := repeatTextChunks(t, splitRepeatFixtureAtRuneOffsets(t, repeated, offsets))
			filter, err := newOpenAIOutputFilter(
				openAIOutputFilterRepeatGuard,
				threshold,
				10,
				"openai.snap.six-paragraph.pending",
			)
			if err != nil {
				t.Fatalf("newOpenAIOutputFilter: %v", err)
			}
			decision, err := filter.Evaluate(
				context.Background(),
				repeatGuardFilterContext(t, "attempt.six-paragraph.pending", false),
				repeatEvidenceBatch(t, events[len(events)-1], events, nil),
			)
			if err != nil {
				t.Fatalf("Evaluate: %v", err)
			}
			if decision.Kind() != streamgate.FilterDecisionKindViolation || decision.RecoveryIntent() == nil {
				t.Fatalf("decision = %q intent=%v, want continuation violation", decision.Kind(), decision.RecoveryIntent())
			}
			cursor := decision.RecoveryIntent().Directive().Cursor()
			if cursor != len(fixture) {
				t.Fatalf("cursor = %d, want fixture byte length %d", cursor, len(fixture))
			}
			if !utf8.ValidString(repeated[:cursor]) {
				t.Fatalf("cursor %d splits a UTF-8 rune", cursor)
			}
		})

		t.Run(fmt.Sprintf("committed_look_behind_%d", threshold), func(t *testing.T) {
			firstChunks := repeatTextChunks(t, splitRepeatFixtureAtRuneOffsets(
				t,
				fixture,
				[]int{1, threshold - 1, threshold + 5, fixtureRunes - 2},
			))
			secondChunks := repeatTextChunks(t, splitRepeatFixtureAtRuneOffsets(
				t,
				fixture,
				[]int{2, threshold - 3, threshold + 9, fixtureRunes - 1},
			))
			boundedLookBehindText := string([]rune(fixture)[fixtureRunes-threshold:])
			if got := utf8.RuneCountInString(boundedLookBehindText); got != threshold {
				t.Fatalf("look-behind runes = %d, want %d", got, threshold)
			}
			boundedLookBehind := repeatTextChunks(t, splitRepeatFixtureAtRuneOffsets(
				t,
				boundedLookBehindText,
				[]int{1, threshold / 2, threshold - 1},
			))
			filter, err := newOpenAIOutputFilter(
				openAIOutputFilterRepeatGuard,
				threshold,
				10,
				"openai.snap.six-paragraph.look-behind",
			)
			if err != nil {
				t.Fatalf("newOpenAIOutputFilter: %v", err)
			}
			fctx := repeatGuardFilterContext(t, "attempt.six-paragraph.look-behind", false)
			first, err := filter.Evaluate(
				context.Background(),
				fctx,
				repeatEvidenceBatch(t, firstChunks[len(firstChunks)-1], firstChunks, nil),
			)
			if err != nil || first.Kind() != streamgate.FilterDecisionKindPass {
				t.Fatalf("first decision = %q, %v, want pass", first.Kind(), err)
			}
			decision, err := filter.Evaluate(
				context.Background(),
				fctx,
				repeatEvidenceBatch(t, secondChunks[len(secondChunks)-1], secondChunks, boundedLookBehind),
			)
			if err != nil {
				t.Fatalf("look-behind Evaluate: %v", err)
			}
			if decision.Kind() != streamgate.FilterDecisionKindViolation {
				t.Fatalf("look-behind decision = %q, want violation", decision.Kind())
			}
			if got := decision.RecoveryIntent().Directive().Cursor(); got != len(fixture) {
				t.Fatalf("look-behind cursor = %d, want %d", got, len(fixture))
			}
		})
	}
}

func TestRepeatGuardAssistantHistoryAnchorDecision(t *testing.T) {
	fixture := koreanRepeatParagraphFixture(t)
	fingerprint, ok := openAIRepeatTextFingerprint(fixture)
	if !ok {
		t.Fatal("build fixture fingerprint")
	}

	assistantOnlyBody, err := json.Marshal(map[string]any{
		"messages": []any{
			map[string]any{"role": "assistant", "content": fixture},
			map[string]any{"role": "assistant", "reasoning_content": fixture},
			map[string]any{"role": "assistant", "reasoning_text": fixture},
		},
	})
	if err != nil {
		t.Fatalf("marshal assistant-only fixture: %v", err)
	}
	history, err := decodeOpenAIChatRepeatHistory(assistantOnlyBody)
	if err != nil {
		t.Fatalf("decode assistant-only fixture: %v", err)
	}
	if count, candidate := history.assistantCandidate(openAIRepeatChannelContent, fingerprint); !candidate || count != 3 {
		t.Fatalf("assistant content candidate = (%d,%t), want (3,true)", count, candidate)
	}
	if count, candidate := history.assistantCandidate(openAIRepeatChannelReasoning, fingerprint); !candidate || count != 3 {
		t.Fatalf("assistant reasoning candidate = (%d,%t), want (3,true)", count, candidate)
	}

	event := repeatTextEvent(t, streamgate.EventKindTextDelta, fixture)
	filter, err := newOpenAIOutputFilter(
		openAIOutputFilterRepeatGuard,
		500,
		10,
		"openai.snap.assistant-anchor",
		history,
	)
	if err != nil {
		t.Fatalf("newOpenAIOutputFilter: %v", err)
	}
	decision, err := filter.Evaluate(
		context.Background(),
		repeatGuardFilterContext(t, "attempt.assistant-anchor", false),
		repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
	)
	if err != nil {
		t.Fatalf("Evaluate assistant anchor: %v", err)
	}
	if decision.Kind() != streamgate.FilterDecisionKindViolation ||
		decision.Evidence().DescriptorCode() != "assistant_history_anchor_repeat" {
		t.Fatalf("assistant anchor decision = (%q,%q), want history violation",
			decision.Kind(), decision.Evidence().DescriptorCode())
	}

	userBody, err := json.Marshal(map[string]any{
		"messages": []any{
			map[string]any{"role": "user", "content": fixture},
			map[string]any{"role": "assistant", "content": fixture},
			map[string]any{"role": "assistant", "reasoning_content": fixture},
			map[string]any{"role": "assistant", "reasoning_text": fixture},
		},
	})
	if err != nil {
		t.Fatalf("marshal user exclusion fixture: %v", err)
	}
	userHistory, err := decodeOpenAIChatRepeatHistory(userBody)
	if err != nil {
		t.Fatalf("decode user exclusion fixture: %v", err)
	}
	if _, candidate := userHistory.assistantCandidate(openAIRepeatChannelContent, fingerprint); candidate {
		t.Fatal("user provenance did not exclude the content history anchor")
	}
	if _, candidate := userHistory.assistantCandidate(openAIRepeatChannelReasoning, fingerprint); candidate {
		t.Fatal("user provenance did not exclude the reasoning history anchor")
	}
	userFilter, err := newOpenAIOutputFilter(
		openAIOutputFilterRepeatGuard,
		500,
		10,
		"openai.snap.user-exclusion",
		userHistory,
	)
	if err != nil {
		t.Fatalf("new user-exclusion filter: %v", err)
	}
	userDecision, err := userFilter.Evaluate(
		context.Background(),
		repeatGuardFilterContext(t, "attempt.user-exclusion", false),
		repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
	)
	if err != nil {
		t.Fatalf("Evaluate user exclusion: %v", err)
	}
	if userDecision.Kind() != streamgate.FilterDecisionKindPass {
		t.Fatalf("user occurrence decision = %q, want pass", userDecision.Kind())
	}
}

func TestRepeatGuardAssistantHistoryAnchorBoundaries(t *testing.T) {
	const (
		anchor      = "assistant anchor spans provider 경계 with stable whitespace"
		novelPrefix = "novel provider prefix: "
	)
	historyBody, err := json.Marshal(map[string]any{
		"messages": []any{
			map[string]any{"role": "assistant", "content": anchor},
			map[string]any{"role": "assistant", "reasoning_content": anchor},
		},
	})
	if err != nil {
		t.Fatalf("marshal history: %v", err)
	}
	history, err := decodeOpenAIChatRepeatHistory(historyBody)
	if err != nil {
		t.Fatalf("decode history: %v", err)
	}

	newFilter := func(t *testing.T, suffix string, snapshot openAIRepeatHistorySnapshot) *openAIOutputFilter {
		t.Helper()
		filter, filterErr := newOpenAIOutputFilter(
			openAIOutputFilterRepeatGuard,
			500,
			10,
			"openai.snap.anchor-boundary."+suffix,
			snapshot,
		)
		if filterErr != nil {
			t.Fatalf("newOpenAIOutputFilter: %v", filterErr)
		}
		return filter
	}
	assertViolation := func(t *testing.T, decision streamgate.FilterDecision, err error, wantCursor int) {
		t.Helper()
		if err != nil {
			t.Fatalf("Evaluate: %v", err)
		}
		if decision.Kind() != streamgate.FilterDecisionKindViolation ||
			decision.Evidence().DescriptorCode() != "assistant_history_anchor_repeat" {
			t.Fatalf("decision = (%q,%q), want assistant-history violation",
				decision.Kind(), decision.Evidence().DescriptorCode())
		}
		if decision.RecoveryIntent() == nil ||
			decision.RecoveryIntent().Directive().Cursor() != wantCursor {
			t.Fatalf("continuation cursor = %v, want %d", decision.RecoveryIntent(), wantCursor)
		}
		if !utf8.ValidString((novelPrefix + anchor)[:wantCursor]) {
			t.Fatalf("cursor %d is not an original UTF-8 boundary", wantCursor)
		}
	}

	t.Run("novel prefix in one event", func(t *testing.T) {
		value := novelPrefix + anchor
		event := repeatTextEvent(t, streamgate.EventKindTextDelta, value)
		decision, evalErr := newFilter(t, "one-event", history).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.one-event", false),
			repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
		)
		assertViolation(t, decision, evalErr, len(novelPrefix))
	})

	t.Run("novel prefix and anchor in separate provider events", func(t *testing.T) {
		prefixEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, novelPrefix)
		anchorEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, anchor)
		decision, evalErr := newFilter(t, "split-events", history).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.split-events", false),
			repeatEvidenceBatch(
				t,
				anchorEvent,
				[]streamgate.NormalizedEvent{prefixEvent, anchorEvent},
				nil,
			),
		)
		assertViolation(t, decision, evalErr, len(novelPrefix))
	})

	t.Run("novel prefix committed before anchor", func(t *testing.T) {
		filter := newFilter(t, "committed-prefix", history)
		fctx := repeatGuardFilterContext(t, "attempt.anchor.committed-prefix", false)
		prefixEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, novelPrefix)
		first, firstErr := filter.Evaluate(
			t.Context(),
			fctx,
			repeatEvidenceBatch(t, prefixEvent, []streamgate.NormalizedEvent{prefixEvent}, nil),
		)
		if firstErr != nil || first.Kind() != streamgate.FilterDecisionKindPass {
			t.Fatalf("prefix decision = %q, %v, want pass", first.Kind(), firstErr)
		}
		anchorEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, anchor)
		decision, evalErr := filter.Evaluate(
			t.Context(),
			fctx,
			repeatEvidenceBatch(
				t,
				anchorEvent,
				[]streamgate.NormalizedEvent{anchorEvent},
				[]streamgate.NormalizedEvent{prefixEvent},
			),
		)
		assertViolation(t, decision, evalErr, len(novelPrefix))
	})

	t.Run("anchor crosses committed look-behind", func(t *testing.T) {
		filter := newFilter(t, "cross-look-behind", history)
		fctx := repeatGuardFilterContext(t, "attempt.anchor.cross-look-behind", false)
		anchorRunes := []rune(anchor)
		split := len(anchorRunes) / 2
		firstValue := novelPrefix + string(anchorRunes[:split])
		firstEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, firstValue)
		first, firstErr := filter.Evaluate(
			t.Context(),
			fctx,
			repeatEvidenceBatch(t, firstEvent, []streamgate.NormalizedEvent{firstEvent}, nil),
		)
		if firstErr != nil || first.Kind() != streamgate.FilterDecisionKindPass {
			t.Fatalf("partial-anchor decision = %q, %v, want pass", first.Kind(), firstErr)
		}
		secondEvent := repeatTextEvent(t, streamgate.EventKindTextDelta, string(anchorRunes[split:]))
		decision, evalErr := filter.Evaluate(
			t.Context(),
			fctx,
			repeatEvidenceBatch(
				t,
				secondEvent,
				[]streamgate.NormalizedEvent{secondEvent},
				[]streamgate.NormalizedEvent{firstEvent},
			),
		)
		assertViolation(t, decision, evalErr, len(firstValue))
	})

	t.Run("whitespace normalization preserves source cursor", func(t *testing.T) {
		value := novelPrefix + "assistant\tanchor spans\nprovider  경계 with stable whitespace"
		event := repeatTextEvent(t, streamgate.EventKindTextDelta, value)
		decision, evalErr := newFilter(t, "whitespace", history).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.whitespace", false),
			repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
		)
		assertViolation(t, decision, evalErr, len(novelPrefix))
	})

	t.Run("distinct suffix passes", func(t *testing.T) {
		value := novelPrefix + "a distinct assistant suffix with no historical match"
		event := repeatTextEvent(t, streamgate.EventKindTextDelta, value)
		decision, evalErr := newFilter(t, "distinct-suffix", history).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.distinct-suffix", false),
			repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
		)
		if evalErr != nil || decision.Kind() != streamgate.FilterDecisionKindPass {
			t.Fatalf("distinct suffix decision = %q, %v, want pass", decision.Kind(), evalErr)
		}
	})

	t.Run("user provenance excludes window", func(t *testing.T) {
		userBody, marshalErr := json.Marshal(map[string]any{
			"messages": []any{
				map[string]any{"role": "user", "content": anchor},
				map[string]any{"role": "assistant", "content": anchor},
				map[string]any{"role": "assistant", "reasoning_content": anchor},
			},
		})
		if marshalErr != nil {
			t.Fatalf("marshal user history: %v", marshalErr)
		}
		userHistory, decodeErr := decodeOpenAIChatRepeatHistory(userBody)
		if decodeErr != nil {
			t.Fatalf("decode user history: %v", decodeErr)
		}
		value := novelPrefix + anchor
		event := repeatTextEvent(t, streamgate.EventKindTextDelta, value)
		decision, evalErr := newFilter(t, "user-exclusion", userHistory).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.user-exclusion", false),
			repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
		)
		if evalErr != nil || decision.Kind() != streamgate.FilterDecisionKindPass {
			t.Fatalf("user-excluded decision = %q, %v, want pass", decision.Kind(), evalErr)
		}
	})

	t.Run("completed progress excludes window", func(t *testing.T) {
		progressHistory := history
		progressHistory.hasCompletedProgress = true
		value := novelPrefix + anchor
		event := repeatTextEvent(t, streamgate.EventKindTextDelta, value)
		decision, evalErr := newFilter(t, "progress-exclusion", progressHistory).Evaluate(
			t.Context(),
			repeatGuardFilterContext(t, "attempt.anchor.progress-exclusion", false),
			repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
		)
		if evalErr != nil || decision.Kind() != streamgate.FilterDecisionKindPass {
			t.Fatalf("progress-excluded decision = %q, %v, want pass", decision.Kind(), evalErr)
		}
	})
}

func TestRepeatGuardKoreanMultibyteRolling(t *testing.T) {
	for _, threshold := range []int{200, 500} {
		t.Run(strings.TrimSpace(strings.Repeat("threshold_", 1))+string(rune('0'+threshold/100)), func(t *testing.T) {
			block := uniqueKoreanRunes(threshold/2 + 80)
			repeated := block + block
			event := repeatTextEvent(t, streamgate.EventKindTextDelta, repeated)
			filter, err := newOpenAIOutputFilter(
				openAIOutputFilterRepeatGuard,
				threshold,
				10,
				"openai.snap.repeat",
			)
			if err != nil {
				t.Fatalf("newOpenAIOutputFilter: %v", err)
			}
			decision, err := filter.Evaluate(
				context.Background(),
				repeatGuardFilterContext(t, "attempt.korean", false),
				repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
			)
			if err != nil {
				t.Fatalf("Evaluate: %v", err)
			}
			if decision.Kind() != streamgate.FilterDecisionKindViolation || decision.RecoveryIntent() == nil {
				t.Fatalf("repeat decision = %q intent=%v, want violation with continuation", decision.Kind(), decision.RecoveryIntent())
			}
			cursor := decision.RecoveryIntent().Directive().Cursor()
			if cursor != len(block) {
				t.Fatalf("cursor = %d, want UTF-8 byte offset %d", cursor, len(block))
			}
			if !utf8.ValidString(repeated[:cursor]) {
				t.Fatalf("cursor %d split a UTF-8 rune", cursor)
			}
			if decision.Evidence().DescriptorCode() != "repeat_content_detected" {
				t.Fatalf("descriptor = %q", decision.Evidence().DescriptorCode())
			}
		})
	}
}

func TestRepeatGuardCommittedLookBehind(t *testing.T) {
	block := uniqueKoreanRunes(180)
	first := repeatTextEvent(t, streamgate.EventKindTextDelta, block)
	filter, err := newOpenAIOutputFilter(openAIOutputFilterRepeatGuard, 200, 10, "openai.snap.lookbehind")
	if err != nil {
		t.Fatalf("newOpenAIOutputFilter: %v", err)
	}
	fctx := repeatGuardFilterContext(t, "attempt.lookbehind", false)
	firstDecision, err := filter.Evaluate(context.Background(), fctx, repeatEvidenceBatch(t, first, []streamgate.NormalizedEvent{first}, nil))
	if err != nil || firstDecision.Kind() != streamgate.FilterDecisionKindPass {
		t.Fatalf("first decision = %q, %v, want pass", firstDecision.Kind(), err)
	}

	second := repeatTextEvent(t, streamgate.EventKindTextDelta, block)
	decision, err := filter.Evaluate(
		context.Background(),
		fctx,
		repeatEvidenceBatch(t, second, []streamgate.NormalizedEvent{second}, []streamgate.NormalizedEvent{first}),
	)
	if err != nil {
		t.Fatalf("second Evaluate: %v", err)
	}
	if decision.Kind() != streamgate.FilterDecisionKindViolation {
		t.Fatalf("look-behind decision = %q, want violation", decision.Kind())
	}
	if got := decision.RecoveryIntent().Directive().Cursor(); got != len(block) {
		t.Fatalf("look-behind cursor = %d, want %d", got, len(block))
	}
}

func TestRepeatGuardActionProgressMatrix(t *testing.T) {
	actionFingerprint, _ := openAIRepeatActionFingerprint("lookup", json.RawMessage(`{"id":1}`))
	toolEvent, err := streamgate.NewToolCallFragmentEvent(
		streamGateChannelDefault,
		"call.current",
		"lookup",
		`{"id":1}`,
		time.Now(),
	)
	if err != nil {
		t.Fatalf("NewToolCallFragmentEvent: %v", err)
	}
	for _, tc := range []struct {
		name     string
		history  openAIRepeatHistorySnapshot
		wantKind streamgate.FilterDecisionKind
	}{
		{
			name: "no progress blocks held action",
			history: openAIRepeatHistorySnapshot{
				repeatedActions: map[streamgate.FixedFingerprint]int{actionFingerprint: 1},
			},
			wantKind: streamgate.FilterDecisionKindFatal,
		},
		{
			name:     "no repeated action passes",
			history:  openAIRepeatHistorySnapshot{repeatedActions: map[streamgate.FixedFingerprint]int{}},
			wantKind: streamgate.FilterDecisionKindPass,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			filter, filterErr := newOpenAIOutputFilter(
				openAIOutputFilterRepeatActionGuard,
				200,
				10,
				"openai.snap.action",
				tc.history,
			)
			if filterErr != nil {
				t.Fatalf("newOpenAIOutputFilter: %v", filterErr)
			}
			decision, evalErr := filter.Evaluate(
				context.Background(),
				repeatGuardFilterContext(t, "attempt.action", false),
				repeatEvidenceBatch(t, repeatTerminalEvent(t), []streamgate.NormalizedEvent{toolEvent}, nil),
			)
			if evalErr != nil {
				t.Fatalf("Evaluate: %v", evalErr)
			}
			if decision.Kind() != tc.wantKind {
				t.Fatalf("decision = %q, want %q", decision.Kind(), tc.wantKind)
			}
			if tc.wantKind == streamgate.FilterDecisionKindFatal &&
				decision.Evidence().DescriptorCode() != "repeated_action_no_progress" {
				t.Fatalf("fatal descriptor = %q", decision.Evidence().DescriptorCode())
			}
		})
	}
}

func TestRepeatGuardSplitToolArguments(t *testing.T) {
	repeatedFingerprint, ok := openAIRepeatActionFingerprint("lookup", json.RawMessage(`{"id":1}`))
	if !ok {
		t.Fatal("build repeated action fingerprint")
	}
	repeatedHistory := openAIRepeatHistorySnapshot{
		repeatedActions: map[streamgate.FixedFingerprint]int{repeatedFingerprint: 1},
	}

	tests := []struct {
		name           string
		history        openAIRepeatHistorySnapshot
		fragments      []streamgate.NormalizedEvent
		wantKind       streamgate.FilterDecisionKind
		wantDescriptor string
	}{
		{
			name:    "chat split repeated call",
			history: repeatedHistory,
			fragments: []streamgate.NormalizedEvent{
				repeatToolEvent(t, "call-chat", "lookup", `{"id":`),
				repeatToolEvent(t, "call-chat", "lookup", `1}`),
			},
			wantKind:       streamgate.FilterDecisionKindFatal,
			wantDescriptor: "repeated_action_no_progress",
		},
		{
			name:    "responses split distinct call",
			history: repeatedHistory,
			fragments: []streamgate.NormalizedEvent{
				repeatToolEvent(t, "fc-responses", "lookup", `{"id":`),
				repeatToolEvent(t, "fc-responses", "lookup", `2}`),
			},
			wantKind:       streamgate.FilterDecisionKindPass,
			wantDescriptor: "repeat_action_clear",
		},
		{
			name:    "interleaved ids remain isolated",
			history: repeatedHistory,
			fragments: []streamgate.NormalizedEvent{
				repeatToolEvent(t, "call-distinct", "lookup", `{"id":`),
				repeatToolEvent(t, "call-repeat", "lookup", `{"id":`),
				repeatToolEvent(t, "call-distinct", "lookup", `2}`),
				repeatToolEvent(t, "call-repeat", "lookup", `1}`),
			},
			wantKind:       streamgate.FilterDecisionKindFatal,
			wantDescriptor: "repeated_action_no_progress",
		},
		{
			name:    "incomplete json fails closed",
			history: repeatedHistory,
			fragments: []streamgate.NormalizedEvent{
				repeatToolEvent(t, "call-incomplete", "lookup", `{"id":`),
			},
			wantKind:       streamgate.FilterDecisionKindFatal,
			wantDescriptor: "action_arguments_incomplete",
		},
		{
			name:    "conflicting name fails closed",
			history: repeatedHistory,
			fragments: []streamgate.NormalizedEvent{
				repeatToolEvent(t, "call-conflict", "lookup", `{"id":`),
				repeatToolEvent(t, "call-conflict", "mutate", `1}`),
			},
			wantKind:       streamgate.FilterDecisionKindFatal,
			wantDescriptor: "action_name_conflict",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			filter, err := newOpenAIOutputFilter(
				openAIOutputFilterRepeatActionGuard,
				200,
				10,
				"openai.snap.action-split",
				tc.history,
			)
			if err != nil {
				t.Fatalf("newOpenAIOutputFilter: %v", err)
			}
			requirement := filter.HoldRequirement(repeatGuardFilterContext(t, "attempt.action-split", false))
			if requirement.Mode() != streamgate.FilterHoldModeTerminalGate {
				t.Fatalf("action hold mode = %q, want terminal_gate", requirement.Mode())
			}
			decision, err := filter.Evaluate(
				context.Background(),
				repeatGuardFilterContext(t, "attempt.action-split", false),
				repeatEvidenceBatch(t, repeatTerminalEvent(t), tc.fragments, nil),
			)
			if err != nil {
				t.Fatalf("Evaluate: %v", err)
			}
			if decision.Kind() != tc.wantKind {
				t.Fatalf("decision = %q, want %q", decision.Kind(), tc.wantKind)
			}
			if got := decision.Evidence().DescriptorCode(); got != tc.wantDescriptor {
				t.Fatalf("descriptor = %q, want %q", got, tc.wantDescriptor)
			}
		})
	}
}

func TestRepeatGuardToolSideEffectNoRepair(t *testing.T) {
	block := uniqueKoreanRunes(160)
	event := repeatTextEvent(t, streamgate.EventKindTextDelta, block+block)
	filter, err := newOpenAIOutputFilter(openAIOutputFilterRepeatGuard, 200, 10, "openai.snap.sideeffect")
	if err != nil {
		t.Fatalf("newOpenAIOutputFilter: %v", err)
	}
	decision, err := filter.Evaluate(
		context.Background(),
		repeatGuardFilterContext(t, "attempt.sideeffect", true),
		repeatEvidenceBatch(t, event, []streamgate.NormalizedEvent{event}, nil),
	)
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if decision.Kind() != streamgate.FilterDecisionKindFatal || decision.RecoveryIntent() != nil {
		t.Fatalf("side-effect decision = %q intent=%v, want fatal without repair", decision.Kind(), decision.RecoveryIntent())
	}
}

func TestRepeatGuardIdleDoesNotRelease(t *testing.T) {
	filter, err := newOpenAIOutputFilter(openAIOutputFilterRepeatGuard, 500, 10, "openai.snap.idle")
	if err != nil {
		t.Fatalf("newOpenAIOutputFilter: %v", err)
	}
	requirement := filter.HoldRequirement(repeatGuardFilterContext(t, "attempt.idle", false))
	if requirement.Mode() != streamgate.FilterHoldModeRolling || requirement.EvidenceRunes() != 500 {
		t.Fatalf("idle hold = (%q, %d), want rolling 500", requirement.Mode(), requirement.EvidenceRunes())
	}
}

// outputFilterGateCfg builds a stream-gate config declaring the three semantic
// output filters with the given base enforcement.
func outputFilterGateCfg(enforcement string, selectors ...config.StreamGateFilterSelectorConf) config.StreamEvidenceGateConf {
	return config.StreamEvidenceGateConf{
		Enabled: true,
		Filters: []config.StreamGateFilterPolicyConf{
			{Filter: config.StreamGateFilterRepeatGuard, Enforcement: enforcement, Priority: 10},
			{Filter: config.StreamGateFilterSchemaGate, Enforcement: enforcement, Priority: 15},
			{Filter: config.StreamGateFilterProviderError, Enforcement: enforcement, Priority: 20, Selectors: selectors},
		},
	}
}

func schemaOutputFilterContext(requestRef string) openAIOutputFilterContext {
	return openAIOutputFilterContext{endpoint: openAIRebuildEndpointChat, hasScheme: true, requestRef: requestRef}
}

// TestOpenAIOutputFilterRegistrations verifies the config->Core
// translation: ids, required capabilities, enforcement, priority, and that
// schema_gate only registers when a scheme is present (S01/S02).
func TestOpenAIOutputFilterRegistrations(t *testing.T) {
	gateCfg := outputFilterGateCfg(config.StreamGateFilterEnforcementBlocking)

	// Without a scheme, schema_gate is not registered.
	regsNoScheme, _, err := openAIOutputFilterRegistrations(gateCfg, openAIOutputFilterContext{endpoint: openAIRebuildEndpointChat, requestRef: "openai.snap.1"})
	if err != nil {
		t.Fatalf("openAIOutputFilterRegistrations(no scheme): %v", err)
	}
	if len(regsNoScheme) != 3 {
		t.Fatalf("no-scheme registrations = %d, want 3 (schema_gate skipped, repeat action sibling included)", len(regsNoScheme))
	}

	regs, _, err := openAIOutputFilterRegistrations(gateCfg, schemaOutputFilterContext("openai.snap.1"))
	if err != nil {
		t.Fatalf("openAIOutputFilterRegistrations(scheme): %v", err)
	}
	byID := make(map[string]streamgate.FilterRegistration, len(regs))
	for _, r := range regs {
		byID[r.FilterID()] = r
	}
	want := map[string]struct {
		cap      string
		priority int
	}{
		openAIRepeatGuardFilterID:       {"output.repeat_guard", 10},
		openAIRepeatActionGuardFilterID: {"output.repeat_guard", 10},
		openAISchemaGateFilterID:        {"output.schema_gate", 15},
		openAIProviderErrorFilterID:     {"output.provider_error", 20},
	}
	if len(byID) != len(want) {
		t.Fatalf("scheme registrations = %d, want %d", len(byID), len(want))
	}
	for id, w := range want {
		reg, ok := byID[id]
		if !ok {
			t.Fatalf("missing registration %q", id)
		}
		if reg.RequiredCapabilityID() != w.cap {
			t.Errorf("%s capability = %q, want %q", id, reg.RequiredCapabilityID(), w.cap)
		}
		if reg.Priority() != w.priority {
			t.Errorf("%s priority = %d, want %d", id, reg.Priority(), w.priority)
		}
		if reg.Enforcement() != streamgate.FilterEnforcementBlocking {
			t.Errorf("%s enforcement = %q, want blocking", id, reg.Enforcement())
		}
	}
}

// TestOpenAIOutputFiltersOutcomeMatrix drives the configured filters, including
// the internal repeat-action sibling, through the Core epoch-binding contract
// and asserts the S14 outcome roles: rolling -> evaluated, terminal-gate
// blocking -> deferred, error-event-only -> not applicable on a clean epoch.
func TestOpenAIOutputFiltersOutcomeMatrix(t *testing.T) {
	gateCfg := outputFilterGateCfg(config.StreamGateFilterEnforcementBlocking)
	regs, policies, err := openAIOutputFilterRegistrations(gateCfg, schemaOutputFilterContext("openai.snap.1"))
	if err != nil {
		t.Fatalf("openAIOutputFilterRegistrations: %v", err)
	}
	snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regs, policies)
	if err != nil {
		t.Fatalf("NewFilterRegistrySnapshot: %v", err)
	}
	reqCtx, err := streamgate.NewRequestFilterContext(
		streamGateConfigGeneration, "attempt.1", streamGateEnvironment,
		openAIRebuildEndpointChat, openAIRebuildFamily, "",
		streamgate.CommitStateTransportUncommitted, false, false, "",
	)
	if err != nil {
		t.Fatalf("NewRequestFilterContext: %v", err)
	}
	reqSnap, err := snap.BeginRequest(reqCtx)
	if err != nil {
		t.Fatalf("BeginRequest: %v", err)
	}
	target, err := streamgate.NewAttemptTarget("client-model", "ornith:35b", "prov-a", "normalized",
		[]string{"output.repeat_guard", "output.schema_gate", "output.provider_error"})
	if err != nil {
		t.Fatalf("NewAttemptTarget: %v", err)
	}
	resolved, err := reqSnap.ResolveAttempt(target)
	if err != nil {
		t.Fatalf("ResolveAttempt: %v", err)
	}
	byID := make(map[string]streamgate.ResolvedFilter, len(resolved))
	for _, r := range resolved {
		byID[r.FilterID()] = r
	}

	// repeat_guard: subscribed + trigger-ready => evaluated this epoch.
	repeat := bindEpoch(t, byID[openAIRepeatGuardFilterID], true, true)
	if !repeat.EvaluatedForEpoch() {
		t.Errorf("repeat_guard EvaluatedForEpoch() = false, want true (ready rolling filter)")
	}

	// repeat action sibling: subscribed fragments without terminal => deferred.
	repeatAction := bindEpoch(t, byID[openAIRepeatActionGuardFilterID], true, false)
	if got := repeatAction.NormalizeOutcome().Kind(); got != streamgate.FilterOutcomeKindDeferredByRequirement {
		t.Errorf("repeat action outcome = %q, want deferred_by_requirement", got)
	}

	// schema_gate: subscribed, trigger not ready, blocking => deferred.
	schema := bindEpoch(t, byID[openAISchemaGateFilterID], true, false)
	if got := schema.NormalizeOutcome().Kind(); got != streamgate.FilterOutcomeKindDeferredByRequirement {
		t.Errorf("schema_gate outcome = %q, want deferred_by_requirement", got)
	}

	// provider_error: not subscribed on a clean epoch => not applicable.
	provErr := bindEpoch(t, byID[openAIProviderErrorFilterID], false, false)
	if got := provErr.NormalizeOutcome().Kind(); got != streamgate.FilterOutcomeKindNotApplicableForEpoch {
		t.Errorf("provider_error outcome = %q, want not_applicable_for_epoch", got)
	}
}

func bindEpoch(t *testing.T, rf streamgate.ResolvedFilter, subscribed, triggerReady bool) streamgate.EpochFilter {
	t.Helper()
	app, err := streamgate.NewFilterApplicability(rf.FilterID(), subscribed, triggerReady)
	if err != nil {
		t.Fatalf("NewFilterApplicability(%s): %v", rf.FilterID(), err)
	}
	ef, err := rf.BindEpoch(1, app)
	if err != nil {
		t.Fatalf("BindEpoch(%s): %v", rf.FilterID(), err)
	}
	return ef
}

// TestOpenAIProviderErrorFoundationDoesNotReplayUnmatchedError verifies that
// the foundation lifecycle records a sanitized pass and never invents a
// recovery intent before the provider-error matcher Task exists.
func TestOpenAIProviderErrorFoundationDoesNotReplayUnmatchedError(t *testing.T) {
	filter, err := newOpenAIOutputFilter(openAIOutputFilterProviderError, 500, 20, "openai.snap.1")
	if err != nil {
		t.Fatalf("newOpenAIOutputFilter: %v", err)
	}
	fctx, err := streamgate.NewFilterContextBuilder(streamGateConfigGeneration, "attempt.1").
		SetEnvironment(streamGateEnvironment).SetEndpoint(openAIRebuildEndpointChat).
		SetActualModel("ornith:35b").SetActualProvider("prov-a").SetExecutionPath("provider_tunnel").
		SetCommitState(streamgate.CommitStateTransportUncommitted).Build()
	if err != nil {
		t.Fatalf("build fctx: %v", err)
	}
	providerError, err := newOpenAIProviderErrorEvent("provider_tunnel_error")
	if err != nil {
		t.Fatalf("newOpenAIProviderErrorEvent: %v", err)
	}
	batch, err := streamgate.NewEvidenceBatch([]streamgate.NormalizedEvent{providerError}, nil, nil, nil, true, streamgate.CommitStateTransportUncommitted, time.Now())
	if err != nil {
		t.Fatalf("NewEvidenceBatch: %v", err)
	}
	decision, err := filter.Evaluate(context.Background(), fctx, batch)
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if decision.Kind() != streamgate.FilterDecisionKindPass {
		t.Fatalf("decision kind = %q, want pass", decision.Kind())
	}
	if decision.RecoveryIntent() != nil {
		t.Fatalf("unmatched provider error created recovery intent: %+v", decision.RecoveryIntent())
	}
	if got := decision.Evidence().DescriptorCode(); got != "provider_error_observed_unmatched" {
		t.Fatalf("descriptor = %q, want provider_error_observed_unmatched", got)
	}
}

// TestOpenAIRepeatAndSchemaFiltersPassCleanEpoch verifies the rolling and
// terminal-gate participants pass on clean content/terminal epochs.
func TestOpenAIRepeatAndSchemaFiltersPassCleanEpoch(t *testing.T) {
	fctx, err := streamgate.NewFilterContextBuilder(streamGateConfigGeneration, "attempt.1").
		SetEndpoint(openAIRebuildEndpointChat).SetExecutionPath("normalized").
		SetCommitState(streamgate.CommitStateTransportUncommitted).Build()
	if err != nil {
		t.Fatalf("build fctx: %v", err)
	}
	td, err := streamgate.NewTextDeltaEvent(streamGateChannelDefault, "hello", time.Now())
	if err != nil {
		t.Fatalf("NewTextDeltaEvent: %v", err)
	}
	batch, err := streamgate.NewEvidenceBatch([]streamgate.NormalizedEvent{td}, nil, nil, nil, false, streamgate.CommitStateTransportUncommitted, time.Now())
	if err != nil {
		t.Fatalf("NewEvidenceBatch: %v", err)
	}
	for _, kind := range []openAIOutputFilterKind{
		openAIOutputFilterRepeatGuard,
		openAIOutputFilterRepeatActionGuard,
		openAIOutputFilterSchemaGate,
	} {
		filter, err := newOpenAIOutputFilter(kind, 500, 10, "")
		if err != nil {
			t.Fatalf("newOpenAIOutputFilter(%s): %v", kind, err)
		}
		decision, err := filter.Evaluate(context.Background(), fctx, batch)
		if err != nil {
			t.Fatalf("Evaluate(%s): %v", kind, err)
		}
		if decision.Kind() != streamgate.FilterDecisionKindPass {
			t.Errorf("%s clean decision = %q, want pass", kind, decision.Kind())
		}
	}
}

func TestOpenAIRepeatHoldBufferContract(t *testing.T) {
	gateCfg := outputFilterGateCfg(config.StreamGateFilterEnforcementBlocking)
	fctxNoScheme := openAIOutputFilterContext{endpoint: openAIRebuildEndpointChat, requestRef: "openai.snap.1"}
	regsNoScheme, policiesNoScheme, err := openAIOutputFilterRegistrations(gateCfg, fctxNoScheme)
	if err != nil {
		t.Fatalf("openAIOutputFilterRegistrations(no scheme): %v", err)
	}

	snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regsNoScheme, policiesNoScheme)
	if err != nil {
		t.Fatalf("NewFilterRegistrySnapshot: %v", err)
	}
	reqCtx, err := streamgate.NewRequestFilterContext(
		streamGateConfigGeneration, "attempt.1", streamGateEnvironment,
		openAIRebuildEndpointChat, openAIRebuildFamily, "",
		streamgate.CommitStateTransportUncommitted, false, false, "",
	)
	if err != nil {
		t.Fatalf("NewRequestFilterContext: %v", err)
	}
	reqSnap, err := snap.BeginRequest(reqCtx)
	if err != nil {
		t.Fatalf("BeginRequest: %v", err)
	}
	target, err := streamgate.NewAttemptTarget("client-model", "ornith:35b", "prov-a", "normalized",
		[]string{"output.repeat_guard", "output.provider_error"})
	if err != nil {
		t.Fatalf("NewAttemptTarget: %v", err)
	}
	resolved, err := reqSnap.ResolveAttempt(target)
	if err != nil {
		t.Fatalf("ResolveAttempt: %v", err)
	}

	plan, err := streamgate.NewEvidencePlanFromResolvedFilters(resolved)
	if err != nil {
		t.Fatalf("NewEvidencePlanFromResolvedFilters: %v", err)
	}

	const wantBound = openAIRepeatHoldMaxBufferRunes // 1048576
	if got := plan.MaxBufferRunes(streamGateChannelDefault); got != wantBound {
		t.Errorf("composed channel maxBufferRunes = %d, want %d", got, wantBound)
	}

	bindings := plan.BindingsForChannel(streamGateChannelDefault)
	foundRepeat := false
	foundAction := false
	for _, b := range bindings {
		req := b.Requirement()
		switch b.FilterID() {
		case openAIRepeatGuardFilterID:
			foundRepeat = true
			if got := req.MaxBufferRunes(); got != wantBound {
				t.Errorf("repeat_guard MaxBufferRunes = %d, want %d", got, wantBound)
			}
			if got := req.EvidenceRunes(); got != 500 {
				t.Errorf("repeat_guard EvidenceRunes = %d, want 500", got)
			}
		case openAIRepeatActionGuardFilterID:
			foundAction = true
			if got := req.MaxBufferRunes(); got != wantBound {
				t.Errorf("repeat_action_guard MaxBufferRunes = %d, want %d", got, wantBound)
			}
		}
	}
	if !foundRepeat {
		t.Error("missing repeat_guard binding in compiled plan")
	}
	if !foundAction {
		t.Error("missing repeat_action_guard binding in compiled plan")
	}

	// Verify standalone schema requirement retains default bound (4096).
	regsScheme, policiesScheme, err := openAIOutputFilterRegistrations(gateCfg, schemaOutputFilterContext("openai.snap.1"))
	if err != nil {
		t.Fatalf("openAIOutputFilterRegistrations(scheme): %v", err)
	}
	snapScheme, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regsScheme, policiesScheme)
	if err != nil {
		t.Fatalf("NewFilterRegistrySnapshot(scheme): %v", err)
	}
	reqSnapScheme, err := snapScheme.BeginRequest(reqCtx)
	if err != nil {
		t.Fatalf("BeginRequest(scheme): %v", err)
	}
	targetScheme, err := streamgate.NewAttemptTarget("client-model", "ornith:35b", "prov-a", "normalized",
		[]string{"output.repeat_guard", "output.schema_gate", "output.provider_error"})
	if err != nil {
		t.Fatalf("NewAttemptTarget(scheme): %v", err)
	}
	resolvedScheme, err := reqSnapScheme.ResolveAttempt(targetScheme)
	if err != nil {
		t.Fatalf("ResolveAttempt(scheme): %v", err)
	}
	for _, rf := range resolvedScheme {
		if rf.FilterID() == openAISchemaGateFilterID {
			if got := rf.HoldRequirement().MaxBufferRunes(); got != streamgate.DefaultMaxBufferRunes {
				t.Errorf("schema_gate MaxBufferRunes = %d, want default %d", got, streamgate.DefaultMaxBufferRunes)
			}
		}
	}
}
