package agent

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/messagesend"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// barrierSendScopeDriver blocks INSIDE the runtime delivery RPC and, while
// blocked, reads the send scope the way a racing first tool call would. This
// locks the ordering reviewers required: a tool call racing inside the
// delivery RPC must observe either the new turn's targets (ack landed) or an
// unavailable scope (handover window) — never the previous turn's targets,
// and never an empty scope misread as a proactive allowance.
type barrierSendScopeDriver struct {
	busyDeliveryStubDriver
	entered    chan struct{}
	enteredOne sync.Once
	release    chan struct{}
	scopeReads chan messagesendScopeRead
	process    *Process
}

type messagesendScopeRead struct {
	scope messagesend.Scope
	live  bool
}

func (d *barrierSendScopeDriver) SupportsAckedBusyRuntimeInputDelivery() bool { return true }

func (d *barrierSendScopeDriver) DeliverAckedBusyRuntimeInput(ctx context.Context, _ drivercontract.RuntimeInput, _ string) error {
	d.enteredOne.Do(func() { close(d.entered) })
	select {
	case <-d.release:
	case <-ctx.Done():
		return ctx.Err()
	}
	scope, live := d.readScope()
	select {
	case d.scopeReads <- messagesendScopeRead{scope: scope, live: live}:
	default:
	}
	return nil
}

func (d *barrierSendScopeDriver) readScope() (messagesend.Scope, bool) {
	if d.process == nil {
		return messagesend.Scope{}, false
	}
	return d.process.SendScope()
}

func newBarrierProcess(driver *barrierSendScopeDriver) *Process {
	ap := newAdmissionTestProcess(driver, false)
	driver.process = ap
	return ap
}

func TestBusyDeliveryKeepsTurnScopeThroughRPCWindow(t *testing.T) {
	scopeReads := make(chan messagesendScopeRead, 1)
	driver := &barrierSendScopeDriver{
		entered:    make(chan struct{}),
		release:    make(chan struct{}),
		scopeReads: scopeReads,
	}
	ap := newBarrierProcess(driver)
	ap.mu.Lock()
	ap.addSendTargetLocked("#agentteam-in-agentteam:52a2a221", sendScopeSourceConversation)
	ap.mu.Unlock()

	delivered := make(chan error, 1)
	go func() {
		delivered <- ap.DeliverForHandoff(context.Background(), types.InboxMessage{
			MessageID: "m1",
			Target:    "#agentteam-overnight:38bf4096",
		})
	}()
	<-driver.entered

	// Busy same-turn delivery is an APPEND handover: the turn's existing
	// target stays readable while the delivery RPC is still blocked.
	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"),
		"same-turn busy delivery must keep the turn's existing targets readable")

	close(driver.release)
	require.NoError(t, <-delivered, "delivery error")
	scope, live = ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"))
	require.False(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"),
		"acked-busy RPC success without an exact ack must not authorize B")
}

func TestFailedDeliveryRestoresPreviousScope(t *testing.T) {
	driver := &barrierSendScopeDriver{
		entered:    make(chan struct{}),
		release:    make(chan struct{}),
		scopeReads: make(chan messagesendScopeRead, 1),
	}
	ap := newBarrierProcess(driver)
	ap.mu.Lock()
	ap.addSendTargetLocked("#agentteam-in-agentteam:52a2a221", sendScopeSourceConversation)
	ap.mu.Unlock()

	delivered := make(chan error, 1)
	cancelled, cancel := context.WithCancel(context.Background())
	go func() {
		delivered <- ap.DeliverForHandoff(cancelled, types.InboxMessage{
			MessageID: "m2",
			Target:    "#agentteam-overnight:38bf4096",
		})
	}()
	<-driver.entered
	cancel()
	<-delivered

	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"),
		"a failed delivery must restore the previous turn's scope")
	require.False(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"))
}

func TestSendScopeUnavailableAcrossInterruptHandover(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-scope-handover",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#agentteam-in-agentteam:52a2a221", sendScopeSourceConversation)

	saved := ap.beginSendScopeHandoverLocked()
	_, live := ap.SendScope()
	require.False(t, live, "handover window must be unavailable")

	ap.abortSendScopeHandoverLocked(saved)
	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"))

	ap.beginSendScopeHandoverLocked()
	ap.commitSendScopeHandoverLocked(types.InboxMessage{Target: "#agentteam-overnight:38bf4096"})
	scope, live = ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"))
	require.False(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"))
}

