package orchestrator

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
	runkernel "github.com/tidbcloud/AgentTeam/internal/workflow/run"
)

// executeComplete runs a complete callback with the given output/workItems and
// returns the error (nil when the callback committed).
func executeComplete(
	t *testing.T,
	now time.Time,
	output string,
	workItems string,
	configure func(repository *callbackRepositoryStub),
) error {
	t.Helper()
	repository := callbackServiceFixture(t, now)
	if configure != nil {
		configure(repository)
	}
	repository.resolve = func(params run.ResolveAwaitingEffectParams) (run.ResolveAwaitingEffectResult, error) {
		t.Fatal("a validation failure must not reach the Store resolver")
		return run.ResolveAwaitingEffectResult{}, nil
	}
	service, err := NewCallbackService(repository, CallbackServiceOptions{
		Now:             func() time.Time { return now },
		ScopeAuthorizer: &callbackScopeAuthorizerStub{},
	})
	require.NoError(t, err)
	_, err = service.ExecuteCallback(context.Background(), CallbackCommand{
		WorkspaceID: "wsp_1", RunID: "wri_1", CallbackID: "wfx_1",
		AgentID: "agt_assigned", Action: CallbackActionComplete,
		Output: json.RawMessage(output), WorkItems: json.RawMessage(workItems),
		IdempotencyKey: "typed-validation",
	})
	return err
}

func callbackValidationDiagnostics(t *testing.T, err error) []publicerr.Diagnostic {
	t.Helper()
	var validation *CallbackValidationError
	require.ErrorAs(t, err, &validation)
	require.NotEmpty(t, validation.Diagnostics)
	return validation.Diagnostics
}

func TestCallbackValidationOutputContractTypedDiagnostic(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 0, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackOutputContract(t, repository, plan.ArtifactContract{{
			Name:   "planSummary",
			Schema: plan.ArtifactSchema{Type: plan.TypeString, Required: true},
		}, {
			Name:   "count",
			Schema: plan.ArtifactSchema{Type: plan.TypeNumber, Required: true},
		}})
	}
	err := executeComplete(t, now, `{"planSummary":"ok","count":"not-a-number"}`, ``, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics := callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "callback.output.number_invalid", diagnostics[0].Code)
	require.Equal(t, "callback.output.count", diagnostics[0].Path)
	require.Equal(t, "count", diagnostics[0].Field)
	require.Nil(t, diagnostics[0].Index)

	err = executeComplete(t, now, `{"planSummary":"ok","extra":true}`, ``, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics = callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "callback.output.undeclared_field", diagnostics[0].Code)
	require.Equal(t, "callback.output", diagnostics[0].Path)
	require.Equal(t, "extra", diagnostics[0].Field)
}

func TestCallbackValidationWorkScopeTypedDiagnostic(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 10, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackCollaborationScopes(t, repository, runkernel.WorkScopeModeAgent, nil, nil)
	}
	err := executeComplete(t, now, `{"word":"ok"}`, ``, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics := callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, CallbackValidationCodeWorkScope, diagnostics[0].Code)
	require.Equal(t, "callback.workScope", diagnostics[0].Path)
	require.Contains(t, diagnostics[0].Message, "required")
}

