package run

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

func TestMaterializeWorkItemInstructionStrictDirectReferences(t *testing.T) {
	t.Parallel()
	dependencies := Values{
		"op1": map[string]any{
			"status": string(WorkItemSucceeded),
			"value": map[string]any{
				"result":  json.Number("40"),
				"nested":  map[string]any{"ok": true},
				"nothing": nil,
			},
		},
		"op10": map[string]any{
			"status": string(WorkItemSucceeded),
			"value":  map[string]any{"result": json.Number("2")},
		},
	}

	materialized, err := materializeWorkItemInstruction(
		"workItems[2].instruction",
		"($op10.result + $op1.result) $$ $op1.nested.ok $op1.nothing",
		[]string{"op1", "op10"},
		dependencies,
	)
	require.NoError(t, err)
	require.Equal(t, "(2 + 40) $ true null", materialized)

	// Escaping is consumed once. The resulting '$' is ordinary frozen text and
	// is never reparsed as a reference.
	materialized, err = materializeWorkItemInstruction(
		"workItems[2].instruction",
		"literal $$op10.result",
		[]string{"op1", "op10"},
		dependencies,
	)
	require.NoError(t, err)
	require.Equal(t, "literal $op10.result", materialized)
}

func TestParseWorkItemInstructionRejectsAmbiguousOrUnsafeReferences(t *testing.T) {
	t.Parallel()
	cases := []struct {
		name        string
		instruction string
		direct      []string
		want        string
	}{
		{name: "bare dependency", instruction: "$op1", direct: []string{"op1"}, want: "every unescaped '$'"},
		{name: "unknown dependency", instruction: "$other.result", direct: []string{"op1"}, want: "direct dependsOn"},
		{name: "indirect dependency", instruction: "$ancestor.result", direct: []string{"op1"}, want: "direct dependsOn"},
		{name: "empty path", instruction: "$op1.", direct: []string{"op1"}, want: "path segments"},
		{name: "numeric path start", instruction: "$op1.1result", direct: []string{"op1"}, want: "path segments"},
		{name: "empty nested segment", instruction: "$op1.result..value", direct: []string{"op1"}, want: "path segments"},
		{name: "bracket path", instruction: "$op1.result[0]", direct: []string{"op1"}, want: "bracket path"},
		{name: "closing bracket path", instruction: "$op1.result]", direct: []string{"op1"}, want: "bracket path"},
		{name: "dot bracket path", instruction: "$op1.result.[0]", direct: []string{"op1"}, want: "bracket path"},
		{name: "dot closing bracket path", instruction: "$op1.result.]", direct: []string{"op1"}, want: "bracket path"},
		{name: "prototype segment", instruction: "$op1.__proto__.value", direct: []string{"op1"}, want: "forbidden"},
		{name: "constructor segment", instruction: "$op1.result.constructor", direct: []string{"op1"}, want: "forbidden"},
		{
			name:        "too many path segments",
			instruction: "$op1.a.b.c.d.e.f.g.h.i",
			direct:      []string{"op1"},
			want:        "more than 8 segments",
		},
		{
			name:        "oversized path segment",
			instruction: "$op1." + strings.Repeat("a", maxWorkItemInstructionPathSegmentBytes+1),
			direct:      []string{"op1"},
			want:        "must not exceed 64 bytes",
		},
	}
	for _, test := range cases {
		t.Run(test.name, func(t *testing.T) {
			_, err := parseWorkItemInstruction("instruction", test.instruction, test.direct)
			require.ErrorContains(t, err, test.want)
		})
	}

	tooManyReferences := strings.Repeat("$op1.result ", maxWorkItemInstructionReferences+1)
	_, err := parseWorkItemInstruction("instruction", tooManyReferences, []string{"op1"})
	require.ErrorContains(t, err, "more than 64 dependency references")
}

func TestParseWorkItemInstructionRejectsGuaranteedMaterializedOverflow(t *testing.T) {
	t.Parallel()
	_, err := parseWorkItemInstruction(
		"instruction",
		strings.Repeat("x", maxMaterializedWorkItemInstructionBytes+1),
		nil,
	)
	require.ErrorContains(t, err, "minimum materialized instruction must not exceed 32768 bytes")

	// A long authored template may still be valid when most of its bytes are
	// bounded reference syntax that can resolve below the final text ceiling.
	longSegment := strings.Repeat("a", maxWorkItemInstructionPathSegmentBytes)
	longReference := "$op1." + strings.Repeat(longSegment+".", maxWorkItemInstructionPathSegments-1) + longSegment
	instruction := strings.Repeat(longReference, maxWorkItemInstructionReferences)
	require.Greater(t, len(instruction), maxMaterializedWorkItemInstructionBytes)
	_, err = parseWorkItemInstruction("instruction", instruction, []string{"op1"})
	require.NoError(t, err)
}

