package streamgate

import (
	"context"
	"errors"
	"reflect"
	"sync"
	"testing"
	"time"
)

type coordinatorCallRecorder struct {
	mu    sync.Mutex
	calls []string
}

func (r *coordinatorCallRecorder) add(call string) {
	r.mu.Lock()
	r.calls = append(r.calls, call)
	r.mu.Unlock()
}

func (r *coordinatorCallRecorder) snapshot() []string {
	r.mu.Lock()
	defer r.mu.Unlock()
	return append([]string(nil), r.calls...)
}

type coordinatorEventSource struct{}

func (coordinatorEventSource) NextEvent(context.Context) (NormalizedEvent, error) {
	return NormalizedEvent{}, errors.New("unused")
}

type coordinatorController struct {
	recorder *coordinatorCallRecorder
	attempt  string
	err      error
	started  chan struct{}
	release  chan struct{}
	once     sync.Once
}

func (c *coordinatorController) AbortAttempt(ctx context.Context) error {
	c.recorder.add("abort:" + c.attempt)
	if c.started != nil {
		c.once.Do(func() { close(c.started) })
	}
	if c.release != nil {
		select {
		case <-c.release:
		case <-ctx.Done():
			return ctx.Err()
		}
	}
	return c.err
}

type coordinatorPreparationSnapshot struct {
	recorder *coordinatorCallRecorder
	ref      string
	err      error
	mu       sync.Mutex
	releases int
}

func (s *coordinatorPreparationSnapshot) SnapshotRef() string { return s.ref }

func (s *coordinatorPreparationSnapshot) Release() error {
	s.recorder.add("snapshot.release:" + s.ref)
	s.mu.Lock()
	s.releases++
	s.mu.Unlock()
	return s.err
}

func (s *coordinatorPreparationSnapshot) releaseCount() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.releases
}

type coordinatorPreparer struct {
	recorder  *coordinatorCallRecorder
	id        string
	directive RecoveryDirective
	err       error
	wait      bool
	mu        sync.Mutex
	calls     int
}

func (p *coordinatorPreparer) PrepareRecoveryPlan(
	ctx context.Context,
	_ RecoveryPlan,
	_ RecoveryPreparationSnapshot,
) (RecoveryDirective, error) {
	p.recorder.add("prepare:" + p.id)
	p.mu.Lock()
	p.calls++
	p.mu.Unlock()
	if p.wait {
		<-ctx.Done()
		return RecoveryDirective{}, ctx.Err()
	}
	return p.directive, p.err
}

func (p *coordinatorPreparer) callCount() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.calls
}

type coordinatorRebuilder struct {
	recorder     *coordinatorCallRecorder
	err          error
	promptTokens int
	retained     uint64
	peak         uint64
	max          uint64
	mu           sync.Mutex
	calls        int
}

func (r *coordinatorRebuilder) RebuildRequest(
	_ context.Context,
	_ RecoveryRequestSnapshotRef,
	plan RecoveryPlan,
) (RebuiltRequestDraft, error) {
	r.recorder.add("rebuild:" + plan.PlanID())
	r.mu.Lock()
	r.calls++
	r.mu.Unlock()
	if r.err != nil {
		return RebuiltRequestDraft{}, r.err
	}
	retained, peak, maxBytes := r.retained, r.peak, r.max
	if maxBytes == 0 {
		retained, peak, maxBytes = 32, 64, 1024
	}
	return NewRebuiltRequestDraftWithIdempotency(
		plan.PlanID(),
		plan.IdempotencyKey(),
		"request."+plan.PlanID(),
		"chat_completions",
		"openai",
		retained,
		peak,
		maxBytes,
		r.promptTokens,
		plan.RequiredCapabilities(),
	)
}

func (r *coordinatorRebuilder) callCount() int {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.calls
}

type coordinatorDispatcher struct {
	recorder *coordinatorCallRecorder
	err      error
	invalid  bool
	mu       sync.Mutex
	calls    int
	requests []RebuiltRequest
}

func (d *coordinatorDispatcher) DispatchAttempt(
	_ context.Context,
	request RebuiltRequest,
) (AttemptBinding, error) {
	d.recorder.add("dispatch:" + request.PlanID())
	d.mu.Lock()
	d.calls++
	call := d.calls
	d.requests = append(d.requests, request)
	d.mu.Unlock()
	if d.err != nil {
		return AttemptBinding{}, d.err
	}
	if d.invalid {
		return AttemptBinding{}, nil
	}
	return NewAttemptBinding(
		"attempt.recovery."+string(rune('a'-1+call)),
		"model.actual",
		"provider.actual",
		"normalized",
		coordinatorEventSource{},
		&coordinatorController{recorder: d.recorder, attempt: "attempt.recovery." + string(rune('a'-1+call))},
	)
}

func (d *coordinatorDispatcher) callCount() int {
	d.mu.Lock()
	defer d.mu.Unlock()
	return d.calls
}

func (d *coordinatorDispatcher) lastRequest() RebuiltRequest {
	d.mu.Lock()
	defer d.mu.Unlock()
	return d.requests[len(d.requests)-1]
}

func coordinatorPolicy(t *testing.T, total int, limits map[RecoveryStrategy]int) RecoveryPolicySnapshot {
	t.Helper()
	policy, err := NewRecoveryPolicySnapshot(total, limits)
	if err != nil {
		t.Fatalf("NewRecoveryPolicySnapshot: %v", err)
	}
	return policy
}

func coordinatorUsage(t *testing.T, total int, byStrategy map[RecoveryStrategy]int) RecoveryUsageSnapshot {
	t.Helper()
	usage, err := NewRecoveryUsageSnapshot(total, byStrategy)
	if err != nil {
		t.Fatalf("NewRecoveryUsageSnapshot: %v", err)
	}
	return usage
}

func coordinatorRequestSnapshot(t *testing.T) RecoveryRequestSnapshotRef {
	t.Helper()
	snapshot, err := NewRecoveryRequestSnapshotRef("request.snapshot", 32, 64, 1024)
	if err != nil {
		t.Fatalf("NewRecoveryRequestSnapshotRef: %v", err)
	}
	return snapshot
}

