package exec

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

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

func TestMutationReceiptSelectorValidatorBindsScalarAndCompositeSelectors(t *testing.T) {
	tests := []struct {
		name    string
		action  string
		input   string
		receipt mutationSuccessReceipt
		valid   bool
	}{
		{
			name:   "scalar exact selector",
			action: "channel update",
			input:  `{"requestId":"req-1","channelId":"chn-1","expectedVersion":3}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "channel update",
				Identifiers:    map[string]string{"channelId": "chn-1", "requestId": "req-1"},
				IdempotencyKey: "req-1",
			}, valid: true,
		},
		{
			name:   "scalar mismatch",
			action: "channel update",
			input:  `{"requestId":"req-1","channelId":"chn-1","expectedVersion":3}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "channel update",
				Identifiers:    map[string]string{"channelId": "chn-other", "requestId": "req-1"},
				IdempotencyKey: "req-1",
			}, valid: false,
		},
		{
			name:   "missing required scalar",
			action: "channel update",
			input:  `{"requestId":"req-1","channelId":"chn-1","expectedVersion":3}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "channel update", Identifiers: map[string]string{"requestId": "req-1"}, IdempotencyKey: "req-1",
			}, valid: false,
		},
		{
			name:   "workflow step requires both selectors",
			action: "step",
			input:  `{"action":"complete","runId":"wri-1","stepId":"wfx-1","idempotencyKey":"step-1","output":{}}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "workflow step",
				Identifiers: map[string]string{"runId": "wri-1", "stepId": "wfx-1", "idempotencyKey": "step-1"}, IdempotencyKey: "step-1",
			}, valid: true,
		},
		{
			name:   "workflow step run mismatch",
			action: "step",
			input:  `{"action":"complete","runId":"wri-1","stepId":"wfx-1","idempotencyKey":"step-1","output":{}}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "workflow step",
				Identifiers: map[string]string{"runId": "wri-other", "stepId": "wfx-1", "idempotencyKey": "step-1"}, IdempotencyKey: "step-1",
			}, valid: false,
		},
		{
			name:   "workflow step extra selector",
			action: "step",
			input:  `{"action":"complete","runId":"wri-1","stepId":"wfx-1","idempotencyKey":"step-1","output":{}}`,
			receipt: mutationSuccessReceipt{
				Applied: true, Action: "workflow step",
				Identifiers: map[string]string{"runId": "wri-1", "stepId": "wfx-1", "id": "foreign", "idempotencyKey": "step-1"}, IdempotencyKey: "step-1",
			}, valid: false,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			input := decodeReceiptObject(json.RawMessage(tc.input))
			parts := strings.SplitN(tc.action, " ", 2)
			if tc.action == "step" {
				parts = []string{"workflow", "step"}
			}
			require.Len(t, parts, 2)
			require.Equal(t, tc.valid, validateMutationReceiptAgainstInput(parts[0], parts[1], input, &tc.receipt))
		})
	}
}

func TestMutationReceiptSelectorValidatorBindsCompleteBatchOrder(t *testing.T) {
	input := decodeReceiptObject(json.RawMessage(`{"requestId":"batch-1","agentIds":["agt-1","agt-2","agt-3"]}`))
	base := func(value string) mutationSuccessReceipt {
		return mutationSuccessReceipt{
			Applied: true, Action: "agent delete",
			Identifiers: map[string]string{"agentIds": value, "requestId": "batch-1"}, IdempotencyKey: "batch-1",
		}
	}
	tests := []struct {
		name  string
		value string
		valid bool
	}{
		{name: "exact request order", value: `["agt-1","agt-2","agt-3"]`, valid: true},
		{name: "omitted member", value: `["agt-1","agt-2"]`},
		{name: "added member", value: `["agt-1","agt-2","agt-3","agt-4"]`},
		{name: "substituted member", value: `["agt-1","agt-other","agt-3"]`},
		{name: "reordered members", value: `["agt-2","agt-1","agt-3"]`},
		{name: "noncanonical JSON array", value: `["agt-1", "agt-2", "agt-3"]`},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			receipt := base(tc.value)
			require.Equal(t, tc.valid, validateMutationReceiptAgainstInput("agent", "delete", input, &receipt))
		})
	}

	ids := make([]string, 64)
	for index := range ids {
		ids[index] = fmt.Sprintf("agt-%02d", index)
	}
	request, err := json.Marshal(map[string]any{"requestId": "max-batch", "agentIds": ids})
	require.NoError(t, err)
	maxInput := decodeReceiptObject(request)
	encoded, err := json.Marshal(ids)
	require.NoError(t, err)
	maxReceipt := base(string(encoded))
	maxReceipt.Action = "agent delete"
	maxReceipt.IdempotencyKey = "max-batch"
	maxReceipt.Identifiers = map[string]string{"agentIds": string(encoded), "requestId": "max-batch"}
	require.True(t, validateMutationReceiptAgainstInput("agent", "delete", maxInput, &maxReceipt))
}

func TestMutationReceiptCreateSelectorsComeOnlyFromValidatedOutput(t *testing.T) {
	tests := []struct {
		name   string
		domain string
		action string
		input  string
		data   any
		key    string
		value  string
	}{
		{
			name: "project create", domain: "project", action: "create",
			input: `{"rootPath":"/repo","name":"demo"}`,
			data:  map[string]any{"project": map[string]any{"id": "prj-1"}}, key: "id", value: "prj-1",
		},
		{
			name: "workflow plan create", domain: "workflow", action: "plan",
			input: `{"action":"create","key":"daily","displayName":"Daily","source":"apiVersion: loop.plan/v1\nkind: PlanVersion\n"}`,
			data:  map[string]any{"plan": map[string]any{"id": "pln-1"}, "draft": map[string]any{"planId": "pln-1"}}, key: "planId", value: "pln-1",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			input := json.RawMessage(tc.input)
			receipt := mutationReceiptForAction(tc.domain, tc.action, input, tc.data)
			require.NotNil(t, receipt)
			require.Equal(t, tc.value, receipt.Identifiers[tc.key])
			// A decoder cannot see the complete oversized response, but it still
			// accepts only the operation's output-addressed key family.
			decoded := decodeReceiptObject(input)
			require.True(t, validateMutationReceiptAgainstInput(tc.domain, tc.action, decoded, receipt))
		})
	}
}

func TestMutationReceiptJSONRejectsDuplicateKeys(t *testing.T) {
	input := json.RawMessage(`{"requestId":"req-1","channelId":"chn-1","expectedVersion":1}`)
	raw := json.RawMessage(`{"mutation_applied":true,"mutation_receipt":{"applied":true,"action":"channel update","identifiers":{"channelId":"chn-1","channelId":"chn-other"},"idempotencyKey":"req-1"}}`)
	applied, receipt := decodeMutationReceiptMetadata(raw, "channel update", input)
	require.False(t, applied)
	require.Nil(t, receipt)
}
