package run

import (
	"testing"

	"github.com/stretchr/testify/require"
)

// selectOneTaskWorkScopePlan is a send_message callback whose responder is a
// selectOne candidate pool AND whose workScope is a fixed Task scope. This is
// the exact shape Review P0-1 targets: the reducer must project a
// TaskWatchHandoff for the eventual frozen winner even though no concrete
// responder is known until the capacity scheduler selects one.
func selectOneTaskWorkScopePlan() string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  task:
    type: scope
    required: true
  candidates:
    type: array
    required: true
    items: {type: agent, required: true}
graph:
  entry:
    nodeId: start
    port: in
    input:
      task: {ref: plan.input.task}
      candidates: {ref: plan.input.candidates}
  nodes:
    - id: start
      type: send_message
      input:
        task: {type: scope, required: true}
        candidates:
          type: array
          required: true
          items: {type: agent, required: true}
      sendMessage:
        scope: {value: {kind: task, scopeId: tsk_fixed}}
        responder: {selectOne: {ref: input.candidates}}
        text: "Pick one."
        completion: callback
        workScope: {ref: input.task}
        callbackOutput:
          result: {type: string, required: true}
    - id: done
      type: end
      end: {outcome: succeeded}
  edges:
    - id: e1
      source: {nodeId: start, port: completed}
      target: {nodeId: done, port: in}
`
}

// TestSelectOneReducerProjectsTaskWatchHandoff proves Review P0-1 at the
// reducer level: a send_message callback with a selectOne candidate pool AND a
// fixed Task workScope must project a non-nil TaskWatchHandoff (previously nil
// because the reducer only built handoffs for fixed responders). The handoff
// carries the Task scope, retains the callback activation as the source, and
// has no causal predecessors yet. Observing the frozen winner advances the
// frontier and freezes the resolved responder identity.
func TestSelectOneReducerProjectsTaskWatchHandoff(t *testing.T) {
	t.Parallel()
	kernel := compilePlanKernel(t, selectOneTaskWorkScopePlan())
	state, err := kernel.Start(Values{
		"task":       map[string]any{"kind": "task", "scopeId": "tsk_fixed"},
		"candidates": []any{"agt_a", "agt_b", "agt_a"},
	})
	require.NoError(t, err)
	transition, err := kernel.Step(state, reducerNow)
	require.NoError(t, err)
	require.Len(t, transition.Intents, 1)
	intent := transition.Intents[0]
	require.True(t, intent.Callback)
	require.Empty(t, intent.Responder, "a selectOne pool must not render a fixed responder")
	require.Equal(t, []string{"agt_a", "agt_b"}, intent.ResponderCandidates,
		"the reducer must freeze the ordered, deduplicated candidate pool")
	require.NotNil(t, intent.TaskWatchHandoff,
		"a selectOne callback with a fixed Task workScope must project a TaskWatchHandoff")
	require.Equal(t, "tsk_fixed", intent.TaskWatchHandoff.TaskScope.ScopeID)
	require.Equal(t, intent.ActivationID, intent.TaskWatchHandoff.WorkItemID,
		"a static selectOne callback retains its activation as the handoff source")
	require.Empty(t, intent.TaskWatchHandoff.CausalPredecessorAgentIDs,
		"the first selectOne callback has no causal predecessors")

	// Observing the frozen winner (equivalent to the message receipt committing
	// with the selectOne winner) freezes the resolved responder and advances the
	// frontier token.
	state = observeTaskWatchResponder(t, kernel, transition.State, intent, "agt_b")
	activation, err := state.activation(intent.ActivationID)
	require.NoError(t, err)
	require.NotNil(t, activation.CallbackTaskWatch)
	require.Equal(t, "agt_b", activation.CallbackTaskWatch.ResponderAgentID,
		"the observed winner must freeze as the normalized Task Watch responder")

	// JSON round-trip preserves the frozen frontier across recovery.
	restored, err := JSONRoundTrip(state)
	require.NoError(t, err)
	require.Equal(t, state, restored, "frozen selectOne winner survives recovery")
}
