package run

import (
	"encoding/json"
	"testing"

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

func TestNormalizeContractDefaultsAndDynamicConstraints(t *testing.T) {
	t.Parallel()
	defaultCount := plan.ArtifactLiteral(`3`)
	contract := plan.ArtifactContract{
		{Name: "choice", Schema: plan.ArtifactSchema{Type: plan.TypeEnum, Required: true, Values: []string{"a", "b"}}},
		{Name: "count", Schema: plan.ArtifactSchema{Type: plan.TypeNumber, Default: &defaultCount, Values: []string{}}},
		{Name: "tags", Schema: plan.ArtifactSchema{Type: plan.TypeArray, Required: true, Values: []string{}, Items: &plan.ArtifactSchema{Type: plan.TypeString, Values: []string{}}}},
	}
	value, err := NormalizeContract("input", contract, Values{"choice": "a", "tags": []any{"x", "y"}})
	require.NoError(t, err)
	require.Equal(t, json.Number("3"), value["count"])

	_, err = NormalizeContract("input", contract, Values{"choice": "c", "tags": []any{"x"}})
	require.ErrorContains(t, err, "disallowed enum")
	_, err = NormalizeContract("input", contract, Values{"choice": "a", "tags": []any{3}})
	require.ErrorContains(t, err, "input.tags[0]")
	_, err = NormalizeContract("input", contract, Values{"choice": "a", "tags": []any{}, "extra": true})
	require.ErrorContains(t, err, "undeclared")
}

func TestNormalizeContractDurationDatetimeAndObject(t *testing.T) {
	t.Parallel()
	contract := plan.ArtifactContract{
		{Name: "at", Schema: plan.ArtifactSchema{Type: plan.TypeDatetime, Required: true, Values: []string{}}},
		{Name: "for", Schema: plan.ArtifactSchema{Type: plan.TypeDuration, Required: true, Values: []string{}}},
		{Name: "meta", Schema: plan.ArtifactSchema{Type: plan.TypeObject, Required: true, Values: []string{}}},
	}
	value, err := NormalizeContract("input", contract, Values{
		"at": "2026-07-21T12:00:00Z", "for": int64(10), "meta": Values{"n": 2},
	})
	require.NoError(t, err)
	require.Equal(t, json.Number("10"), value["for"])
	require.Equal(t, json.Number("2"), value["meta"].(map[string]any)["n"])

	_, err = NormalizeContract("input", contract, Values{"at": "2026-07-21T20:00:00+08:00", "for": 0, "meta": map[string]any{}})
	require.Error(t, err)
}

func TestNormalizeContractScopeUsesNormalizedMessageScopeValue(t *testing.T) {
	t.Parallel()

	contract := plan.ArtifactContract{{
		Name: "destination",
		Schema: plan.ArtifactSchema{
			Type:     plan.TypeScope,
			Required: true,
			Values:   []string{},
		},
	}}
	want := map[string]any{
		"kind":    string(domain.MessageScopeKindTask),
		"scopeId": "tsk_abcdefghijklmn",
	}
	for _, input := range []any{
		want,
		domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "tsk_abcdefghijklmn"},
	} {
		normalized, err := NormalizeContract("input", contract, Values{"destination": input})
		require.NoError(t, err)
		require.Equal(t, want, normalized["destination"])
	}

	for _, invalid := range []any{
		"#tsk_abcdefghijklmn",
		map[string]any{"kind": "task", "scopeId": "tsk_abcdefghijklmn", "extra": true},
		map[string]any{"kind": "team", "scopeId": "tsk_abcdefghijklmn"},
	} {
		_, err := NormalizeContract("input", contract, Values{"destination": invalid})
		require.ErrorContains(t, err, "canonical message scope")
	}
}

func TestNormalizeContractResponderUnion(t *testing.T) {
	t.Parallel()
	contract := plan.ArtifactContract{
		{Name: "responder", Schema: plan.ArtifactSchema{Type: plan.TypeResponder, Required: true, Values: []string{}}},
	}

	// Fixed normalized agent string.
	value, err := NormalizeContract("item", contract, Values{"responder": "agt_a"})
	require.NoError(t, err)
	require.Equal(t, "agt_a", value["responder"])

	// A blank fixed string is rejected.
	_, err = NormalizeContract("item", contract, Values{"responder": "  "})
	require.ErrorContains(t, err, "non-blank agent")

	// A valid selectOne pool is normalizedized preserving first-occurrence order
	// and deduplicating.
	value, err = NormalizeContract("item", contract, Values{
		"responder": map[string]any{"selectOne": []any{"agt_b", "agt_a", "agt_b"}},
	})
	require.NoError(t, err)
	require.Equal(t, map[string]any{"selectOne": []any{"agt_b", "agt_a"}}, value["responder"])

	// Empty pool rejected.
	_, err = NormalizeContract("item", contract, Values{"responder": map[string]any{"selectOne": []any{}}})
	require.ErrorContains(t, err, "must not be empty")

	// Pool beyond the safe upper bound rejected.
	tooMany := make([]any, plan.MaxAllowedResponders+1)
	for index := range tooMany {
		tooMany[index] = "agt_x"
	}
	_, err = NormalizeContract("item", contract, Values{"responder": map[string]any{"selectOne": tooMany}})
	require.ErrorContains(t, err, "must not contain more than")

	// Non-agent pool candidate rejected.
	_, err = NormalizeContract("item", contract, Values{
		"responder": map[string]any{"selectOne": []any{"agt_a", 42}},
	})
	require.ErrorContains(t, err, "must be a non-blank agent")

	// Unknown responder object fields rejected (selectOne + strategy are the only keys).
	_, err = NormalizeContract("item", contract, Values{
		"responder": map[string]any{"selectOne": []any{"agt_a"}, "other": true},
	})
	require.ErrorContains(t, err, "unsupported responder field")

	// A non-string, non-pool value is rejected.
	_, err = NormalizeContract("item", contract, Values{"responder": []any{"agt_a"}})
	require.ErrorContains(t, err, "fixed agent string or a selectOne candidate pool")
}