func coordinatorBinding(t *testing.T, recorder *coordinatorCallRecorder, controller *coordinatorController) AttemptBinding {
	t.Helper()
	if controller == nil {
		controller = &coordinatorController{recorder: recorder, attempt: "attempt.initial"}
	}
	binding, err := NewAttemptBinding(
		"attempt.initial",
		"model.initial",
		"provider.initial",
		"tunnel",
		coordinatorEventSource{},
		controller,
	)
	if err != nil {
		t.Fatalf("NewAttemptBinding: %v", err)
	}
	return binding
}

func newCoordinatorFixture(
	t *testing.T,
	policy RecoveryPolicySnapshot,
	usage RecoveryUsageSnapshot,
	recorder *coordinatorCallRecorder,
	controller *coordinatorController,
	rebuilder *coordinatorRebuilder,
	dispatcher *coordinatorDispatcher,
	preparers map[string]RecoveryPlanPreparer,
	timeout time.Duration,
) *RecoveryCoordinator {
	t.Helper()
	coordinator, err := NewRecoveryCoordinator(RecoveryCoordinatorOptions{
		Policy:             policy,
		Usage:              usage,
		RequestSnapshot:    coordinatorRequestSnapshot(t),
		CurrentBinding:     coordinatorBinding(t, recorder, controller),
		Rebuilder:          rebuilder,
		Dispatcher:         dispatcher,
		Preparers:          preparers,
		PreparationTimeout: timeout,
	})
	if err != nil {
		t.Fatalf("NewRecoveryCoordinator: %v", err)
	}
	return coordinator
}

func coordinatorArbitration(t *testing.T, strategy RecoveryStrategy, priority int) ArbitrationResult {
	t.Helper()
	var (
		directive RecoveryDirective
		err       error
	)
	switch strategy {
	case RecoveryStrategyExactReplay:
		directive, err = NewRecoveryDirectiveExact("request.original")
	case RecoveryStrategySchemaRepair:
		directive, err = NewRecoveryDirectiveSchema("schema.one", "patch.required")
	case RecoveryStrategyContinuationRepair, RecoveryStrategyManagedContinuation:
		directive, err = NewRecoveryDirectiveContinuation(7, "continuation.snapshot")
	default:
		t.Fatalf("unsupported strategy %q", strategy)
	}
	if err != nil {
		t.Fatalf("new recovery directive: %v", err)
	}
	intent, err := NewRecoveryIntent(strategy, directive, "retryable_violation", priority)
	if err != nil {
		t.Fatalf("NewRecoveryIntent: %v", err)
	}
	result, err := NewArbitrationResult(
		ArbitrationActionRecover,
		BaseDispositionTerminalErrorCandidate,
		"filter.retry",
		"rule.retry",
		&intent,
		nil,
	)
	if err != nil {
		t.Fatalf("NewArbitrationResult: %v", err)
	}
	return result
}

func coordinatorCycleInput(t *testing.T, planID string, strategy RecoveryStrategy) RecoveryCycleInput {
	t.Helper()
	commit := CommitStateTransportUncommitted
	terminalReason := TerminalReason("")
	if strategy == RecoveryStrategyContinuationRepair || strategy == RecoveryStrategyManagedContinuation {
		commit = CommitStateStreamOpen
	}
	if strategy == RecoveryStrategyManagedContinuation {
		terminalReason = TerminalReasonOutputCap
	}
	return RecoveryCycleInput{
		Arbitration:          coordinatorArbitration(t, strategy, 10),
		PlanID:               planID,
		IdempotencyKey:       "idem." + planID,
		ConsumerID:           "consumer.retry",
		CommitState:          commit,
		TerminalReason:       terminalReason,
		RequiredCapabilities: []string{"cap.retry"},
	}
}

func TestRecoveryCoordinatorSerialOrderingBindingAndReentry(t *testing.T) {
	recorder := &coordinatorCallRecorder{}
	rebuilder := &coordinatorRebuilder{recorder: recorder, promptTokens: 20}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(
		t,
		coordinatorPolicy(t, 3, nil),
		coordinatorUsage(t, 0, nil),
		recorder,
		nil,
		rebuilder,
		dispatcher,
		nil,
		0,
	)

	input := coordinatorCycleInput(t, "plan.one", RecoveryStrategyExactReplay)
	result, err := coordinator.Execute(context.Background(), input)
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if got, want := recorder.snapshot(), []string{
		"abort:attempt.initial",
		"rebuild:plan.one",
		"dispatch:plan.one",
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("call order = %v, want %v", got, want)
	}
	plan, ok := result.Plan()
	if !ok || plan.PlanID() != "plan.one" || plan.ResumeMode() != RecoveryResumeModeReplaceAttempt {
		t.Fatalf("unexpected plan: %#v, %v", plan, ok)
	}
	binding, ok := result.Binding()
	if !ok || binding.AttemptID() != "attempt.recovery.a" || binding.Model() != "model.actual" || binding.Provider() != "provider.actual" || binding.ExecutionPath() != "normalized" {
		t.Fatalf("unexpected binding: %#v, %v", binding, ok)
	}
	if got := result.UsageSnapshot().RequestFaultRecoveries(); got != 1 {
		t.Fatalf("result usage = %d, want 1", got)
	}
	if current, ok := coordinator.CurrentBinding(); !ok || current.AttemptID() != binding.AttemptID() {
		t.Fatalf("current binding = %#v, %v", current, ok)
	}

	input.RequiredCapabilities[0] = "cap.mutated"
	if got := plan.RequiredCapabilities(); !reflect.DeepEqual(got, []string{"cap.retry"}) {
		t.Fatalf("plan capabilities mutated: %v", got)
	}
	ids := coordinator.UsedPlanIDs()
	ids[0] = "changed"
	if got := coordinator.UsedPlanIDs(); !reflect.DeepEqual(got, []string{"plan.one"}) {
		t.Fatalf("used plan ids mutated: %v", got)
	}

	if _, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.one", RecoveryStrategyExactReplay)); !errors.Is(err, ErrRecoveryPlanReentry) {
		t.Fatalf("re-entry error = %v, want %v", err, ErrRecoveryPlanReentry)
	}
	if dispatcher.callCount() != 1 || rebuilder.callCount() != 1 {
		t.Fatal("re-entry performed host side effects")
	}
}