func TestCallbackValidationWorkItemsShapeTypedDiagnostic(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 20, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackOutputContract(t, repository, nil)
		setCallbackWorkItems(t, repository, []string{"agt_dev_a"})
	}
	// Invalid key pattern at index 2.
	err := executeComplete(t, now, `{}`, `[
		{"key":"one","workScope":{"kind":"task","scopeId":"tsk_one"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"A"},
		{"key":"two","workScope":{"kind":"task","scopeId":"tsk_two"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"B"},
		{"key":"THREE","workScope":{"kind":"task","scopeId":"tsk_three"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"C"}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics := callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "callback.work_items.key_invalid", diagnostics[0].Code)
	require.Equal(t, "callback.workItems[2].key", diagnostics[0].Path)
	require.Equal(t, "key", diagnostics[0].Field)
	require.NotNil(t, diagnostics[0].Index)
	require.Equal(t, 2, *diagnostics[0].Index)

	// Dependency cycle through two items.
	err = executeComplete(t, now, `{}`, `[
		{"key":"a","dependsOn":["b"],"workScope":{"kind":"task","scopeId":"tsk_a"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"A"},
		{"key":"b","dependsOn":["a"],"workScope":{"kind":"task","scopeId":"tsk_b"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"B"}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics = callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "callback.work_items.dependency_cycle", diagnostics[0].Code)

	// Responder outside the frozen allowlist.
	err = executeComplete(t, now, `{}`, `[
		{"key":"one","workScope":{"kind":"task","scopeId":"tsk_one"},"relatedScopes":[],"responder":"agt_outsider","instruction":"A"}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics = callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "callback.work_items.responder_not_allowed", diagnostics[0].Code)
	require.Equal(t, "callback.workItems[0].responder", diagnostics[0].Path)
}

func TestCallbackValidationDependencyReferenceTypedDiagnostic(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 30, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackOutputContract(t, repository, nil)
		setCallbackWorkItems(t, repository, []string{"agt_dev_a"})
	}
	// A reference to a non-direct key is invalid dependency-reference syntax.
	err := executeComplete(t, now, `{}`, `[
		{"key":"one","workScope":{"kind":"task","scopeId":"tsk_one"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"Use $missing.value."}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics := callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Equal(t, "work_item.instruction_reference_invalid", diagnostics[0].Code)
	require.Equal(t, "callback.workItems[0].instruction", diagnostics[0].Path)
	require.Equal(t, "instruction", diagnostics[0].Field)
	require.NotNil(t, diagnostics[0].Index)
	require.Equal(t, 0, *diagnostics[0].Index)

	// A reference past a declared number is a static contract diagnostic.
	err = executeComplete(t, now, `{}`, `[
		{"key":"one","dependsOn":["two"],"workScope":{"kind":"task","scopeId":"tsk_one"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"Use $two.result.nested."},
		{"key":"two","workScope":{"kind":"task","scopeId":"tsk_two"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"Calculate."}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics = callbackValidationDiagnostics(t, err)
	require.Len(t, diagnostics, 1)
	require.Contains(t, diagnostics[0].Code, "contract_type_invalid")
	require.Equal(t, "callback.workItems[0].instruction", diagnostics[0].Path)
}

// TestCallbackValidationMultiDiagnosticAggregation verifies that a single
// complete attempt violating several independent dimensions projects every
// typed diagnostic deterministically instead of the first failure only.
func TestCallbackValidationMultiDiagnosticAggregation(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 40, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackOutputContract(t, repository, plan.ArtifactContract{{
			Name:   "planSummary",
			Schema: plan.ArtifactSchema{Type: plan.TypeString, Required: true},
		}})
		setCallbackWorkItems(t, repository, []string{"agt_dev_a"})
	}
	// Invalid output type + missing workScope + invalid workItem key, all at once.
	err := executeComplete(t, now, `{"planSummary":7}`, `[
		{"key":"BadKey","workScope":{"kind":"task","scopeId":"tsk_one"},"relatedScopes":[],"responder":"agt_dev_a","instruction":"A"}
	]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	var validation *CallbackValidationError
	require.ErrorAs(t, err, &validation)
	// workScope is not required by this fixture (no WorkScopeMode), so only the
	// output type and the invalid WorkItem key surface.
	require.Len(t, validation.Diagnostics, 2)
	codes := []string{validation.Diagnostics[0].Code, validation.Diagnostics[1].Code}
	require.Contains(t, codes, "callback.output.type_invalid")
	require.Contains(t, codes, "callback.work_items.key_invalid")
	require.NotEqual(t, "step_output_invalid", validation.Code)
}

// TestCallbackValidationDiagnosticsNeverLeakRawStoreError verifies the typed
// diagnostic messages are bounded display-safe text, not raw repository errors.
func TestCallbackValidationDiagnosticsNeverLeakRawStoreError(t *testing.T) {
	now := time.Date(2026, 8, 1, 1, 50, 0, 0, time.UTC)
	configure := func(repository *callbackRepositoryStub) {
		setCallbackOutputContract(t, repository, plan.ArtifactContract{{
			Name:   "word",
			Schema: plan.ArtifactSchema{Type: plan.TypeString, Required: true},
		}})
		setCallbackWorkItems(t, repository, []string{"agt_dev_a"})
	}
	err := executeComplete(t, now, `{}`, `[{
		"key":"one","workScope":{"kind":"task","scopeId":"tsk_secret"},
		"relatedScopes":[],"responder":"agt_dev_a","instruction":"A"
	}]`, configure)
	require.ErrorIs(t, err, ErrCallbackOutputInvalid)
	diagnostics := callbackValidationDiagnostics(t, err)
	for _, diagnostic := range diagnostics {
		require.NotContains(t, diagnostic.Message, "SELECT")
		require.NotContains(t, diagnostic.Message, "secret")
		require.Len(t, []rune(diagnostic.Message), len([]rune(diagnostic.Message))) // valid UTF-8
	}
}

var _ = errors.Is