// TestNormalizeWorkItemsSelectOneRandomStrategy proves the dynamic WorkItem
// `{selectOne:[...], strategy:"random"}` responder is normalized with the
// strategy preserved, and a random pool with fewer than two deduplicated
// candidates is rejected fail-closed.
func TestNormalizeWorkItemsSelectOneRandomStrategy(t *testing.T) {
	contract := plan.ArtifactContract{{Name: "workItems", Schema: plan.ArtifactSchema{Type: plan.TypeWorkItems}}}

	items, err := NormalizeContract("callback", contract, Values{
		"workItems": []any{map[string]any{
			"key":           "k1",
			"workScope":     map[string]any{"kind": "task", "scopeId": "tsk_a"},
			"relatedScopes": []any{},
			"responder":     map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "random"},
			"instruction":   "run",
			"dependsOn":     []any{},
		}},
	})
	require.NoError(t, err)
	rawItems, ok := items["workItems"].([]any)
	require.True(t, ok)
	require.Len(t, rawItems, 1)
	normalized, ok := rawItems[0].(map[string]any)
	require.True(t, ok)
	responder, ok := normalized["responder"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, []any{"agt_a", "agt_b"}, responder["selectOne"])
	require.Equal(t, "random", responder["strategy"])

	// A random pool with one deduplicated candidate is rejected.
	_, err = NormalizeContract("callback", contract, Values{
		"workItems": []any{map[string]any{
			"key": "k1", "workScope": map[string]any{"kind": "task", "scopeId": "tsk_a"}, "relatedScopes": []any{}, "responder": map[string]any{"selectOne": []any{"agt_a"}, "strategy": "random"}, "instruction": "run", "dependsOn": []any{},
		}},
	})
	require.ErrorContains(t, err, "at least two deduplicated candidates")
}

// TestNormalizeWorkItemsSelectOneStrategyPresenceNormalized pins the strict
// dynamic WorkItem JSON semantics: an absent strategy field alone is the legacy
// ordered default; a present field must resolve to exactly "ordered" or "random"
// (explicit empty/null/whitespace-padded/case-variant/unknown fails closed).
func TestNormalizeWorkItemsSelectOneStrategyPresenceNormalized(t *testing.T) {
	for _, tc := range []struct {
		name      string
		responder map[string]any
		want      plan.SelectOneStrategy
	}{
		{"absent stays ordered", map[string]any{"selectOne": []any{"agt_a", "agt_b"}}, plan.SelectOneStrategy("")},
		{"explicit ordered", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "ordered"}, plan.SelectOneStrategyOrdered},
		{"random", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "random"}, plan.SelectOneStrategyRandom},
	} {
		t.Run("valid/"+tc.name, func(t *testing.T) {
			items, err := NormalizeWorkItems("callback.workItems", []any{map[string]any{
				"key": "k1", "workScope": map[string]any{"kind": "task", "scopeId": "tsk_a"}, "relatedScopes": []any{}, "dependsOn": []any{},
				"responder": tc.responder, "instruction": "run",
			}}, nil)
			require.NoError(t, err)
			require.Equal(t, tc.want, items[0].SelectOneStrategy)
		})
	}

	for _, tc := range []struct {
		name      string
		responder map[string]any
	}{
		{"explicit empty", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": ""}},
		{"whitespace padded", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": " random "}},
		{"case variant", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "Random"}},
		{"unknown", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "weighted"}},
		{"null", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": nil}},
		{"number", map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": 7}},
	} {
		t.Run("invalid/"+tc.name, func(t *testing.T) {
			_, err := NormalizeWorkItems("callback.workItems", []any{map[string]any{
				"key": "k1", "workScope": map[string]any{"kind": "task", "scopeId": "tsk_a"}, "relatedScopes": []any{}, "dependsOn": []any{},
				"responder": tc.responder, "instruction": "run",
			}}, nil)
			require.Error(t, err, "responder %v must be rejected", tc.responder)
		})
	}
}
