package exec

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

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

func TestWorkflowBusinessAuthorizationIsPlanTriggerRunStep(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	for _, name := range []string{"plan", "trigger", "run", "step"} {
		_, ok := catalog.RenderActionInputSchemaJSON("workflow", name)
		require.True(t, ok, name)
	}
	for _, removed := range []string{"binding", "invocation", "callback"} {
		_, ok := catalog.RenderActionInputSchemaJSON("workflow", removed)
		require.False(t, ok, removed)
	}
}

func TestWorkflowBusinessActionsRejectInternalRuntimeVocabulary(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	cases := map[string][]string{
		"plan":    {"get", "revise"},
		"trigger": {"get", "binding", "revise", "activate", "restore", "list_revisions", "get_revision"},
		"run":     {"invoke", "get", "list_activations", "list_effects", "list_events", "get_current", "complete", "fail"},
		"step":    {"current", "get_current", "failureCode", "failureMessage"},
	}
	for object, forbidden := range cases {
		schema, ok := catalog.RenderActionInputSchemaJSON("workflow", object)
		require.True(t, ok)
		for _, word := range forbidden {
			require.NotContains(t, schema, `"`+word+`"`, object)
		}
	}
}

func TestWorkflowBusinessActionSchemasAcceptExamples(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	examples := map[string][]string{
		"plan":    {`{"action":"show","planId":"pln_1"}`, `{"action":"update","planId":"pln_1","expectedRevision":1,"source":"apiVersion: loop.plan/v1"}`},
		"trigger": {`{"action":"show","triggerId":"wfb_1"}`, `{"action":"enable","triggerId":"wfb_1","revision":1,"expectedRevision":1}`},
		"run":     {`{"action":"start","planId":"pln_1","input":{}}`, `{"action":"show","runId":"wri_1"}`, `{"action":"logs","runId":"wri_1","tail":20}`},
		"step":    {`{"action":"list","limit":50}`, `{"action":"get","runId":"wri_1","stepId":"wfx_1"}`, `{"action":"fail","runId":"wri_1","stepId":"wfx_1","code":"blocked","message":"no access","idempotencyKey":"step-1"}`, `{"action":"pause","runId":"wri_1","stepId":"wfx_1","idempotencyKey":"step-pause-1"}`, `{"action":"park","runId":"wri_1","stepId":"wfx_1"}`, `{"action":"park","runId":"wri_1","stepId":"wfx_1","idempotencyKey":"step-park-1"}`},
	}
	for object, values := range examples {
		for _, value := range values {
			require.NoError(t, catalog.ValidateInput("workflow", object, json.RawMessage(value)), object+": "+value)
		}
	}
}

func TestWorkflowStepSchemaRejectsRetiredWaitAction(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	// The public `wait` action was removed entirely: validation must reject it at
	// the closed-schema boundary with no alias.
	for _, value := range []string{
		`{"action":"wait","runId":"wri_1","stepId":"wfx_1"}`,
		`{"action":"wait"}`,
		`{"action":"list","runId":"wri_1","stepId":"wfx_1","limit":1}`,
	} {
		err := catalog.ValidateInput("workflow", "step", json.RawMessage(value))
		require.Error(t, err, "step schema must reject retired wait action: "+value)
	}
	// pause requires a stable key; park requires only the closed runId+stepId
	// selectors and accepts a legacy key without using it as lifecycle receipt.
	for _, value := range []string{
		`{"action":"pause","runId":"wri_1","stepId":"wfx_1","output":{}}`,
		`{"action":"park","runId":"wri_1","stepId":"wfx_1","idempotencyKey":"k","output":{}}`,
		`{"action":"pause"}`,
	} {
		err := catalog.ValidateInput("workflow", "step", json.RawMessage(value))
		require.Error(t, err, "step schema must reject non-resolved pause/park: "+value)
	}
}