func TestRecoveryCoordinatorPreparationOrderingAndSnapshotLifetime(t *testing.T) {
	recorder := &coordinatorCallRecorder{}
	preparedDirective, err := NewRecoveryDirectiveContinuation(13, "continuation.prepared")
	if err != nil {
		t.Fatal(err)
	}
	preparer := &coordinatorPreparer{recorder: recorder, id: "preparer.one", directive: preparedDirective}
	snapshot := &coordinatorPreparationSnapshot{recorder: recorder, ref: "preparation.snapshot"}
	rebuilder := &coordinatorRebuilder{recorder: recorder, promptTokens: 40}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(
		t,
		coordinatorPolicy(t, 3, nil),
		coordinatorUsage(t, 0, nil),
		recorder,
		nil,
		rebuilder,
		dispatcher,
		map[string]RecoveryPlanPreparer{"preparer.one": preparer},
		time.Second,
	)
	input := coordinatorCycleInput(t, "plan.prepare", RecoveryStrategyContinuationRepair)
	input.PreparerID = "preparer.one"
	input.PreparationSnapshot = snapshot

	result, err := coordinator.Execute(context.Background(), input)
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if got, want := recorder.snapshot(), []string{
		"abort:attempt.initial",
		"prepare:preparer.one",
		"snapshot.release:preparation.snapshot",
		"rebuild:plan.prepare",
		"dispatch:plan.prepare",
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("call order = %v, want %v", got, want)
	}
	if preparer.callCount() != 1 || snapshot.releaseCount() != 1 {
		t.Fatalf("preparer calls=%d releases=%d, want 1/1", preparer.callCount(), snapshot.releaseCount())
	}
	plan, ok := result.Plan()
	if !ok || plan.PreparationStatus() != RecoveryPreparationPrepared || plan.Directive().Cursor() != 13 || plan.ResumeMode() != RecoveryResumeModeContinueStream {
		t.Fatalf("prepared plan = %#v, %v", plan, ok)
	}

	retrySnapshot := &coordinatorPreparationSnapshot{recorder: recorder, ref: "preparation.retry"}
	retry := coordinatorCycleInput(t, "plan.prepare", RecoveryStrategyContinuationRepair)
	retry.PreparerID = "preparer.one"
	retry.PreparationSnapshot = retrySnapshot
	if _, err := coordinator.Execute(context.Background(), retry); !errors.Is(err, ErrRecoveryPlanReentry) {
		t.Fatalf("prepared plan re-entry error = %v, want %v", err, ErrRecoveryPlanReentry)
	}
	if preparer.callCount() != 1 || dispatcher.callCount() != 1 {
		t.Fatalf("re-entry preparer=%d dispatcher=%d, want 1/1", preparer.callCount(), dispatcher.callCount())
	}
	if retrySnapshot.releaseCount() != 1 {
		t.Fatalf("rejected re-entry snapshot releases = %d, want 1", retrySnapshot.releaseCount())
	}
}

