package run

import (
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

func TestOptionalPlanInputPassThroughAndNullPredicate(t *testing.T) {
	t.Parallel()
	artifact, diagnostics, err := plan.Compile([]byte(optionalInputSwitchPlan))
	require.NoError(t, err)
	require.False(t, plan.HasErrors(diagnostics), "%#v", diagnostics)
	require.Len(t, artifact.Entry.Bindings, 1)
	require.True(t, artifact.Entry.Bindings[0].Value.MayBeAbsent())

	wire, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	graph, err := DecodeArtifact(wire)
	require.NoError(t, err)
	kernel, err := NewKernel(graph)
	require.NoError(t, err)

	state, err := kernel.Start(Values{})
	require.NoError(t, err, "an omitted optional Plan input must not fail its implicit entry binding")
	require.Empty(t, state.Activations[0].Input)
	terminal := stepUntilTerminal(t, kernel, state, 3)
	require.Equal(t, StatusSucceeded, terminal.Status, "input.maybe == null must select the missing branch")
}

func TestOptionalCallbackOutputPassesThroughAnEdgeAsAbsent(t *testing.T) {
	t.Parallel()
	artifact, diagnostics, err := plan.Compile([]byte(optionalCallbackEdgePlan))
	require.NoError(t, err)
	require.False(t, plan.HasErrors(diagnostics), "%#v", diagnostics)
	var completed *plan.ArtifactEdge
	for index := range artifact.Edges {
		if artifact.Edges[index].ID == "completed" {
			completed = &artifact.Edges[index]
			break
		}
	}
	require.NotNil(t, completed)
	require.Len(t, completed.Bindings, 1)
	require.True(t, completed.Bindings[0].Value.MayBeAbsent())

	wire, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	graph, err := DecodeArtifact(wire)
	require.NoError(t, err)
	kernel, err := NewKernel(graph)
	require.NoError(t, err)
	state, err := kernel.Start(Values{})
	require.NoError(t, err)
	waiting, err := kernel.Step(state, reducerNow)
	require.NoError(t, err)
	require.Len(t, waiting.Intents, 1)

	resumed, err := kernel.Complete(waiting.State, Completion{
		ActivationID: waiting.Intents[0].ActivationID,
		Port:         "completed",
		Output:       Values{},
	}, reducerNow)
	require.NoError(t, err, "an omitted optional callback field must not fail its edge binding")
	require.Len(t, resumed.State.Activations, 2)
	require.Equal(t, "route", resumed.State.Activations[1].NodeID)
	require.Empty(t, resumed.State.Activations[1].Input)
	require.Equal(t, StatusSucceeded, stepUntilTerminal(t, kernel, resumed.State, 3).Status)
}

func TestOptionalLoadsPreserveAbsenceAcrossBindingLoopAndJoinContexts(t *testing.T) {
	t.Parallel()
	optionalLoad := func(space plan.ArtifactLoadSpace, owner, branch string) plan.ArtifactExpression {
		return plan.ArtifactExpression{
			Op: plan.ArtifactOpLoad, ResultType: plan.TypeString,
			Load: &plan.ArtifactLoad{Space: space, ScopeOwnerID: owner, Branch: branch, Path: []string{"maybe"}, Optional: true},
		}
	}
	optionalContract := plan.ArtifactContract{{
		Name: "maybe", Schema: plan.ArtifactSchema{Type: plan.TypeString, Values: []string{}},
	}}

	for _, test := range []struct {
		name       string
		expression plan.ArtifactExpression
		context    EvalContext
	}{
		{
			name:       "plan input",
			expression: optionalLoad(plan.ArtifactLoadPlanInput, "", ""),
			context:    EvalContext{PlanInput: Values{}},
		},
		{
			name:       "loop state",
			expression: optionalLoad(plan.ArtifactLoadLoopState, "repeat", ""),
			context: EvalContext{Loops: map[string]LoopValues{
				"repeat": {State: Values{}},
			}},
		},
		{
			name:       "inactive join branch",
			expression: optionalLoad(plan.ArtifactLoadJoinBranch, "fan", "right"),
			context: EvalContext{Joins: map[string]JoinValues{
				"fan": {Branches: map[string]Values{"left": {"maybe": "present"}}},
			}},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			value, err := EvalExpression(test.expression, test.context)
			require.NoError(t, err)
			require.Nil(t, value)

			bound, err := evalBindings("edge", []plan.ArtifactBinding{{Target: "maybe", Value: test.expression}}, optionalContract, test.context)
			require.NoError(t, err)
			require.Empty(t, bound, "an absent optional source must omit, not null, its destination")
		})
	}
}

func TestOptionalLoadEqualsNullWhileRequiredMissingLoadStillFails(t *testing.T) {
	t.Parallel()
	optional := plan.ArtifactExpression{
		Op: plan.ArtifactOpLoad, ResultType: plan.TypeString,
		Load: &plan.ArtifactLoad{Space: plan.ArtifactLoadPlanInput, Path: []string{"maybe"}, Optional: true},
	}
	null := plan.ArtifactExpression{Op: plan.ArtifactOpConst, ResultType: plan.ArtifactTypeNull, Value: plan.ArtifactLiteral(`null`)}
	equals := plan.ArtifactExpression{Op: plan.ArtifactOpEqual, ResultType: plan.TypeBoolean, Args: []plan.ArtifactExpression{optional, null}}

	value, err := EvalExpression(equals, EvalContext{PlanInput: Values{}})
	require.NoError(t, err)
	require.Equal(t, true, value)

	required := optional
	required.Load = &plan.ArtifactLoad{Space: plan.ArtifactLoadPlanInput, Path: []string{"must"}}
	_, err = EvalExpression(required, EvalContext{PlanInput: Values{}})
	require.ErrorContains(t, err, `field "must" is missing`)
}

const optionalInputSwitchPlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  maybe: {type: string}
graph:
  entry: {nodeId: route, port: in}
  nodes:
  - id: route
    type: switch
    input:
      maybe: {type: string}
    switch:
      mode: first_match
      cases:
      - port: missing
        when: 'input.maybe == null'
      - port: present
        isDefault: true
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: unexpected_present_value}
  edges:
  - id: missing
    source: {nodeId: route, port: missing}
    target: {nodeId: success, port: in}
  - id: present
    source: {nodeId: route, port: present}
    target: {nodeId: failure, port: in}
`

const optionalCallbackEdgePlan = `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: ask, port: in}
  nodes:
  - id: ask
    type: send_message
    sendMessage:
      target: "#all"
      text: reply
      completion: callback
      callbackOutput:
        maybe: {type: string}
      retry: {maxAttempts: 1, backoff: 1s}
  - id: route
    type: switch
    input:
      maybe: {type: string}
    switch:
      mode: first_match
      cases:
      - port: missing
        when: 'input.maybe == null'
      - port: present
        isDefault: true
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: callback_failed}
  edges:
  - id: completed
    source: {nodeId: ask, port: completed}
    target: {nodeId: route, port: in}
  - id: failed
    source: {nodeId: ask, port: failed}
    target: {nodeId: failure, port: in}
  - id: missing
    source: {nodeId: route, port: missing}
    target: {nodeId: success, port: in}
  - id: present
    source: {nodeId: route, port: present}
    target: {nodeId: failure, port: in}
`