func TestAckDuringRPCWindowInstallsScopeAtomically(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-ack-window",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.beginSendScopeHandoverLocked()

	// The ack-time commit is what a correlated ack triggers while the delivery
	// RPC is still in flight; it must install the new targets immediately.
	ap.mu.Lock()
	ap.commitSendScopeHandoverLocked(types.InboxMessage{Target: "dm:@alice"})
	ap.mu.Unlock()

	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("dm:@alice"))
}

func TestGuardPassedReplacementDoesNotRevokeRequest(t *testing.T) {
	// Linearization contract: once the guard passed (scope read returned
	// live+hit), a later replacement does not retroactively revoke that
	// request. Simulate at the Process level: a successful snapshot stays the
	// authorization for the in-flight request even after the scope retires.
	ap := &Process{
		AgentID:               "agent-contract",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#agentteam-overnight:38bf4096", sendScopeSourceConversation)

	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"), "guard passed for this request")

	// A replacement retires the scope afterwards; the already-authorized
	// request keeps its captured snapshot.
	ap.resetSendScopeLocked()
	require.True(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"),
		"the passed request's captured scope is not retroactively revoked")

	// New requests fail closed against the retired scope state.
	_, nextLive := ap.SendScope()
	require.True(t, nextLive) // live process, empty scope => proactive allowed per contract
}

func TestBusySameTurnDeliveryAppendsToExistingScope(t *testing.T) {
	driver := &barrierSendScopeDriver{
		entered:    make(chan struct{}),
		release:    make(chan struct{}),
		scopeReads: make(chan messagesendScopeRead, 1),
	}
	ap := newBarrierProcess(driver)
	ap.mu.Lock()
	ap.addSendTargetLocked("#agentteam-in-agentteam:52a2a221", sendScopeSourceConversation)
	ap.mu.Unlock()

	delivered := make(chan error, 1)
	go func() {
		delivered <- ap.DeliverForHandoff(context.Background(), types.InboxMessage{
			MessageID: "m-busy-append",
			Target:    "#agentteam-overnight:38bf4096",
		})
	}()
	<-driver.entered

	// Same-turn busy delivery keeps the turn's existing targets readable
	// during the RPC: A stays visible while B is being delivered.
	inFlight, live := ap.SendScope()
	require.True(t, live)
	require.True(t, inFlight.ContainsTarget("#agentteam-in-agentteam:52a2a221"),
		"same-turn busy delivery must not clear the turn's existing targets")

	close(driver.release)
	require.NoError(t, <-delivered, "delivery error")

	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-in-agentteam:52a2a221"), "A must remain after B's delivery")
	require.False(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"),
		"B joins only at its exact ack, not at RPC success")
}

func TestSessionMismatchedAckDoesNotAuthorizeTarget(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-ack-mismatch",
		SessionID:             "session-A",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.beginSendScopeHandoverLocked()

	// An ack arriving on a DIFFERENT session must not install the target even
	// though the inputID matches: the fast path runs the same correlation
	// check as the normal path before touching the send scope.
	acked := drivercontract.ParsedEvent{
		RuntimeInputID: "input-mismatch",
		SessionID:      "session-B",
	}
	pending := pendingRuntimeInput{
		Message:   types.InboxMessage{MessageID: "m1", Target: "#ops"},
		Input:     drivercontract.RuntimeInput{ID: "input-mismatch"},
		SessionID: "session-A",
		CreatedAt: time.Now().UTC(),
	}
	ap.pendingRuntimeInputs = map[string]pendingRuntimeInput{"input-mismatch": pending}
	// The runtime ack handler with a mismatched session leaves the handover
	// window (and the scope) untouched for this input.
	require.True(t, runtimeInputAckedSessionMismatch(pending, acked),
		"mismatched session must be detected by the shared correlation check")
}

func TestValidateSendScopeFailsAfterPostSnapshotStop(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-post-snapshot-stop",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#agentteam-overnight:38bf4096", sendScopeSourceConversation)

	scope, live, generation := ap.SendScopeWithGeneration()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#agentteam-overnight:38bf4096"))
	require.True(t, ap.ValidateSendScope(generation), "unchanged live scope validates")

	// Stop lands AFTER the snapshot but BEFORE the final validation: the
	// pointer-level checks elsewhere may still see the same process; the
	// generation/liveness validation must fail closed.
	ap.stopping = true
	require.False(t, ap.ValidateSendScope(generation),
		"post-snapshot stop must fail the final validation")

	ap.stopping = false
	ap.state = processStateStopped
	require.False(t, ap.ValidateSendScope(generation))

	// A scope change (retirement/handover) after the snapshot also fails.
	ap.state = processStateRunning
	require.True(t, ap.ValidateSendScope(generation))
	ap.resetSendScopeLocked()
	require.False(t, ap.ValidateSendScope(generation),
		"post-snapshot retirement must fail the final validation")
}

