package run

import (
	"testing"

	"github.com/stretchr/testify/require"

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

// ---------------------------------------------------------------------------
// Reconciliation regression coverage (#t2751).
//
// The clean integration merged the #t2587 normalized structured-error design
// (validationErrorf codes projected as typed callback diagnostics) with the
// upstream 1132ab92c selectOne strategy feature (strategy sibling in
// responder.selectOne, WorkItem.SelectOneStrategy, preserve-presence). These
// tests pin the RECONCILED behavior: every strategy/unsupported-field/random-
// pool failure routes through the normalized structured ValidationError codes
// (never a bare fmt.Errorf), while the strategy feature and its presence
// semantics are preserved exactly. This guards against a future merge dropping
// either the structured code/stage/path/hint/diagnostics surface or the
// selectOne strategy support.
// ---------------------------------------------------------------------------

// responderContract is the TypeResponder callback output contract used to drive
// normalizeResponderSchemaValue through the public NormalizeContract boundary.
func responderContract() plan.ArtifactContract {
	return plan.ArtifactContract{
		{Name: "responder", Schema: plan.ArtifactSchema{Type: plan.TypeResponder, Required: true, Values: []string{}}},
	}
}

func requireValidationCode(t *testing.T, err error, wantCode string) {
	t.Helper()
	require.Error(t, err, "expected a structured validation error")
	diagnostic, ok := ValidationErrorDiagnostic(err)
	require.Truef(t, ok, "error is not a structured ValidationError: %v", err)
	require.Equal(t, wantCode, diagnostic.Code, "unexpected diagnostic code for %v", err)
}

// TestReconciliationResponderStrategyErrorsAreStructured proves the selectOne
// strategy path (port of upstream 1132ab92c) routes every failure through the
// normalized #t2587 structured validation codes, never a bare fmt.Errorf.
func TestReconciliationResponderStrategyErrorsAreStructured(t *testing.T) {
	t.Parallel()

	// strategy present but not a string -> structured select_one_invalid.
	_, err := NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a"}, "strategy": 42}})
	requireValidationCode(t, err, validationCodeOutputSelectOneInvalid)

	// strategy present but not a closed value (explicit empty) -> structured.
	_, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a"}, "strategy": ""}})
	requireValidationCode(t, err, validationCodeOutputSelectOneInvalid)

	// Unknown strategy value -> structured.
	_, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a"}, "strategy": "chaotic"}})
	requireValidationCode(t, err, validationCodeOutputSelectOneInvalid)

	// Unsupported sibling next to selectOne -> structured select_one_invalid.
	_, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a"}, "other": true}})
	requireValidationCode(t, err, validationCodeOutputSelectOneInvalid)

	// Missing selectOne entirely -> structured responder_invalid.
	_, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"strategy": "random"}})
	requireValidationCode(t, err, validationCodeOutputResponderInvalid)

	// Random strategy with fewer than two deduplicated candidates -> structured.
	_, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a"}, "strategy": "random"}})
	requireValidationCode(t, err, validationCodeOutputSelectOneInvalid)
}

// TestReconciliationResponderStrategyPresenceIsPreserved proves the reconciled
// normalizeResponderSchemaValue preserves strategy presence EXACTLY: absent
// stays absent (legacy ordered default), explicit "ordered"/"random" round-trip.
func TestReconciliationResponderStrategyPresenceIsPreserved(t *testing.T) {
	t.Parallel()

	// Absent strategy -> the frozen responder carries no strategy sibling.
	value, err := NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a", "agt_b"}}})
	require.NoError(t, err)
	responder, ok := value["responder"].(map[string]any)
	require.True(t, ok)
	require.NotContains(t, responder, "strategy", "absent strategy must stay absent (legacy ordered default)")
	require.Equal(t, []any{"agt_a", "agt_b"}, responder["selectOne"])

	// Explicit "ordered" -> preserved verbatim.
	value, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "ordered"}})
	require.NoError(t, err)
	responder = value["responder"].(map[string]any)
	require.Equal(t, "ordered", responder["strategy"], "explicit ordered strategy must be preserved, not normalized to absent")

	// Explicit "random" -> preserved verbatim.
	value, err = NormalizeContract("item", responderContract(), Values{"responder": map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "random"}})
	require.NoError(t, err)
	responder = value["responder"].(map[string]any)
	require.Equal(t, "random", responder["strategy"], "explicit random strategy must be preserved")
}