func TestRecoveryCoordinatorFailureBarriers(t *testing.T) {
	hostErr := errors.New("raw host detail")
	validContinuation, _ := NewRecoveryDirectiveContinuation(9, "continuation.prepared")
	invalidExact, _ := NewRecoveryDirectiveExact("request.other")
	managedExhausted := mustManagedBudget(t, 10, 100, 10, 0, nil, 0)

	tests := []struct {
		name             string
		strategy         RecoveryStrategy
		controllerErr    error
		preparer         *coordinatorPreparer
		snapshotErr      error
		rebuildErr       error
		dispatchErr      error
		dispatchInvalid  bool
		managed          *ManagedTrajectoryBudget
		wantErr          error
		wantCalls        []string
		wantUsage        int
		wantSnapshotCall int
	}{
		{
			name:             "abort failure",
			strategy:         RecoveryStrategyContinuationRepair,
			controllerErr:    hostErr,
			preparer:         &coordinatorPreparer{id: "preparer.one", directive: validContinuation},
			wantErr:          ErrRecoveryAbortFailed,
			wantCalls:        []string{"abort:attempt.initial", "snapshot.release:preparation.snapshot"},
			wantSnapshotCall: 1,
		},
		{
			name:             "preparer failure",
			strategy:         RecoveryStrategyContinuationRepair,
			preparer:         &coordinatorPreparer{id: "preparer.one", directive: validContinuation, err: hostErr},
			wantErr:          ErrRecoveryPreparationFailed,
			wantCalls:        []string{"abort:attempt.initial", "prepare:preparer.one", "snapshot.release:preparation.snapshot"},
			wantSnapshotCall: 1,
		},
		{
			name:             "preparer deadline",
			strategy:         RecoveryStrategyContinuationRepair,
			preparer:         &coordinatorPreparer{id: "preparer.one", wait: true},
			wantErr:          ErrRecoveryPreparationDeadline,
			wantCalls:        []string{"abort:attempt.initial", "prepare:preparer.one", "snapshot.release:preparation.snapshot"},
			wantSnapshotCall: 1,
		},
		{
			name:             "preparer invalid directive",
			strategy:         RecoveryStrategyContinuationRepair,
			preparer:         &coordinatorPreparer{id: "preparer.one", directive: invalidExact},
			wantErr:          ErrRecoveryPreparationInvalid,
			wantCalls:        []string{"abort:attempt.initial", "prepare:preparer.one", "snapshot.release:preparation.snapshot"},
			wantSnapshotCall: 1,
		},
		{
			name:             "snapshot release failure",
			strategy:         RecoveryStrategyContinuationRepair,
			preparer:         &coordinatorPreparer{id: "preparer.one", directive: validContinuation},
			snapshotErr:      hostErr,
			wantErr:          ErrRecoveryPreparationSnapshotRelease,
			wantCalls:        []string{"abort:attempt.initial", "prepare:preparer.one", "snapshot.release:preparation.snapshot"},
			wantSnapshotCall: 1,
		},
		{
			name:       "rebuild failure",
			strategy:   RecoveryStrategyExactReplay,
			rebuildErr: hostErr,
			wantErr:    ErrRecoveryRebuildFailed,
			wantCalls:  []string{"abort:attempt.initial", "rebuild:plan.failure"},
		},
		{
			name:      "managed allowance exhausted",
			strategy:  RecoveryStrategyManagedContinuation,
			managed:   &managedExhausted,
			wantErr:   ErrManagedTrajectoryExhausted,
			wantCalls: []string{"abort:attempt.initial", "rebuild:plan.failure"},
		},
		{
			name:        "dispatch failure consumes usage",
			strategy:    RecoveryStrategyExactReplay,
			dispatchErr: hostErr,
			wantErr:     ErrRecoveryDispatchFailed,
			wantCalls:   []string{"abort:attempt.initial", "rebuild:plan.failure", "dispatch:plan.failure"},
			wantUsage:   1,
		},
		{
			name:            "invalid binding consumes usage",
			strategy:        RecoveryStrategyExactReplay,
			dispatchInvalid: true,
			wantErr:         ErrRecoveryDispatchFailed,
			wantCalls:       []string{"abort:attempt.initial", "rebuild:plan.failure", "dispatch:plan.failure"},
			wantUsage:       1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			recorder := &coordinatorCallRecorder{}
			controller := &coordinatorController{recorder: recorder, attempt: "attempt.initial", err: tt.controllerErr}
			rebuilder := &coordinatorRebuilder{recorder: recorder, err: tt.rebuildErr}
			if tt.strategy == RecoveryStrategyManagedContinuation {
				rebuilder.promptTokens = 100
			}
			dispatcher := &coordinatorDispatcher{recorder: recorder, err: tt.dispatchErr, invalid: tt.dispatchInvalid}
			var preparers map[string]RecoveryPlanPreparer
			var snapshot *coordinatorPreparationSnapshot
			if tt.preparer != nil {
				tt.preparer.recorder = recorder
				preparers = map[string]RecoveryPlanPreparer{"preparer.one": tt.preparer}
				snapshot = &coordinatorPreparationSnapshot{recorder: recorder, ref: "preparation.snapshot", err: tt.snapshotErr}
			}
			coordinator := newCoordinatorFixture(
				t,
				coordinatorPolicy(t, 3, nil),
				coordinatorUsage(t, 0, nil),
				recorder,
				controller,
				rebuilder,
				dispatcher,
				preparers,
				5*time.Millisecond,
			)
			input := coordinatorCycleInput(t, "plan.failure", tt.strategy)
			input.ManagedTrajectory = tt.managed
			if snapshot != nil {
				input.PreparerID = "preparer.one"
				input.PreparationSnapshot = snapshot
			}

			result, err := coordinator.Execute(context.Background(), input)
			if !errors.Is(err, tt.wantErr) {
				t.Fatalf("Execute error = %v, want %v", err, tt.wantErr)
			}
			if got := recorder.snapshot(); !reflect.DeepEqual(got, tt.wantCalls) {
				t.Fatalf("calls = %v, want %v", got, tt.wantCalls)
			}
			if got := result.UsageSnapshot().RequestFaultRecoveries(); got != tt.wantUsage {
				t.Fatalf("result usage = %d, want %d", got, tt.wantUsage)
			}
			if got := coordinator.UsageSnapshot().RequestFaultRecoveries(); got != tt.wantUsage {
				t.Fatalf("coordinator usage = %d, want %d", got, tt.wantUsage)
			}
			if snapshot != nil && snapshot.releaseCount() != tt.wantSnapshotCall {
				t.Fatalf("snapshot releases = %d, want %d", snapshot.releaseCount(), tt.wantSnapshotCall)
			}
			if dispatcher.callCount() > 1 {
				t.Fatalf("dispatcher called %d times", dispatcher.callCount())
			}
		})
	}
}