func TestValidAckThenRPCErrorKeepsCommittedScope(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-ack-then-error",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#a-turn", sendScopeSourceConversation)

	// REPLACE handover begins (interrupt), the exact ack commits B atomically
	// inside the RPC, then the RPC returns a transport error.
	saved := ap.beginSendScopeHandoverLocked()
	ap.commitSendScopeHandoverLocked(types.InboxMessage{Target: "#b-turn"})

	// The RPC error path runs its rollback — it must NOT restore A over the
	// committed B.
	ap.abortSendScopeHandoverLocked(saved)

	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#b-turn"), "committed replacement scope survives the RPC error")
	require.False(t, scope.ContainsTarget("#a-turn"), "old turn target must not roll back over the committed scope")

	// Without an intervening ack commit, the rollback still restores A.
	ap2 := &Process{
		AgentID:               "agent-no-ack-error",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap2.addSendTargetLocked("#a-turn", sendScopeSourceConversation)
	saved2 := ap2.beginSendScopeHandoverLocked()
	ap2.abortSendScopeHandoverLocked(saved2)
	scope2, live2 := ap2.SendScope()
	require.True(t, live2)
	require.True(t, scope2.ContainsTarget("#a-turn"), "pure transport error still restores the previous scope")
	require.False(t, scope2.ContainsTarget("#b-turn"))
}

func TestAmbientTaskTargetDoesNotWidenScope(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-ambient-task",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	// Ordinary channel message carrying an ambient session active-task
	// target (dispatcher backfill): must NOT add the task scope.
	ap.addSendTargetsLocked(types.InboxMessage{
		MessageID: "m-ambient",
		Target:    "#team",
		ActiveTaskTarget: types.ActiveTaskTarget{
			Channel:    "#team",
			TaskNumber: 12,
			TaskScope:  "#tsk_ambient",
		},
	})
	scope, live := ap.SendScope()
	require.True(t, live)
	require.False(t, scope.ContainsTarget("#tsk_ambient"),
		"ambient session task target must not widen the send scope")
	require.True(t, scope.ContainsTarget("#team"))

	// A genuine task dispatch on the message itself: task_dispatch is a
	// legacy read-only envelope, so it still must NOT derive a task scope
	// target. Sending to a task target requires explicit allow_cross_target.
	ap.addSendTargetsLocked(types.InboxMessage{
		MessageID:   "m-dispatch",
		Target:      "#team",
		MessageType: "task_dispatch",
		Metadata: map[string]any{
			"task": map[string]any{
				"channelRef": "#team",
				"taskNumber": 12,
				"taskScope":  "#tsk_explicit",
				"title":      "T",
			},
		},
	})
	scope, live = ap.SendScope()
	require.True(t, live)
	require.False(t, scope.ContainsTarget("#tsk_explicit"),
		"task_dispatch must not derive an implicit task scope target")
	require.True(t, scope.ContainsTarget("#team"))
}

func TestNewProcessSeedsInitialDeliveryBeforeSpawn(t *testing.T) {
	var seeded bool
	var seededTargets []messagesend.Target
	driver := &launchStateCharacterizationDriver{name: "builtin"}
	process, err := NewProcess(
		context.Background(),
		driver,
		testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-launch-seed", WorkDir: t.TempDir()}, testLaunchSurfaces{Prompt: "wake"}),
		ProcessOptions{
			InitialDelivery: &types.InboxMessage{MessageID: "m1", Target: "#agentteam-overnight:38bf4096"},
			OnProcessConstructed: func(p *Process) {
				// Runs after seeding, BEFORE Spawn: the first tool call inside
				// Spawn must already resolve the wake target.
				scope, live := p.SendScope()
				seeded = live
				seededTargets = scope.Targets
			},
		},
	)
	if err != nil {
		t.Skipf("spawn unavailable in this environment: %v", err)
	}
	defer process.Stop()
	require.True(t, seeded, "pre-Spawn constructor must see the seeded wake target")
	require.Len(t, seededTargets, 1)
	require.Equal(t, "#agentteam-overnight:38bf4096", seededTargets[0].Target)
}