// workItemsContract returns the TypeWorkItems contract used to drive the
// normalized WorkItem path (normalizeWorkItem -> normalizedWorkItemResponder).
func workItemsContract() plan.ArtifactContract {
	return plan.ArtifactContract{{Name: "workItems", Schema: plan.ArtifactSchema{Type: plan.TypeWorkItems}}}
}

func oneWorkItem(responder any) Values {
	return Values{"workItems": []any{map[string]any{
		"key":           "k1",
		"workScope":     map[string]any{"kind": "task", "scopeId": "tsk_a"},
		"relatedScopes": []any{},
		"responder":     responder,
		"instruction":   "run",
		"dependsOn":     []any{},
	}}}
}

// TestReconciliationWorkItemStrategyErrorsAreStructured proves the WorkItem
// responder path (normalizedWorkItemResponder) also routes the ported strategy
// feature through the normalized WorkItem structured codes.
func TestReconciliationWorkItemStrategyErrorsAreStructured(t *testing.T) {
	t.Parallel()
	contract := workItemsContract()

	// Invalid strategy value -> structured work_items.responder_invalid.
	_, err := NormalizeContract("callback", contract, oneWorkItem(map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "chaotic"}))
	requireValidationCode(t, err, validationCodeWorkItemsResponderInvalid)

	// strategy not a string -> structured work_items.responder_invalid.
	_, err = NormalizeContract("callback", contract, oneWorkItem(map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": 7}))
	requireValidationCode(t, err, validationCodeWorkItemsResponderInvalid)

	// Unsupported sibling -> structured work_items.responder_invalid.
	_, err = NormalizeContract("callback", contract, oneWorkItem(map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "other": true}))
	requireValidationCode(t, err, validationCodeWorkItemsResponderInvalid)

	// Random strategy with a single candidate -> structured.
	_, err = NormalizeContract("callback", contract, oneWorkItem(map[string]any{"selectOne": []any{"agt_a"}, "strategy": "random"}))
	requireValidationCode(t, err, validationCodeWorkItemsResponderInvalid)

	// A disallowed fixed responder -> structured work_items.responder_not_allowed.
	_, err = NormalizeWorkItems("callback", oneWorkItem("agt_x")["workItems"], []string{"agt_a", "agt_b"})
	requireValidationCode(t, err, validationCodeWorkItemsResponderNotAllowed)

	// A disallowed selectOne candidate -> structured work_items.responder_not_allowed.
	_, err = NormalizeWorkItems("callback", oneWorkItem(map[string]any{"selectOne": []any{"agt_a", "agt_z"}})["workItems"], []string{"agt_a", "agt_b"})
	requireValidationCode(t, err, validationCodeWorkItemsResponderNotAllowed)
}

// TestReconciliationWorkItemStrategyRoundTripsThroughFrozenValue proves the
// strategy is carried through NormalizeContract into the frozen WorkItem JSON
// (WorkItemsValue) so the runtime/projector can honor random selection.
func TestReconciliationWorkItemStrategyRoundTripsThroughFrozenValue(t *testing.T) {
	t.Parallel()

	value, err := NormalizeContract("callback", workItemsContract(), oneWorkItem(map[string]any{"selectOne": []any{"agt_a", "agt_b"}, "strategy": "random"}))
	require.NoError(t, err)
	rawItems, ok := value["workItems"].([]any)
	require.True(t, ok)
	require.Len(t, rawItems, 1)
	item, ok := rawItems[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, []any{"agt_a", "agt_b"}, item["responder"].(map[string]any)["selectOne"])
	require.Equal(t, "random", item["responder"].(map[string]any)["strategy"])
}