func TestRecoveryCoordinatorBudgetCapsAndBoundedCycles(t *testing.T) {
	t.Run("zero policy rejects before abort", func(t *testing.T) {
		recorder := &coordinatorCallRecorder{}
		rebuilder := &coordinatorRebuilder{recorder: recorder}
		dispatcher := &coordinatorDispatcher{recorder: recorder}
		coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, 0, nil), coordinatorUsage(t, 0, nil), recorder, nil, rebuilder, dispatcher, nil, 0)
		_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.zero", RecoveryStrategyExactReplay))
		if !errors.Is(err, ErrRecoveryRequestBudgetExhausted) {
			t.Fatalf("error = %v, want request cap", err)
		}
		if len(recorder.snapshot()) != 0 {
			t.Fatalf("host calls before zero-cap rejection: %v", recorder.snapshot())
		}
	})

	for _, total := range []int{1, 3} {
		t.Run("total cap "+string(rune('0'+total)), func(t *testing.T) {
			recorder := &coordinatorCallRecorder{}
			rebuilder := &coordinatorRebuilder{recorder: recorder}
			dispatcher := &coordinatorDispatcher{recorder: recorder}
			coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, total, nil), coordinatorUsage(t, 0, nil), recorder, nil, rebuilder, dispatcher, nil, 0)
			for i := 0; i < total; i++ {
				planID := "plan.cap." + string(rune('a'+i))
				if _, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, planID, RecoveryStrategyExactReplay)); err != nil {
					t.Fatalf("cycle %d: %v", i, err)
				}
			}
			_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.cap.exhausted", RecoveryStrategyExactReplay))
			if !errors.Is(err, ErrRecoveryRequestBudgetExhausted) {
				t.Fatalf("exhausted error = %v", err)
			}
			if dispatcher.callCount() != total || coordinator.UsageSnapshot().RequestFaultRecoveries() != total {
				t.Fatalf("dispatch=%d usage=%d, want %d", dispatcher.callCount(), coordinator.UsageSnapshot().RequestFaultRecoveries(), total)
			}
			if got := len(recorder.snapshot()); got != total*3 {
				t.Fatalf("host call count = %d, want %d", got, total*3)
			}
		})
	}

	t.Run("strategy cap rejects without alternate plan", func(t *testing.T) {
		recorder := &coordinatorCallRecorder{}
		rebuilder := &coordinatorRebuilder{recorder: recorder}
		dispatcher := &coordinatorDispatcher{recorder: recorder}
		policy := coordinatorPolicy(t, 3, map[RecoveryStrategy]int{
			RecoveryStrategyExactReplay:        1,
			RecoveryStrategyContinuationRepair: 2,
		})
		coordinator := newCoordinatorFixture(t, policy, coordinatorUsage(t, 0, nil), recorder, nil, rebuilder, dispatcher, nil, 0)
		if _, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.strategy.a", RecoveryStrategyExactReplay)); err != nil {
			t.Fatal(err)
		}
		_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.strategy.b", RecoveryStrategyExactReplay))
		if !errors.Is(err, ErrRecoveryStrategyBudgetExhausted) {
			t.Fatalf("error = %v, want strategy cap", err)
		}
		if dispatcher.callCount() != 1 || len(recorder.snapshot()) != 3 {
			t.Fatal("strategy-cap rejection performed host side effects")
		}
	})

	t.Run("request total precedes selected strategy cap", func(t *testing.T) {
		recorder := &coordinatorCallRecorder{}
		rebuilder := &coordinatorRebuilder{recorder: recorder}
		dispatcher := &coordinatorDispatcher{recorder: recorder}
		policy := coordinatorPolicy(t, 1, map[RecoveryStrategy]int{
			RecoveryStrategyExactReplay: 1,
		})
		usage := coordinatorUsage(t, 1, map[RecoveryStrategy]int{
			RecoveryStrategyExactReplay: 1,
		})
		coordinator := newCoordinatorFixture(t, policy, usage, recorder, nil, rebuilder, dispatcher, nil, 0)
		_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.total.first", RecoveryStrategyExactReplay))
		if !errors.Is(err, ErrRecoveryRequestBudgetExhausted) {
			t.Fatalf("error = %v, want request-total exhaustion", err)
		}
		if errors.Is(err, ErrRecoveryStrategyBudgetExhausted) {
			t.Fatalf("request-total exhaustion was reported as strategy exhaustion: %v", err)
		}
		if len(recorder.snapshot()) != 0 {
			t.Fatalf("request-total rejection performed host calls: %v", recorder.snapshot())
		}
	})

	if _, err := NewRecoveryPolicySnapshot(4, nil); err == nil {
		t.Fatal("policy accepted 4 recoveries above MVP absolute cap")
	}
}

func TestRecoveryCoordinatorConcurrentCycleAndEligibilityGuards(t *testing.T) {
	t.Run("concurrent cycle is rejected", func(t *testing.T) {
		recorder := &coordinatorCallRecorder{}
		started := make(chan struct{})
		release := make(chan struct{})
		controller := &coordinatorController{recorder: recorder, attempt: "attempt.initial", started: started, release: release}
		rebuilder := &coordinatorRebuilder{recorder: recorder}
		dispatcher := &coordinatorDispatcher{recorder: recorder}
		coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, controller, rebuilder, dispatcher, nil, 0)

		firstDone := make(chan error, 1)
		go func() {
			_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.concurrent.a", RecoveryStrategyExactReplay))
			firstDone <- err
		}()
		<-started
		_, err := coordinator.Execute(context.Background(), coordinatorCycleInput(t, "plan.concurrent.b", RecoveryStrategyExactReplay))
		if !errors.Is(err, ErrRecoveryCycleInProgress) {
			t.Fatalf("concurrent error = %v", err)
		}
		close(release)
		if err := <-firstDone; err != nil {
			t.Fatalf("first cycle: %v", err)
		}
		if dispatcher.callCount() != 1 {
			t.Fatalf("dispatch calls = %d, want 1", dispatcher.callCount())
		}
	})

	tests := []struct {
		name   string
		mutate func(*RecoveryCycleInput, *context.Context)
		want   error
	}{
		{
			name: "caller context canceled",
			mutate: func(_ *RecoveryCycleInput, ctx *context.Context) {
				canceled, cancel := context.WithCancel(context.Background())
				cancel()
				*ctx = canceled
			},
			want: ErrRecoveryCallerCanceled,
		},
		{
			name: "caller flag canceled",
			mutate: func(in *RecoveryCycleInput, _ *context.Context) {
				in.CallerCanceled = true
			},
			want: ErrRecoveryCallerCanceled,
		},
		{
			name: "tool side effect",
			mutate: func(in *RecoveryCycleInput, _ *context.Context) {
				in.HasToolSideEffect = true
			},
			want: ErrRecoverySideEffect,
		},
		{
			name: "complete tool call",
			mutate: func(in *RecoveryCycleInput, _ *context.Context) {
				in.HasCompleteToolCall = true
			},
			want: ErrRecoverySideEffect,
		},
		{
			name: "commit ineligible",
			mutate: func(in *RecoveryCycleInput, _ *context.Context) {
				in.CommitState = CommitStateStreamOpen
			},
			want: ErrRecoveryCommitIneligible,
		},
		{
			name: "terminal committed",
			mutate: func(in *RecoveryCycleInput, _ *context.Context) {
				in.CommitState = CommitStateTerminalCommitted
			},
			want: ErrRecoveryTerminalCommitted,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			recorder := &coordinatorCallRecorder{}
			rebuilder := &coordinatorRebuilder{recorder: recorder}
			dispatcher := &coordinatorDispatcher{recorder: recorder}
			coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, nil, rebuilder, dispatcher, nil, 0)
			input := coordinatorCycleInput(t, "plan.guard", RecoveryStrategyExactReplay)
			ctx := context.Context(context.Background())
			tt.mutate(&input, &ctx)
			_, err := coordinator.Execute(ctx, input)
			if !errors.Is(err, tt.want) {
				t.Fatalf("error = %v, want %v", err, tt.want)
			}
			if len(recorder.snapshot()) != 0 || coordinator.UsageSnapshot().RequestFaultRecoveries() != 0 {
				t.Fatalf("eligibility guard had side effects: calls=%v usage=%d", recorder.snapshot(), coordinator.UsageSnapshot().RequestFaultRecoveries())
			}
		})
	}
}