func TestSuppressedLaunchSeedsNothing(t *testing.T) {
	var sawTargets []messagesend.Target
	driver := &launchStateCharacterizationDriver{name: "builtin"}
	process, err := NewProcess(
		context.Background(),
		driver,
		drivercontract.LaunchConfig{AgentID: "agent-no-seed", WorkDir: t.TempDir()},
		ProcessOptions{
			OnProcessConstructed: func(p *Process) {
				scope, _ := p.SendScope()
				sawTargets = scope.Targets
			},
		},
	)
	if err != nil {
		t.Skipf("spawn unavailable in this environment: %v", err)
	}
	defer process.Stop()
	require.Empty(t, sawTargets, "no-prompt launch must not seed any target")
}

func TestRestoredOrReclaimedAckDoesNotProduceScope(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-restored-ack",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.beginSendScopeHandoverOwnerLocked("input-r")
	ap.mu.Lock()
	ap.pendingRuntimeInputs = map[string]pendingRuntimeInput{"input-r": {
		Message:      types.InboxMessage{MessageID: "m1", Target: "#old"},
		Input:        drivercontract.RuntimeInput{ID: "input-r"},
		SessionID:    "session-1",
		Restored:     true,
		sendScopeGen: ap.sendScope.generation,
	}}
	pending := ap.pendingRuntimeInputs["input-r"]
	ap.commitAckedSendScopeLocked(pending)
	ap.mu.Unlock()
	_, live := ap.SendScope()
	require.False(t, live, "restored ack must not produce scope")

	ap2 := &Process{
		AgentID:               "agent-reclaimed-ack",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap2.beginSendScopeAppendOwnerLocked("input-x")
	ap2.mu.Lock()
	ap2.pendingRuntimeInputs = map[string]pendingRuntimeInput{"input-x": {
		Message:      types.InboxMessage{MessageID: "m2", Target: "#old"},
		Input:        drivercontract.RuntimeInput{ID: "input-x"},
		SessionID:    "session-1",
		reclaimed:    true,
		sendScopeGen: ap2.sendScope.generation,
	}}
	ap2.commitAckedSendScopeLocked(ap2.pendingRuntimeInputs["input-x"])
	ap2.mu.Unlock()
	scope2, live2 := ap2.SendScope()
	require.True(t, live2)
	require.False(t, scope2.ContainsTarget("#old"), "reclaimed ack must not add its target")
}

func TestInvalidConversationTargetPoisonsWholeDelivery(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-invalid-conv",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	// Malformed conversation target WITH a valid task dispatch: the whole
	// delivery must be unavailable, not partially scoped to the task.
	ap.addSendTargetsLocked(types.InboxMessage{
		MessageID:   "m1",
		Target:      "not a valid target!",
		MessageType: "task_dispatch",
		Metadata: map[string]any{
			"task": map[string]any{
				"channelRef": "#team",
				"taskNumber": 7,
				"taskScope":  "#tsk_valid",
				"title":      "T",
			},
		},
	})
	_, live := ap.SendScope()
	require.False(t, live, "invalid conversation target must make the whole delivery unavailable")
}

func TestAmbiguousInterruptKeepsScopeUnavailable(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-ambiguous",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#old-turn", sendScopeSourceConversation)
	saved := ap.beginSendScopeHandoverOwnerLocked("m-amb")

	// Ambiguous transport error: conservative outcome is unavailable — the
	// old turn must NOT come back.
	ap.abortSendScopeHandoverAmbiguousLocked()
	_, live := ap.SendScope()
	require.False(t, live, "ambiguous interrupt must keep the scope unavailable")
	_ = saved

	// A definite failure still restores the old turn.
	ap2 := &Process{
		AgentID:               "agent-definite",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap2.addSendTargetLocked("#old-turn", sendScopeSourceConversation)
	saved2 := ap2.beginSendScopeHandoverOwnerLocked("m-det")
	ap2.abortSendScopeHandoverLocked(saved2)
	scope2, live2 := ap2.SendScope()
	require.True(t, live2)
	require.True(t, scope2.ContainsTarget("#old-turn"), "definite failure restores the old turn")
}

func TestCrossTurnOrdinaryAckInstallsCurrentScope(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-cross-turn",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap.addSendTargetLocked("#a-turn", sendScopeSourceConversation)
	genBefore := ap.sendScope.generation

	// B queues during turn A, then the turn retires (generation advances).
	ap.beginSendScopeAppendOwnerLocked("input-b")
	ap.mu.Lock()
	ap.pendingRuntimeInputs = map[string]pendingRuntimeInput{"input-b": {
		Message:      types.InboxMessage{MessageID: "m-b", Target: "#b-turn"},
		Input:        drivercontract.RuntimeInput{ID: "input-b"},
		SessionID:    "session-1",
		sendScopeGen: genBefore,
	}}
	pendingB := ap.pendingRuntimeInputs["input-b"]
	ap.mu.Unlock()
	ap.resetSendScopeLocked()

	// The exact ack arrives in the new turn: ordinary pending acks install
	// into the CURRENT generation.
	ap.mu.Lock()
	ap.commitAckedSendScopeLocked(pendingB)
	ap.mu.Unlock()
	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#b-turn"),
		"ordinary cross-turn ack must install its target into the live scope")
	require.False(t, scope.ContainsTarget("#a-turn"),
		"the retired turn's target must not survive the retirement")

	// A final-hop receipt from the retired generation stays durability-only.
	ap2 := &Process{
		AgentID:               "agent-stale-finalhop",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}
	ap2.addSendTargetLocked("#live", sendScopeSourceConversation)
	staleGen := ap2.sendScope.generation
	ap2.beginSendScopeAppendOwnerLocked("input-x")
	ap2.mu.Lock()
	ap2.pendingRuntimeInputs = map[string]pendingRuntimeInput{"input-x": {
		Message:          types.InboxMessage{MessageID: "m-x", Target: "#stale"},
		Input:            drivercontract.RuntimeInput{ID: "input-x"},
		SessionID:        "session-1",
		FinalHopDeferred: true,
		sendScopeGen:     staleGen,
	}}
	pendingX := ap2.pendingRuntimeInputs["input-x"]
	ap2.mu.Unlock()
	ap2.resetSendScopeLocked()
	ap2.mu.Lock()
	ap2.commitAckedSendScopeLocked(pendingX)
	ap2.mu.Unlock()
	scope2, live2 := ap2.SendScope()
	require.True(t, live2)
	require.False(t, scope2.ContainsTarget("#stale"),
		"stale final-hop ack must not re-authorize its target")
}