func TestWorkflowPlanUpdateAcceptsOptionalObjectLayout(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)

	require.NoError(t, catalog.ValidateInput("workflow", "plan", json.RawMessage(
		`{"action":"update","planId":"pln_1","expectedRevision":1,"source":"source"}`,
	)))
	require.NoError(t, catalog.ValidateInput("workflow", "plan", json.RawMessage(
		`{"action":"update","planId":"pln_1","expectedRevision":1,"source":"source","layout":{"nodes":{"a":{"x":1}}}}`,
	)))
	require.Error(t, catalog.ValidateInput("workflow", "plan", json.RawMessage(
		`{"action":"update","planId":"pln_1","expectedRevision":1,"source":"source","layout":[]}`,
	)))
}

func TestWorkflowBusinessActionSchemasRejectWhitespaceOnlyRequiredText(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)

	cases := []struct {
		name   string
		object string
		field  string
		input  map[string]any
	}{
		{name: "plan create key", object: "plan", field: "key", input: map[string]any{"action": "create", "key": "key", "displayName": "name", "source": ""}},
		{name: "plan create display name", object: "plan", field: "displayName", input: map[string]any{"action": "create", "key": "key", "displayName": "name", "source": ""}},
		{name: "trigger create key", object: "trigger", field: "key", input: workflowTriggerCreateSchemaInput()},
		{name: "trigger create display name", object: "trigger", field: "displayName", input: workflowTriggerCreateSchemaInput()},
		{name: "trigger create plan", object: "trigger", field: "planId", input: workflowTriggerCreateSchemaInput()},
		{name: "trigger create plan version", object: "trigger", field: "planVersionId", input: workflowTriggerCreateSchemaInput()},
		{name: "trigger show id", object: "trigger", field: "triggerId", input: map[string]any{"action": "show", "triggerId": "trigger"}},
		{name: "run start plan", object: "run", field: "planId", input: map[string]any{"action": "start", "planId": "plan", "input": map[string]any{}}},
		{name: "run show id", object: "run", field: "runId", input: map[string]any{"action": "show", "runId": "run"}},
	}
	whitespace := map[string]string{
		"ASCII":    " \t\r\n",
		"NBSP":     "\u00a0",
		"EM SPACE": "\u2003",
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			for name, blank := range whitespace {
				t.Run(name, func(t *testing.T) {
					tc.input[tc.field] = blank
					raw, marshalErr := json.Marshal(tc.input)
					require.NoError(t, marshalErr)
					require.Error(t, catalog.ValidateInput("workflow", tc.object, raw))
				})
			}

			tc.input[tc.field] = " \u00a0value\u2003 "
			raw, marshalErr := json.Marshal(tc.input)
			require.NoError(t, marshalErr)
			require.NoError(t, catalog.ValidateInput("workflow", tc.object, raw))
		})
	}
}

func TestWorkflowRunStartRequiresNonNullObjectInput(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	for name, raw := range map[string]json.RawMessage{
		"omitted": json.RawMessage(`{"action":"start","planId":"pln_1"}`),
		"null":    json.RawMessage(`{"action":"start","planId":"pln_1","input":null}`),
	} {
		t.Run(name, func(t *testing.T) {
			require.Error(t, catalog.ValidateInput("workflow", "run", raw))
		})
	}
}

func TestWorkflowSchemasDoNotExpandNonblankConstraintToOptionalReferences(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	cases := map[string]struct {
		object string
		raw    json.RawMessage
	}{
		"plan create optional team": {
			object: "plan",
			raw:    json.RawMessage(`{"action":"create","teamId":"\u00a0","key":"key","displayName":"name","source":""}`),
		},
		"plan list optional team": {
			object: "plan",
			raw:    json.RawMessage(`{"action":"list","teamId":"\u00a0"}`),
		},
		"trigger list optional plan": {
			object: "trigger",
			raw:    json.RawMessage(`{"action":"list","planId":"\u00a0"}`),
		},
		"run start optional plan version": {
			object: "run",
			raw:    json.RawMessage(`{"action":"start","planId":"pln_1","planVersionId":"\u00a0","input":{}}`),
		},
		"run list optional plan": {
			object: "run",
			raw:    json.RawMessage(`{"action":"list","planId":"\u00a0"}`),
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			require.NoError(t, catalog.ValidateInput("workflow", tc.object, tc.raw))
		})
	}
}