func TestRecoveryCoordinatorManagedTrajectoryMultiCycle(t *testing.T) {
	recorder := &coordinatorCallRecorder{}
	rebuilder := &coordinatorRebuilder{recorder: recorder, promptTokens: 70}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, 0, nil), coordinatorUsage(t, 0, nil), recorder, nil, rebuilder, dispatcher, nil, 0)

	callerCap := 50
	firstBudget, err := NewManagedTrajectoryBudget(20, 100, 10, 0, &callerCap, 0)
	if err != nil {
		t.Fatal(err)
	}
	first := coordinatorCycleInput(t, "plan.managed.a", RecoveryStrategyManagedContinuation)
	first.ManagedTrajectory = &firstBudget
	firstResult, err := coordinator.Execute(context.Background(), first)
	if err != nil {
		t.Fatalf("first managed cycle: %v", err)
	}
	firstPlan, _ := firstResult.Plan()
	if firstPlan.ManagedAllowance() != 20 || dispatcher.lastRequest().ManagedAllowance() != 20 {
		t.Fatalf("first allowance plan=%d request=%d", firstPlan.ManagedAllowance(), dispatcher.lastRequest().ManagedAllowance())
	}

	rebuilder.promptTokens = 80
	secondBudget, err := NewManagedTrajectoryBudget(20, 100, 10, 0, &callerCap, 20)
	if err != nil {
		t.Fatal(err)
	}
	second := coordinatorCycleInput(t, "plan.managed.b", RecoveryStrategyManagedContinuation)
	second.ManagedTrajectory = &secondBudget
	secondResult, err := coordinator.Execute(context.Background(), second)
	if err != nil {
		t.Fatalf("second managed cycle: %v", err)
	}
	secondPlan, _ := secondResult.Plan()
	if secondPlan.ManagedAllowance() != 10 || dispatcher.lastRequest().ManagedAllowance() != 10 {
		t.Fatalf("second allowance plan=%d request=%d", secondPlan.ManagedAllowance(), dispatcher.lastRequest().ManagedAllowance())
	}
	if coordinator.UsageSnapshot().RequestFaultRecoveries() != 0 {
		t.Fatal("managed cycles consumed fault budget")
	}
	if dispatcher.callCount() != 2 {
		t.Fatalf("managed dispatches = %d, want 2", dispatcher.callCount())
	}
	for _, result := range []RecoveryCycleResult{firstResult, secondResult} {
		plan, _ := result.Plan()
		if plan.ResumeMode() != RecoveryResumeModeContinueStream || result.FailureCauses().Len() != 0 {
			t.Fatalf("managed result plan=%#v causes=%d", plan, result.FailureCauses().Len())
		}
	}
}

func TestRecoveryCoordinatorCauseChainRemainsBounded(t *testing.T) {
	causes := make([]FailureCause, 0, MaxFailureCauses-1)
	for i := 0; i < MaxFailureCauses-1; i++ {
		cause, err := NewFailureCause("filter", "cause_"+string(rune('a'+i)), "", "", "")
		if err != nil {
			t.Fatal(err)
		}
		causes = append(causes, cause)
	}
	chain, err := NewFailureCauseChain(causes)
	if err != nil {
		t.Fatal(err)
	}
	recorder := &coordinatorCallRecorder{}
	controller := &coordinatorController{recorder: recorder, attempt: "attempt.initial", err: errors.New("raw abort")}
	rebuilder := &coordinatorRebuilder{recorder: recorder}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, controller, rebuilder, dispatcher, nil, 0)
	input := coordinatorCycleInput(t, "plan.causes", RecoveryStrategyExactReplay)
	input.FailureCauses = chain
	result, err := coordinator.Execute(context.Background(), input)
	if !errors.Is(err, ErrRecoveryAbortFailed) {
		t.Fatalf("error = %v", err)
	}
	got := result.FailureCauses()
	if got.Len() != MaxFailureCauses {
		t.Fatalf("cause count = %d, want %d", got.Len(), MaxFailureCauses)
	}
	last, err := got.At(MaxFailureCauses - 1)
	if err != nil || last.Code() != "attempt_abort_failed" {
		t.Fatalf("last cause = %#v, %v", last, err)
	}
}