func TestClaudeLikeAckedBusySuccessDoesNotAuthorize(t *testing.T) {
	driver := &barrierSendScopeDriver{
		entered:    make(chan struct{}),
		release:    make(chan struct{}),
		scopeReads: make(chan messagesendScopeRead, 1),
	}
	ap := newBarrierProcess(driver)
	ap.mu.Lock()
	ap.addSendTargetLocked("#a-turn", sendScopeSourceConversation)
	ap.mu.Unlock()

	delivered := make(chan error, 1)
	go func() {
		delivered <- ap.DeliverForHandoff(context.Background(), types.InboxMessage{
			MessageID: "m-b",
			Target:    "#b-turn",
		})
	}()
	<-driver.entered
	close(driver.release)
	require.NoError(t, <-delivered, "delivery error")

	// The driver has NO reconciler (Claude-like): RPC success must not add B.
	scope, live := ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#a-turn"))
	require.False(t, scope.ContainsTarget("#b-turn"),
		"acked-busy RPC success without an exact ack must not authorize B")

	// The exact ack installs B into the same turn.
	ap.mu.Lock()
	ap.pendingRuntimeInputs["m-b"] = pendingRuntimeInput{
		Message:      types.InboxMessage{MessageID: "m-b", Target: "#b-turn"},
		Input:        drivercontract.RuntimeInput{ID: "m-b"},
		SessionID:    "session-1",
		sendScopeGen: ap.sendScope.generation,
	}
	ap.commitAckedSendScopeLocked(ap.pendingRuntimeInputs["m-b"])
	ap.mu.Unlock()
	scope, live = ap.SendScope()
	require.True(t, live)
	require.True(t, scope.ContainsTarget("#a-turn"))
	require.True(t, scope.ContainsTarget("#b-turn"), "exact ack appends B")
}

func TestPostSnapshotInvalidDeliveryFailsValidation(t *testing.T) {
	ap := &Process{
		AgentID:               "agent-post-snapshot-poison",
		SessionID:             "session-1",
		processLifecycleState: processLifecycleState{state: processStateRunning},
	}

	// Guard snapshots an empty-but-live scope (turn with no targets yet).
	scope, live, generation := ap.SendScopeWithGeneration()
	require.True(t, live)
	require.Empty(t, scope.Targets)
	require.True(t, ap.ValidateSendScope(generation), "clean snapshot validates")

	// A runtime-visible delivery with an unnormalizable conversation target
	// poisons the turn AFTER the snapshot.
	ap.addSendTargetsLocked(types.InboxMessage{MessageID: "m1", Target: "not a valid target!"})

	require.False(t, ap.ValidateSendScope(generation),
		"post-snapshot invalid delivery must fail the final validation")
	_, liveAfter := ap.SendScope()
	require.False(t, liveAfter, "poisoned turn reports unavailable")
}
