package run

import (
	"testing"

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

// selectOneRuntimePlan is a send_message callback whose responder is a
// selectOne candidate pool bound to the activation input. The reducer must
// render the ordered pool into the Intent's ResponderCandidates and leave the
// fixed Responder empty, so the capacity scheduler picks the winner at
// dispatch time.
func selectOneRuntimePlan() string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  candidates:
    type: array
    required: true
    items: {type: agent, required: true}
graph:
  entry:
    nodeId: start
    port: in
    input:
      candidates: {ref: plan.input.candidates}
  nodes:
    - id: start
      type: send_message
      input:
        candidates:
          type: array
          required: true
          items: {type: agent, required: true}
      sendMessage:
        scope: {value: {kind: channel, scopeId: chn_game}}
        responder: {selectOne: {ref: input.candidates}}
        text: "Pick one."
        completion: callback
        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}
`
}

// TestSelectOneReducerEmitsOrderedCandidateIntent proves the reducer end to
// end: a send_message.selectOne plan Start→Step renders the ordered candidate
// pool into Intent.ResponderCandidates (deduplicated first-occurrence order),
// leaves the fixed Responder empty, and arms a callback. Completing as the
// selected winner routes to the callback port and reaches a succeeded run.
func TestSelectOneReducerEmitsOrderedCandidateIntent(t *testing.T) {
	t.Parallel()
	kernel := compilePlanKernel(t, selectOneRuntimePlan())
	state, err := kernel.Start(Values{"candidates": []any{"agt_c", "agt_a", "agt_b", "agt_c"}})
	require.NoError(t, err)
	transition, err := kernel.Step(state, reducerNow)
	require.NoError(t, err)
	require.Len(t, transition.Intents, 1, "selectOne send_message must arm exactly one intent")
	intent := transition.Intents[0]
	require.Equal(t, IntentSendMessage, intent.Kind)
	require.True(t, intent.Callback, "selectOne only applies to callback send_message")
	require.Empty(t, intent.Responder, "a selectOne pool must not render a fixed responder")
	require.Equal(t, []string{"agt_c", "agt_a", "agt_b"}, intent.ResponderCandidates,
		"the reducer must freeze the ordered, deduplicated candidate pool losslessly")

	// Completing as the winner (any candidate) resolves the callback port.
	completed, err := kernel.Complete(transition.State, Completion{
		ActivationID:             intent.ActivationID,
		Port:                     "completed",
		CallbackResponderAgentID: "agt_a",
		Output:                   Values{"result": "done"},
	}, reducerNow)
	require.NoError(t, err)
	terminal := stepUntilTerminal(t, kernel, completed.State, 4)
	require.Equal(t, StatusSucceeded, terminal.Status)
}

// TestSelectOneReducerRejectsDynamicEmptyPool proves the reducer fails closed
// when a ref/dynamic selectOne pool evaluates to an empty array at runtime: the
// activation routes to the recoverable failed port with the selectOne code and
// the Run fails instead of rendering a callback with no candidate.
func TestSelectOneReducerRejectsDynamicEmptyPool(t *testing.T) {
	t.Parallel()
	kernel := compilePlanKernel(t, selectOneRuntimePlan())
	state, err := kernel.Start(Values{"candidates": []any{}})
	require.NoError(t, err)
	transition, err := kernel.Step(state, reducerNow)
	require.NoError(t, err)
	require.Empty(t, transition.Intents, "an empty dynamic pool must not arm a callback intent")
	require.NotContains(t, eventKinds(transition.Events), "effect_requested")
	action, err := transition.State.activation(state.Activations[0].ID)
	require.NoError(t, err)
	require.NotNil(t, action.Failure)
	require.Equal(t, "message.responder_select_one_invalid", action.Failure.Code)
	require.Equal(t, []string{"failed"}, action.SelectedPorts)

	terminal := stepUntilTerminal(t, kernel, transition.State, 2)
	require.Equal(t, StatusFailed, terminal.Status)
}