// TestRecoveryCoordinatorTwoViolationsSinglePlan verifies that two filter
// violations from the same epoch are arbitrated into one deterministic winner
// and that the coordinator executes exactly one abort/rebuild/dispatch bundle
// driven by the arbiter's single RecoveryIntent. This closes the Arbiter ->
// Coordinator seam for S14.
func TestRecoveryCoordinatorTwoViolationsSinglePlan(t *testing.T) {
	ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	evidence := buildValidEvidence(t, ts)

	// Two violations with different stable filters, same priority.
	// Tie-break is ascending filterID, so "a.recovery" wins over "z.recovery".
	dir, err := NewRecoveryDirectiveExact("request.original")
	if err != nil {
		t.Fatal(err)
	}
	intent, err := NewRecoveryIntent(RecoveryStrategyExactReplay, dir, "rule.violated", 100)
	if err != nil {
		t.Fatal(err)
	}

	winDecision, err := NewFilterDecision(
		FilterDecisionKindViolation, "c.winner", "a.recovery", "r.winner", evidence, &intent,
	)
	if err != nil {
		t.Fatal(err)
	}
	loseDecision, err := NewFilterDecision(
		FilterDecisionKindViolation, "c.loser", "z.recovery", "r.loser", evidence, &intent,
	)
	if err != nil {
		t.Fatal(err)
	}

	winOutcome := buildBlockingOutcome(t, "a.recovery", 100, winDecision)
	loseOutcome := buildBlockingOutcome(t, "z.recovery", 100, loseDecision)

	set, err := NewEvaluationSet([]EpochFilterOutcome{winOutcome, loseOutcome})
	if err != nil {
		t.Fatalf("NewEvaluationSet: %v", err)
	}

	batch, err := NewEvidenceBatch(
		[]NormalizedEvent{buildValidEvent(t, EventKindTextDelta, ts)},
		map[string][]NormalizedEvent{}, map[string][]NormalizedEvent{},
		nil, false, CommitStateTransportUncommitted, ts,
	)
	if err != nil {
		t.Fatalf("NewEvidenceBatch: %v", err)
	}

	arbitration, err := Arbitrate(context.Background(), batch, set)
	if err != nil {
		t.Fatalf("Arbitrate: %v", err)
	}
	if arbitration.Action() != ArbitrationActionRecover {
		t.Fatalf("Arbitrate action = %v, want recover", arbitration.Action())
	}
	if arbitration.FilterID() != "a.recovery" || arbitration.RuleID() != "r.winner" {
		t.Fatalf("Arbitrate winner = %s/%s, want a.recovery/r.winner", arbitration.FilterID(), arbitration.RuleID())
	}

	recorder := &coordinatorCallRecorder{}
	rebuilder := &coordinatorRebuilder{recorder: recorder, promptTokens: 20}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(
		t,
		coordinatorPolicy(t, 3, nil),
		coordinatorUsage(t, 0, nil),
		recorder,
		nil,
		rebuilder,
		dispatcher,
		nil,
		0,
	)

	planID := "plan.two.violations"
	input := RecoveryCycleInput{
		Arbitration:          arbitration,
		PlanID:               planID,
		IdempotencyKey:       "idem." + planID,
		ConsumerID:           "c.winner",
		CommitState:          CommitStateTransportUncommitted,
		RequiredCapabilities: []string{"cap.retry"},
	}

	result, err := coordinator.Execute(context.Background(), input)
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}

	// Exactly one abort/rebuild/dispatch bundle.
	if got, want := recorder.snapshot(), []string{
		"abort:attempt.initial",
		"rebuild:" + planID,
		"dispatch:" + planID,
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("call order = %v, want %v", got, want)
	}

	// Dispatcher and rebuilder each called exactly once.
	if dispatcher.callCount() != 1 {
		t.Fatalf("dispatcher calls = %d, want 1", dispatcher.callCount())
	}
	if rebuilder.callCount() != 1 {
		t.Fatalf("rebuilder calls = %d, want 1", rebuilder.callCount())
	}

	// Winner contributor identity preserved in coordinator plan.
	plan, ok := result.Plan()
	if !ok {
		t.Fatalf("no plan returned")
	}
	if plan.PlanID() != planID {
		t.Fatalf("plan.PlanID() = %s, want %s", plan.PlanID(), planID)
	}
	contributors := plan.Contributors()
	if len(contributors) != 1 ||
		contributors[0].ConsumerID() != "c.winner" ||
		contributors[0].FilterID() != "a.recovery" ||
		contributors[0].RuleID() != "r.winner" {
		t.Fatalf("plan contributors = %v, want c.winner/a.recovery/r.winner", contributors)
	}

	// Single plan in the coordinator ledger.
	usedIDs := coordinator.UsedPlanIDs()
	if len(usedIDs) != 1 || usedIDs[0] != planID {
		t.Fatalf("used plan IDs = %v, want [%s]", usedIDs, planID)
	}

	// Fault usage consumed by the single dispatch.
	if got := result.UsageSnapshot().RequestFaultRecoveries(); got != 1 {
		t.Fatalf("result usage = %d, want 1", got)
	}

	// Re-entry with the same plan is rejected without additional host calls.
	if _, err := coordinator.Execute(context.Background(), input); !errors.Is(err, ErrRecoveryPlanReentry) {
		t.Fatalf("re-entry error = %v, want %v", err, ErrRecoveryPlanReentry)
	}
	if dispatcher.callCount() != 1 || rebuilder.callCount() != 1 {
		t.Fatal("re-entry performed host side effects")
	}
}