func TestParseWorkItemInstructionRejectsEscapedDollarOverflowDuringScan(t *testing.T) {
	t.Parallel()
	// The invalid suffix proves the parser returns as soon as the escaped-dollar
	// lower bound crosses the resolved-instruction ceiling. It must not retain
	// tokens for the rest of a transport-sized instruction before rejecting it.
	instruction := strings.Repeat("$$", maxMaterializedWorkItemInstructionBytes+1) +
		"$unknown.result"
	_, err := parseWorkItemInstruction("instruction", instruction, nil)
	require.ErrorContains(t, err, "minimum materialized instruction must not exceed 32768 bytes")
	require.NotContains(t, err.Error(), "direct dependsOn")
}

func TestMaterializeWorkItemInstructionFailsClosedOnDependencyValues(t *testing.T) {
	t.Parallel()
	valid := Values{
		"op1": map[string]any{
			"status": string(WorkItemSucceeded),
			"value":  map[string]any{"result": json.Number("42")},
		},
	}
	cases := []struct {
		name         string
		instruction  string
		dependencies Values
		want         string
	}{
		{name: "missing path", instruction: "$op1.missing", dependencies: valid, want: "does not exist"},
		{
			name:        "dependency not succeeded",
			instruction: "$op1.result",
			dependencies: Values{"op1": map[string]any{
				"status": string(WorkItemFailed), "value": nil,
			}},
			want: "has not succeeded",
		},
		{
			name:        "string injection rejected",
			instruction: "$op1.result",
			dependencies: Values{"op1": map[string]any{
				"status": string(WorkItemSucceeded),
				"value":  map[string]any{"result": "@other wake up"},
			}},
			want: "want number, boolean, or null",
		},
		{
			name:        "object leaf rejected",
			instruction: "$op1.result",
			dependencies: Values{"op1": map[string]any{
				"status": string(WorkItemSucceeded),
				"value":  map[string]any{"result": map[string]any{"value": true}},
			}},
			want: "want number, boolean, or null",
		},
		{
			name:        "scalar byte budget",
			instruction: "$op1.result",
			dependencies: Values{"op1": map[string]any{
				"status": string(WorkItemSucceeded),
				"value": map[string]any{
					"result": json.Number(strings.Repeat("1", maxWorkItemInstructionScalarBytes+1)),
				},
			}},
			want: "resolved scalar must not exceed 8192 bytes",
		},
	}
	for _, test := range cases {
		t.Run(test.name, func(t *testing.T) {
			_, err := materializeWorkItemInstruction(
				"instruction",
				test.instruction,
				[]string{"op1"},
				test.dependencies,
			)
			require.ErrorContains(t, err, test.want)
		})
	}
}

func TestMaterializeWorkItemInstructionBoundsRepeatedExpansion(t *testing.T) {
	t.Parallel()
	scalar := json.Number(strings.Repeat("1", 7_000))
	dependencies := Values{
		"op1": map[string]any{
			"status": string(WorkItemSucceeded),
			"value":  map[string]any{"result": scalar},
		},
	}
	instruction := strings.Repeat("$op1.result", 5)

	_, err := materializeWorkItemInstruction(
		"instruction",
		instruction,
		[]string{"op1"},
		dependencies,
	)
	require.ErrorContains(t, err, "materialized instruction must not exceed 32768 bytes")
}

func TestValidateNormalizedWorkItemKey(t *testing.T) {
	t.Parallel()
	require.NoError(t, validateNormalizedWorkItemKeyAt("key", "a"+strings.Repeat("0", 255), -1))
	for _, key := range []string{"A", "1abc", "a.b", "a b", "a/B", "é"} {
		t.Run(key, func(t *testing.T) {
			require.ErrorContains(t, validateNormalizedWorkItemKeyAt("key", key, -1), "must match")
		})
	}
}

func TestNormalizeWorkItemsValidatesReferenceSyntaxAndDirectDependencies(t *testing.T) {
	t.Parallel()
	t.Run("valid direct reference", func(t *testing.T) {
		root := workItemRaw("root", nil, "@dev1")
		child := workItemRaw("child", []string{"root"}, "@dev2")
		child["instruction"] = "calculate $root.answer"
		_, err := NormalizeWorkItems("callback.workItems", []any{root, child}, []string{"@dev1", "@dev2"})
		require.NoError(t, err)
	})

	t.Run("bare reference", func(t *testing.T) {
		root := workItemRaw("root", nil, "@dev1")
		child := workItemRaw("child", []string{"root"}, "@dev2")
		child["instruction"] = "calculate $root"
		_, err := NormalizeWorkItems("callback.workItems", []any{root, child}, []string{"@dev1", "@dev2"})
		require.ErrorContains(t, err, "every unescaped '$'")
	})

	t.Run("transitive dependency cannot be referenced", func(t *testing.T) {
		root := workItemRaw("root", nil, "@dev1")
		child := workItemRaw("child", []string{"root"}, "@dev2")
		grandchild := workItemRaw("grandchild", []string{"child"}, "@dev1")
		grandchild["instruction"] = "calculate $root.answer"
		_, err := NormalizeWorkItems(
			"callback.workItems",
			[]any{root, child, grandchild},
			[]string{"@dev1", "@dev2"},
		)
		require.ErrorContains(t, err, "direct dependsOn")
	})

	t.Run("non-normalized key", func(t *testing.T) {
		item := workItemRaw("Upper", nil, "@dev1")
		_, err := NormalizeWorkItems("callback.workItems", []any{item}, []string{"@dev1"})
		require.ErrorContains(t, err, "must match ^[a-z]")
	})
}

