package exec

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func TestWorkflowOperationClassificationMatrix(t *testing.T) {
	for _, entry := range workflowOperationClassifications {
		t.Run(entry.Object+"/"+entry.Operation, func(t *testing.T) {
			require.Equal(t, entry.Risk, classifyWorkflowOperation(entry.Object, entry.Operation))
		})
	}
	for _, unknown := range [][2]string{{"plan", "submit"}, {"trigger", "replay"}, {"run", "retry"}, {"step", "wait"}, {"unknown", "create"}} {
		require.Equal(t, workflowOperationUnknown, classifyWorkflowOperation(unknown[0], unknown[1]))
		require.False(t, workflowOperationIsMutation(unknown[0], unknown[1]))
	}
}

func TestWorkflowOperationClassificationIsExhaustiveAgainstCatalog(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	require.NoError(t, validateWorkflowOperationMatrix(catalog.actionsSnapshot()))
}

func TestWorkflowOperationTransportUsesClassificationForIdempotency(t *testing.T) {
	var mu sync.Mutex
	seen := make(map[string]string)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Input struct {
				Action string `json:"action"`
			} `json:"input"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		mu.Lock()
		seen[body.Input.Action] = r.Header.Get("Idempotency-Key")
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"error":"operation probe"}`))
	}))
	t.Cleanup(server.Close)
	client := &apiclient.Client{
		ServerURL: server.URL, AgentID: "agt_1", LocalAgentID: "local_1", RouteWorkspaceID: "wsp_1",
		AuthToken: "token", HTTPClient: server.Client(),
	}
	remote := workflowRemote{client: client}
	for _, entry := range workflowOperationClassifications {
		if entry.Object == "step" {
			// Callback transport has a separate body contract; complete/fail/pause
			// carry stable keys, while park may omit its legacy key. All are
			// covered below.
			continue
		}
		_, err := callAgentWorkflowObject[map[string]any, map[string]any](
			context.Background(), remote, entry.Object, map[string]any{"action": entry.Operation},
		)
		require.Error(t, err, entry.Object+"/"+entry.Operation)
	}
	for _, entry := range workflowOperationClassifications {
		if entry.Object == "step" {
			continue
		}
		key := seen[entry.Operation]
		if entry.Risk == workflowOperationMutation {
			require.NotEmpty(t, key, entry.Object+"/"+entry.Operation)
		} else {
			require.Empty(t, key, entry.Object+"/"+entry.Operation)
		}
	}
}

func TestWorkflowStepTransportUsesIdempotencyForEveryMutatingCallback(t *testing.T) {
	var mu sync.Mutex
	seen := make(map[string]string)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Action string `json:"action"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		mu.Lock()
		seen[body.Action] = r.Header.Get("Idempotency-Key")
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		if body.Action == "list" {
			_, _ = w.Write([]byte(`{"steps":[]}`))
			return
		}
		_, _ = w.Write([]byte(`{"runId":"wri_1","stepId":"wfx_1","status":"waiting","runStatus":"running","replayed":false}`))
	}))
	t.Cleanup(server.Close)
	remote := workflowRemote{client: &apiclient.Client{
		ServerURL: server.URL, AgentID: "agt_1", LocalAgentID: "local_1", RouteWorkspaceID: "wsp_1",
		AuthToken: "token", HTTPClient: server.Client(),
	}}
	for _, action := range []string{"list", "get", "complete", "fail", "pause", "park"} {
		input := workflowStepInput{Action: action, RunID: "wri_1", StepID: "wfx_1"}
		if action == "list" {
			input.RunID, input.StepID = "", ""
		}
		if action == "complete" || action == "fail" || action == "pause" || action == "park" {
			input.IdempotencyKey = "callback-key-1"
		}
		_, _, err := remote.step(context.Background(), input, workflowRunContext{WorkspaceID: "wsp_1", AgentID: "agt_1", RunID: input.RunID, EffectID: input.StepID})
		require.NoError(t, err, action)
	}
	require.Empty(t, seen["list"])
	require.Empty(t, seen["get"])
	require.Equal(t, "callback-key-1", seen["complete"])
	require.Equal(t, "callback-key-1", seen["fail"])
	require.Equal(t, "callback-key-1", seen["pause"])
	require.Equal(t, "callback-key-1", seen["park"])
}

func TestOversizedWorkflowPlanShowNeverClaimsApplied(t *testing.T) {
	input := json.RawMessage(`{"action":"show","planId":"pln_1","section":"source"}`)
	_, err := marshalTypedActionResultForActionWithInput(
		"workflow", "plan", input,
		actionResult[workflowPlanResult]{Data: workflowPlanResult{agentView: &workflowPlanAgentViewResponse{
			PlanID: "pln_1", Payload: &workflowPayloadPageResponse{Section: "source", Encoding: "utf8", Value: string(make([]byte, maxSerializedResultBytes))},
		}}},
	)
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.False(t, typed.mutationApplied)
	require.Nil(t, typed.mutationReceipt)
	envelope := ApplyErrorBudget(err)
	require.Nil(t, envelope.Metadata)
	require.NotContains(t, envelope.Hint, "receipt")
}