// TestRecoveryCoordinatorAbortTimeoutBlocksDownstream verifies that when a
// blocking abort is blocked by the caller's context deadline, the coordinator
// performs zero prepare/rebuild/dispatch side effects, zero fault usage, one
// snapshot release, and preserves the current binding. This closes the S20
// no-overlapping-live-attempt barrier.
func TestRecoveryCoordinatorAbortTimeoutBlocksDownstream(t *testing.T) {
	boundedTimeout := 30 * time.Millisecond

	recorder := &coordinatorCallRecorder{}
	started := make(chan struct{})
	release := make(chan struct{}) // never closed -> abort blocks until ctx deadline
	controller := &coordinatorController{
		recorder: recorder,
		attempt:  "attempt.initial",
		started:  started,
		release:  release,
	}

	// Continuation strategy requires preparation so we can assert the prepare
	// step is also blocked.
	preparedDirective, err := NewRecoveryDirectiveContinuation(7, "continuation.snapshot")
	if err != nil {
		t.Fatal(err)
	}
	preparer := &coordinatorPreparer{recorder: recorder, id: "preparer.one", directive: preparedDirective}
	snapshot := &coordinatorPreparationSnapshot{recorder: recorder, ref: "preparation.snapshot"}
	rebuilder := &coordinatorRebuilder{recorder: recorder, promptTokens: 40}
	dispatcher := &coordinatorDispatcher{recorder: recorder}
	coordinator := newCoordinatorFixture(
		t,
		coordinatorPolicy(t, 3, nil),
		coordinatorUsage(t, 0, nil),
		recorder,
		controller,
		rebuilder,
		dispatcher,
		map[string]RecoveryPlanPreparer{"preparer.one": preparer},
		5*time.Second, // generous prepare timeout; ctx deadline wins first
	)

	input := coordinatorCycleInput(t, "plan.abort.timeout", RecoveryStrategyContinuationRepair)
	input.PreparerID = "preparer.one"
	input.PreparationSnapshot = snapshot

	ctx, cancel := context.WithTimeout(context.Background(), boundedTimeout)
	defer cancel()

	result, err := coordinator.Execute(ctx, input)

	// The abort blocks until ctx deadline; the coordinator returns
	// ErrRecoveryAbortFailed (the raw ctx.Err() from the blocking abort).
	if !errors.Is(err, ErrRecoveryAbortFailed) {
		t.Fatalf("Execute error = %v, want %v", err, ErrRecoveryAbortFailed)
	}

	// The started channel confirms the abort began (not skipped).
	select {
	case <-started:
	default:
		t.Fatal("abort did not start before deadline")
	}

	// Zero downstream side effects: no prepare, no rebuild, no dispatch.
	if preparer.callCount() != 0 {
		t.Fatalf("preparer calls = %d, want 0", preparer.callCount())
	}
	if rebuilder.callCount() != 0 {
		t.Fatalf("rebuilder calls = %d, want 0", rebuilder.callCount())
	}
	if dispatcher.callCount() != 0 {
		t.Fatalf("dispatcher calls = %d, want 0", dispatcher.callCount())
	}

	// Snapshot release must happen via the coordinator's deferred release.
	if snapshot.releaseCount() != 1 {
		t.Fatalf("snapshot releases = %d, want 1", snapshot.releaseCount())
	}

	// Fault usage must remain zero (dispatch never began).
	if got := coordinator.UsageSnapshot().RequestFaultRecoveries(); got != 0 {
		t.Fatalf("coordinator usage = %d, want 0", got)
	}
	if got := result.UsageSnapshot().RequestFaultRecoveries(); got != 0 {
		t.Fatalf("result usage = %d, want 0", got)
	}

	// Current binding must be preserved (abort never completed).
	binding, ok := coordinator.CurrentBinding()
	if !ok {
		t.Fatal("current binding lost after abort timeout")
	}
	if binding.AttemptID() != "attempt.initial" {
		t.Fatalf("current binding = %s, want attempt.initial", binding.AttemptID())
	}

	// Single plan ledger: the plan was constructed but never dispatched.
	usedIDs := coordinator.UsedPlanIDs()
	if len(usedIDs) != 1 || usedIDs[0] != "plan.abort.timeout" {
		t.Fatalf("used plan IDs = %v, want [plan.abort.timeout]", usedIDs)
	}

	// Recorder order: abort then snapshot.release (defer runs after early return).
	if got, want := recorder.snapshot(), []string{
		"abort:attempt.initial",
		"snapshot.release:preparation.snapshot",
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("call order = %v, want %v", got, want)
	}
}

// buildBlockingOutcome constructs a blocking EpochFilterOutcome from an
// evaluated decision. Helper for S14 seam fixture.
func buildBlockingOutcome(t *testing.T, filterID string, priority int, decision FilterDecision) EpochFilterOutcome {
	t.Helper()
	oc, err := NewFilterOutcomeEvaluated(decision)
	if err != nil {
		t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
	}
	o, err := NewEpochFilterOutcome(filterID, priority, oc, FilterEnforcementBlocking)
	if err != nil {
		t.Fatalf("NewEpochFilterOutcome: %v", err)
	}
	return o
}

func TestRecoveryCoordinatorReportsPreviousAttemptClosure(t *testing.T) {
	ctx := context.Background()
	recorder := &coordinatorCallRecorder{}

	// Case 1: Pre-abort error (e.g. invalid arbitration) -> previousAttemptClosed = false
	controller1 := &coordinatorController{recorder: recorder, attempt: "attempt.1"}
	rebuilder1 := &coordinatorRebuilder{recorder: recorder}
	dispatcher1 := &coordinatorDispatcher{recorder: recorder}
	coord1 := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, controller1, rebuilder1, dispatcher1, nil, 5*time.Millisecond)

	inputInvalid := coordinatorCycleInput(t, "plan.1", RecoveryStrategyExactReplay)
	inputInvalid.Arbitration = ArbitrationResult{} // invalid arbitration

	res1, _ := coord1.Execute(ctx, inputInvalid)
	if res1.PreviousAttemptClosed() {
		t.Errorf("PreviousAttemptClosed = true on pre-abort error, want false")
	}

	// Case 2: Post-abort error (e.g. rebuild error) -> previousAttemptClosed = true
	controller2 := &coordinatorController{recorder: recorder, attempt: "attempt.2"}
	rebuilder2 := &coordinatorRebuilder{recorder: recorder, err: errors.New("rebuild err")}
	dispatcher2 := &coordinatorDispatcher{recorder: recorder}
	coord2 := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, controller2, rebuilder2, dispatcher2, nil, 5*time.Millisecond)

	inputRebuildFail := coordinatorCycleInput(t, "plan.2", RecoveryStrategyExactReplay)
	res2, _ := coord2.Execute(ctx, inputRebuildFail)
	if !res2.PreviousAttemptClosed() {
		t.Errorf("PreviousAttemptClosed = false on post-abort error, want true")
	}

	// Case 3: Successful recovery cycle -> previousAttemptClosed = true
	controller3 := &coordinatorController{recorder: recorder, attempt: "attempt.3"}
	rebuilder3 := &coordinatorRebuilder{recorder: recorder}
	dispatcher3 := &coordinatorDispatcher{recorder: recorder}
	coord3 := newCoordinatorFixture(t, coordinatorPolicy(t, 3, nil), coordinatorUsage(t, 0, nil), recorder, controller3, rebuilder3, dispatcher3, nil, 5*time.Millisecond)

	inputSuccess := coordinatorCycleInput(t, "plan.3", RecoveryStrategyExactReplay)
	res3, err3 := coord3.Execute(ctx, inputSuccess)
	if err3 != nil {
		t.Fatalf("Execute success case: %v", err3)
	}
	if !res3.PreviousAttemptClosed() {
		t.Errorf("PreviousAttemptClosed = false on successful recovery, want true")
	}
}