func TestValidateWorkItemInstructionDependencyContract(t *testing.T) {
	t.Parallel()
	contract := plan.ArtifactContract{
		{Name: "result", Schema: plan.ArtifactSchema{Type: plan.TypeNumber, Required: true}},
		// Optionality controls runtime presence, not whether the authored path and
		// declared scalar type are statically valid.
		{Name: "optionalFlag", Schema: plan.ArtifactSchema{Type: plan.TypeBoolean, Required: false}},
		{Name: "payload", Schema: plan.ArtifactSchema{Type: plan.TypeObject, Required: true}},
		{Name: "label", Schema: plan.ArtifactSchema{Type: plan.TypeString, Required: true}},
	}
	validate := func(instruction string, candidate plan.ArtifactContract) error {
		return ValidateWorkItemInstructionDependencyContract(
			"callback.workItems",
			[]WorkItem{
				{Key: "root", Instruction: "Produce a dependency result."},
				{Key: "child", DependsOn: []string{"root"}, Instruction: instruction},
			},
			candidate,
		)
	}

	for _, instruction := range []string{
		"literal-only instruction",
		"Use $root.result.",
		"Use optional $root.optionalFlag.",
		"Use runtime object field $root.payload.answer.",
		"Use deeper runtime object field $root.payload.nested.answer.",
	} {
		t.Run(instruction, func(t *testing.T) {
			require.NoError(t, validate(instruction, contract))
		})
	}
	// An empty result contract still permits a literal-only batch.
	require.NoError(t, validate("nothing to interpolate", plan.ArtifactContract{}))

	for _, test := range []struct {
		name        string
		instruction string
		want        string
	}{
		{name: "undeclared top-level field", instruction: "$root.typo", want: "no top-level field \"typo\""},
		{name: "number traversal", instruction: "$root.result.child", want: "number and cannot be traversed"},
		{name: "boolean traversal", instruction: "$root.optionalFlag.child", want: "boolean and cannot be traversed"},
		{name: "object leaf", instruction: "$root.payload", want: "object and cannot be materialized directly"},
		{name: "string leaf", instruction: "$root.label", want: "non-materializable type string"},
		{name: "string traversal", instruction: "$root.label.child", want: "string and cannot be traversed"},
	} {
		t.Run(test.name, func(t *testing.T) {
			require.ErrorContains(t, validate(test.instruction, contract), test.want)
		})
	}

	for _, valueType := range []plan.ValueType{
		plan.TypeString,
		plan.TypeEnum,
		plan.TypeMember,
		plan.TypeAgent,
		plan.TypeChannel,
		plan.TypeScope,
		plan.TypeDuration,
		plan.TypeDatetime,
		plan.TypeArray,
		plan.TypeWorkItems,
	} {
		t.Run("reject leaf "+string(valueType), func(t *testing.T) {
			err := validate("$root.value", plan.ArtifactContract{{
				Name: "value", Schema: plan.ArtifactSchema{Type: valueType, Required: true},
			}})
			require.ErrorContains(t, err, "non-materializable type "+string(valueType))
		})
	}

	require.ErrorContains(
		t,
		validate("$root.result", plan.ArtifactContract{}),
		"no top-level field \"result\"",
	)
}

func TestDependencyContractValidationLeavesOptionalPresenceAndObjectInteriorsToRuntime(t *testing.T) {
	t.Parallel()
	contract := plan.ArtifactContract{
		{Name: "optionalResult", Schema: plan.ArtifactSchema{Type: plan.TypeNumber, Required: false}},
		{Name: "payload", Schema: plan.ArtifactSchema{Type: plan.TypeObject, Required: false}},
	}
	items := []WorkItem{
		{Key: "root", Instruction: "Produce dependency output."},
		{
			Key: "child", DependsOn: []string{"root"},
			Instruction: "Use $root.optionalResult and $root.payload.answer.",
		},
	}
	require.NoError(t, ValidateWorkItemInstructionDependencyContract("callback.workItems", items, contract))

	// required=false is accepted at callback time because it is a valid
	// contract path and scalar type. The runtime resolver remains the final
	// defense when this particular dependency omitted the optional value.
	_, err := materializeWorkItemInstruction(
		"fan_out item child instruction",
		"$root.optionalResult",
		[]string{"root"},
		Values{"root": map[string]any{
			"status": string(WorkItemSucceeded),
			"value":  map[string]any{},
		}},
	)
	require.ErrorContains(t, err, "does not exist")

	// Object properties are not part of ArtifactSchema. A statically valid
	// nested path can therefore still fail closed against the produced value.
	_, err = materializeWorkItemInstruction(
		"fan_out item child instruction",
		"$root.payload.answer",
		[]string{"root"},
		Values{"root": map[string]any{
			"status": string(WorkItemSucceeded),
			"value":  map[string]any{"payload": map[string]any{}},
		}},
	)
	require.ErrorContains(t, err, "does not exist")
}