func TestWorkflowCatalogPreservesFormerShadowConstraints(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	invalid := map[string]struct {
		object string
		raw    json.RawMessage
	}{
		"plan list limit lower bound": {
			object: "plan", raw: json.RawMessage(`{"action":"list","limit":0}`),
		},
		"plan list limit upper bound": {
			object: "plan", raw: json.RawMessage(`{"action":"list","limit":101}`),
		},
		"plan validate null source": {
			object: "plan", raw: json.RawMessage(`{"action":"validate","source":null}`),
		},
		"plan update positive revision": {
			object: "plan", raw: json.RawMessage(`{"action":"update","planId":"pln_1","expectedRevision":0,"source":""}`),
		},
		"trigger list limit upper bound": {
			object: "trigger", raw: json.RawMessage(`{"action":"list","limit":101}`),
		},
		"trigger create null source": {
			object: "trigger", raw: json.RawMessage(`{"action":"create","key":"key","displayName":"name","planId":"pln_1","planVersionId":"pv_1","source":null,"enabled":true}`),
		},
		"trigger validate requires plan": {
			object: "trigger", raw: json.RawMessage(`{"action":"validate","planVersionId":"pv_1","source":{"type":"schedule","schedule":{"mode":"interval"}}}`),
		},
		"trigger update requires target revision": {
			object: "trigger", raw: json.RawMessage(`{"action":"update","planVersionId":"pv_1","source":{"type":"schedule","schedule":{"mode":"interval"}}}`),
		},
		"run list limit lower bound": {
			object: "run", raw: json.RawMessage(`{"action":"list","limit":0}`),
		},
		"run logs tail upper bound": {
			object: "run", raw: json.RawMessage(`{"action":"logs","runId":"run_1","tail":101}`),
		},
	}
	for name, tc := range invalid {
		t.Run(name, func(t *testing.T) {
			require.Error(t, catalog.ValidateInput("workflow", tc.object, tc.raw))
		})
	}

	t.Run("cancel reason keeps JSON Schema code point length", func(t *testing.T) {
		raw, marshalErr := json.Marshal(map[string]any{
			"action": "cancel", "runId": "run_1", "reason": strings.Repeat("界", 2048),
		})
		require.NoError(t, marshalErr)
		require.NoError(t, catalog.ValidateInput("workflow", "run", raw))

		raw, marshalErr = json.Marshal(map[string]any{
			"action": "cancel", "runId": "run_1", "reason": strings.Repeat("界", 2049),
		})
		require.NoError(t, marshalErr)
		require.Error(t, catalog.ValidateInput("workflow", "run", raw))
	})
}

func TestWorkflowExportedPatternsAvoidGoOnlyBracedHexEscapes(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)

	var patterns []string
	for _, object := range []string{"plan", "trigger", "run", "step"} {
		schema, ok := catalog.RenderActionInputSchemaJSON("workflow", object)
		require.True(t, ok, object)
		var document any
		require.NoError(t, json.Unmarshal([]byte(schema), &document))
		collectWorkflowSchemaPatterns(document, &patterns)
	}
	require.NotEmpty(t, patterns)
	for _, pattern := range patterns {
		require.NotContains(t, pattern, `\x{`, "exported OpenAPI 3.1 patterns must remain ECMA-262 portable")
	}
}

func collectWorkflowSchemaPatterns(value any, patterns *[]string) {
	switch value := value.(type) {
	case map[string]any:
		for key, child := range value {
			if key == "pattern" {
				if pattern, ok := child.(string); ok {
					*patterns = append(*patterns, pattern)
				}
				continue
			}
			collectWorkflowSchemaPatterns(child, patterns)
		}
	case []any:
		for _, child := range value {
			collectWorkflowSchemaPatterns(child, patterns)
		}
	}
}

func workflowTriggerCreateSchemaInput() map[string]any {
	return map[string]any{
		"action": "create", "key": "key", "displayName": "name",
		"planId": "plan", "planVersionId": "version", "enabled": true,
		"source": map[string]any{
			"type": "schedule",
			"schedule": map[string]any{
				"mode": "interval", "intervalSeconds": 60, "inputs": map[string]any{},
			},
		},
	}
}
