package plan

import (
	"encoding/json"
	"math"
	"strings"
	"testing"

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

func TestCompileArtifactValueExpr_DirectReferenceAndExpressionHaveIdenticalAST(t *testing.T) {
	v, node := artifactExpressionTestContext()

	direct, err := compileArtifactValueExpr(v, ValueExpr{Ref: "input.name"}, node, referenceContext{}, nil)
	require.NoError(t, err)
	parsed, err := compileArtifactValueExpr(v, ValueExpr{Expr: "input.name"}, node, referenceContext{}, nil)
	require.NoError(t, err)

	assert.Equal(t, direct, parsed)
	assert.Equal(t, ArtifactExpression{
		Op:         ArtifactOpLoad,
		ResultType: TypeString,
		Load: &ArtifactLoad{
			Space:    ArtifactLoadActivationInput,
			Path:     []string{"name"},
			Optional: true,
		},
	}, direct)

	planInput, err := compileArtifactValueExpr(v, ValueExpr{Expr: "plan.input.request"}, node, referenceContext{}, nil)
	require.NoError(t, err)
	assert.Equal(t, ArtifactLoadPlanInput, planInput.Load.Space)
	assert.Equal(t, []string{"request"}, planInput.Load.Path)

	runState, err := compileArtifactValueExpr(v, ValueExpr{Ref: "state.count"}, node, referenceContext{}, nil)
	require.NoError(t, err)
	assert.Equal(t, ArtifactLoadRunState, runState.Load.Space)
	assert.Equal(t, []string{"count"}, runState.Load.Path)
}

func TestCompileArtifactExpression_OperatorsFunctionsArrayAndNull(t *testing.T) {
	v, node := artifactExpressionTestContext()

	comparisons := map[string]ArtifactExpressionOp{
		"input.count == 1": ArtifactOpEqual,
		"input.count != 1": ArtifactOpNotEqual,
		"input.count < 1":  ArtifactOpLess,
		"input.count <= 1": ArtifactOpLessEqual,
		"input.count > 1":  ArtifactOpGreater,
		"input.count >= 1": ArtifactOpGreaterEq,
	}
	for source, opcode := range comparisons {
		t.Run(string(opcode), func(t *testing.T) {
			expression, err := compileArtifactValueExpr(v, ValueExpr{Expr: source}, node, referenceContext{}, nil)
			require.NoError(t, err)
			assert.Equal(t, opcode, expression.Op)
			assert.Equal(t, TypeBoolean, expression.ResultType)
			require.Len(t, expression.Args, 2)
		})
	}

	logical, err := compileArtifactPredicate(v, Predicate(`!(input.enabled && false) || input.name == null`), node, referenceContext{})
	require.NoError(t, err)
	assert.Equal(t, ArtifactOpOr, logical.Op)
	assert.Equal(t, ArtifactOpNot, logical.Args[0].Op)
	assert.Equal(t, ArtifactOpAnd, logical.Args[0].Args[0].Op)
	assert.Equal(t, ArtifactOpEqual, logical.Args[1].Op)
	null := logical.Args[1].Args[1]
	assert.Equal(t, ArtifactOpConst, null.Op)
	assert.Equal(t, ArtifactTypeNull, null.ResultType)
	assert.Equal(t, "null", string(null.Value))

	functions := map[string]ArtifactExpressionOp{
		`contains(input.name, "a")`:   ArtifactOpContains,
		`startsWith(input.name, "a")`: ArtifactOpStartsWith,
		`endsWith(input.name, "z")`:   ArtifactOpEndsWith,
		`length(input.name)`:          ArtifactOpLength,
	}
	for source, opcode := range functions {
		t.Run(string(opcode), func(t *testing.T) {
			expression, err := compileArtifactValueExpr(v, ValueExpr{Expr: source}, node, referenceContext{}, nil)
			require.NoError(t, err)
			assert.Equal(t, opcode, expression.Op)
		})
	}

	membership, err := compileArtifactPredicate(v, Predicate(`input.name in ["a", "b"]`), node, referenceContext{})
	require.NoError(t, err)
	assert.Equal(t, ArtifactOpIn, membership.Op)
	require.Len(t, membership.Args, 2)
	assert.Equal(t, ArtifactOpConst, membership.Args[1].Op, "a fully literal array is folded")
	assert.Equal(t, `["a","b"]`, string(membership.Args[1].Value))

	dynamicArray, err := compileArtifactValueExpr(v, ValueExpr{Expr: `[input.count, 2]`}, node, referenceContext{}, nil)
	require.NoError(t, err)
	assert.Equal(t, ArtifactOpArray, dynamicArray.Op)
	assert.Equal(t, TypeArray, dynamicArray.ResultType)
	require.Len(t, dynamicArray.Args, 2)
	assert.Equal(t, ArtifactOpLoad, dynamicArray.Args[0].Op)
	assert.Equal(t, ArtifactOpConst, dynamicArray.Args[1].Op)

	number, err := compileArtifactValueExpr(v, ValueExpr{Expr: `1.`}, node, referenceContext{}, nil)
	require.NoError(t, err, "the Plan expression evaluator accepts a trailing decimal point")
	assert.Equal(t, "1", string(number.Value))
}

func TestCompileArtifactExpression_JoinAndLoopLoadsAreResolved(t *testing.T) {
	stringSchema := Schema{Type: TypeString}
	owner := &Node{ID: "fanout", Type: NodeFork, Fork: &ForkConfig{Branches: []string{"left", "right"}}}
	joinAll := &Node{
		ID: "gather", Type: NodeJoin,
		Input: map[string]Schema{"answer": stringSchema},
		Join:  &JoinConfig{ForkID: owner.ID, Mode: JoinAll},
	}
	joinAny := &Node{
		ID: "race", Type: NodeJoin,
		Input: map[string]Schema{"answer": stringSchema},
		Join:  &JoinConfig{ForkID: owner.ID, Mode: JoinAny},
	}
	v := &validator{
		document: &Document{},
		nodes: map[string]*Node{
			owner.ID:   owner,
			joinAll.ID: joinAll,
			joinAny.ID: joinAny,
		},
	}

	branch, err := compileArtifactValueExpr(v, ValueExpr{Expr: "branches.left.answer"}, joinAll, referenceContext{joinMerge: true}, nil)
	require.NoError(t, err)
	assert.Equal(t, &ArtifactLoad{
		Space: ArtifactLoadJoinBranch, ScopeOwnerID: owner.ID, Branch: "left", Path: []string{"answer"}, Optional: true,
	}, branch.Load)

	allBranches, err := compileArtifactValueExpr(v, ValueExpr{Ref: "branches"}, joinAll, referenceContext{joinMerge: true}, nil)
	require.NoError(t, err)
	assert.Equal(t, &ArtifactLoad{
		Space: ArtifactLoadJoinBranch, ScopeOwnerID: owner.ID, Path: []string{},
	}, allBranches.Load)

	winnerBranch, err := compileArtifactValueExpr(v, ValueExpr{Expr: "winner.branch"}, joinAny, referenceContext{joinMerge: true}, nil)
	require.NoError(t, err)
	assert.Equal(t, ArtifactLoadJoinWinnerBranch, winnerBranch.Load.Space)
	assert.Equal(t, owner.ID, winnerBranch.Load.ScopeOwnerID)
	assert.Empty(t, winnerBranch.Load.Path)

	winnerValue, err := compileArtifactValueExpr(v, ValueExpr{Ref: "winner.value.answer"}, joinAny, referenceContext{joinMerge: true}, nil)
	require.NoError(t, err)
	assert.Equal(t, &ArtifactLoad{
		Space: ArtifactLoadJoinWinnerValue, ScopeOwnerID: owner.ID, Path: []string{"answer"}, Optional: true,
	}, winnerValue.Load)

	loop := &Node{
		ID: "paginate", Type: NodeLoop,
		Loop: &AgentTeamConfig{
			Mode: LoopCounted, Iterations: 2,
			State: map[string]Schema{"cursor": stringSchema, "item": stringSchema},
		},
	}
	v.nodes[loop.ID] = loop
	loopState, err := compileArtifactValueExpr(v, ValueExpr{Ref: "loop.state.cursor"}, loop, referenceContext{loop: loop.Loop, sourcePort: "body"}, nil)
	require.NoError(t, err)
	assert.Equal(t, &ArtifactLoad{
		Space: ArtifactLoadLoopState, ScopeOwnerID: loop.ID, Path: []string{"cursor"}, Optional: true,
	}, loopState.Load)
	loopResult, err := compileArtifactValueExpr(v, ValueExpr{Expr: "loop.state.item"}, loop, referenceContext{loop: loop.Loop, sourcePort: "done"}, nil)
	require.NoError(t, err)
	assert.Equal(t, &ArtifactLoad{
		Space: ArtifactLoadLoopState, ScopeOwnerID: loop.ID, Path: []string{"item"}, Optional: true,
	}, loopResult.Load)
}

func TestCompileArtifactTemplate_EmitsOnlyTextAndCompiledExpressions(t *testing.T) {
	v, node := artifactExpressionTestContext()
	template, err := compileArtifactTemplate(v, `hello {{ input.name }}!{{ length([input.name, "x"]) }}`, node, referenceContext{})
	require.NoError(t, err)
	require.Len(t, template.Segments, 4)
	assert.Equal(t, ArtifactTemplateSegment{Kind: ArtifactTemplateSegmentText, Text: "hello "}, template.Segments[0])
	assert.Equal(t, ArtifactTemplateSegmentExpr, template.Segments[1].Kind)
	assert.Equal(t, ArtifactLoadActivationInput, template.Segments[1].Expression.Load.Space)
	assert.Equal(t, ArtifactTemplateSegment{Kind: ArtifactTemplateSegmentText, Text: "!"}, template.Segments[2])
	assert.Equal(t, ArtifactTemplateSegmentExpr, template.Segments[3].Kind)
	assert.Equal(t, ArtifactOpLength, template.Segments[3].Expression.Op)
	for index := 1; index < len(template.Segments); index++ {
		assert.False(t,
			template.Segments[index-1].Kind == ArtifactTemplateSegmentText && template.Segments[index].Kind == ArtifactTemplateSegmentText,
			"adjacent text segments must be merged",
		)
	}

	payload, err := json.Marshal(template)
	require.NoError(t, err)
	assert.NotContains(t, string(payload), "{{")
	assert.NotContains(t, string(payload), "input.name")
	assert.NotContains(t, string(payload), "length(")

	merged := ArtifactTemplate{Segments: []ArtifactTemplateSegment{}}
	appendArtifactTemplateText(&merged, "one")
	appendArtifactTemplateText(&merged, " two")
	assert.Equal(t, []ArtifactTemplateSegment{{Kind: ArtifactTemplateSegmentText, Text: "one two"}}, merged.Segments)
}

func TestCompileArtifactLiteral_NormalizedJSONNullNumbersMapsAndDuration(t *testing.T) {
	first := map[string]any{
		"z": []any{
			float64(1),
			int64(2),
			map[string]any{"b": json.Number("3.00"), "a": math.Copysign(0, -1)},
		},
		"a": "first",
	}
	second := map[string]any{
		"a": "first",
		"z": []any{
			uint8(1),
			json.Number("2e0"),
			map[string]any{"a": 0, "b": float32(3)},
		},
	}
	left, err := compileArtifactLiteral(first, nil)
	require.NoError(t, err)
	right, err := compileArtifactLiteral(second, nil)
	require.NoError(t, err)
	assert.Equal(t, `{"a":"first","z":[1,2,{"a":0,"b":3}]}`, string(left))
	assert.Equal(t, left, right, "map order and numeric Go representation are non-semantic")

	null, err := compileArtifactLiteral(nil, nil)
	require.NoError(t, err)
	assert.Equal(t, "null", string(null))
	marshaledNull, err := json.Marshal(null)
	require.NoError(t, err)
	assert.Equal(t, "null", string(marshaledNull))
	var decodedNull ArtifactLiteral
	require.NoError(t, json.Unmarshal(marshaledNull, &decodedNull))
	assert.Equal(t, "null", string(decodedNull))

	durationSchema := &Schema{Type: TypeDuration}
	iso, err := compileArtifactLiteral("PT1H30M", durationSchema)
	require.NoError(t, err)
	goDuration, err := compileArtifactLiteral("90m", durationSchema)
	require.NoError(t, err)
	assert.Equal(t, "5400000000000", string(iso))
	assert.Equal(t, iso, goDuration)
	durationItems := Schema{Type: TypeDuration}
	durationArray, err := compileArtifactLiteral([]any{"1s", "PT2S"}, &Schema{Type: TypeArray, Items: &durationItems})
	require.NoError(t, err)
	assert.Equal(t, `[1000000000,2000000000]`, string(durationArray))

	datetimeSchema := &Schema{Type: TypeDatetime}
	offsetInstant, err := compileArtifactLiteral("2026-07-20T08:30:00+08:00", datetimeSchema)
	require.NoError(t, err)
	utcInstant, err := compileArtifactLiteral("2026-07-20T00:30:00.000Z", datetimeSchema)
	require.NoError(t, err)
	assert.Equal(t, `"2026-07-20T00:30:00Z"`, string(offsetInstant))
	assert.Equal(t, offsetInstant, utcInstant, "equivalent RFC3339 offsets must have one digest representation")
	datetimeItems := Schema{Type: TypeDatetime}
	datetimeArray, err := compileArtifactLiteral(
		[]any{"2026-07-20T08:30:00+08:00", "2026-07-20T00:30:00.123400Z"},
		&Schema{Type: TypeArray, Items: &datetimeItems},
	)
	require.NoError(t, err)
	assert.Equal(t, `["2026-07-20T00:30:00Z","2026-07-20T00:30:00.1234Z"]`, string(datetimeArray))

	var decoded ArtifactLiteral
	require.NoError(t, json.Unmarshal([]byte(`1.00`), &decoded))
	assert.Equal(t, "1", string(decoded))
}

func TestCompileArtifactValueExpr_ExpectedLiteralTypeAndErrors(t *testing.T) {
	v, node := artifactExpressionTestContext()
	durationSchema := &Schema{Type: TypeDuration}
	expression, err := compileArtifactValueExpr(v, ValueExpr{Expr: `"5s"`}, node, referenceContext{}, durationSchema)
	require.NoError(t, err)
	assert.Equal(t, TypeDuration, expression.ResultType)
	assert.Equal(t, "5000000000", string(expression.Value))

	channelValue := any("channel-1")
	channelSchema := &Schema{Type: TypeChannel}
	expression, err = compileArtifactValueExpr(v, ValueExpr{Value: &channelValue}, node, referenceContext{}, channelSchema)
	require.NoError(t, err)
	assert.Equal(t, TypeChannel, expression.ResultType)
	assert.Equal(t, `"channel-1"`, string(expression.Value))

	_, err = compileArtifactValueExpr(v, ValueExpr{Ref: "input.name", Expr: "input.name"}, node, referenceContext{}, nil)
	assert.ErrorContains(t, err, "exactly one")
	_, err = compileArtifactValueExpr(v, ValueExpr{Ref: "input.missing"}, node, referenceContext{}, nil)
	assert.ErrorContains(t, err, "not declared")
	_, err = compileArtifactValueExpr(v, ValueExpr{Ref: "input.name"}, nil, referenceContext{}, nil)
	assert.ErrorContains(t, err, "activation node")
	_, err = compileArtifactValueExpr(v, ValueExpr{Expr: "contains(input.name)"}, node, referenceContext{}, nil)
	assert.ErrorContains(t, err, "requires 2 arguments")
	_, err = compileArtifactTemplate(v, "hello {{ input.name", node, referenceContext{})
	assert.ErrorContains(t, err, "unmatched delimiter")
	_, err = compileArtifactPredicate(v, Predicate("input.name"), node, referenceContext{})
	assert.ErrorContains(t, err, "must be boolean")
}

func artifactExpressionTestContext() (*validator, *Node) {
	stringItems := Schema{Type: TypeString}
	node := &Node{
		ID:   "work",
		Type: NodeSetVariable,
		Input: map[string]Schema{
			"name":    {Type: TypeString},
			"count":   {Type: TypeNumber},
			"enabled": {Type: TypeBoolean},
			"tags":    {Type: TypeArray, Items: &stringItems},
		},
	}
	document := &Document{
		Input: map[string]Schema{"request": {Type: TypeString}},
		State: map[string]Schema{"count": {Type: TypeNumber}},
	}
	return &validator{document: document, nodes: map[string]*Node{node.ID: node}}, node
}

func TestArtifactLiteralJSONContainsNoBase64Encoding(t *testing.T) {
	literal, err := compileArtifactLiteral(map[string]any{"message": "hello"}, nil)
	require.NoError(t, err)
	payload, err := json.Marshal(struct {
		Value ArtifactLiteral `json:"value"`
	}{Value: literal})
	require.NoError(t, err)
	assert.Equal(t, `{"value":{"message":"hello"}}`, strings.TrimSpace(string(payload)))
}
