package exec

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agentview"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func registeredTestInvokers(t *testing.T) map[string]builtinInvoker {
	t.Helper()
	adapter := adapter{builtin: make(map[string]builtinInvoker)}
	for _, registration := range builtinRouteRegistrations(
		agentHandler{}, teamHandler{}, channelHandler{}, profileHandler{}, projectHandler{}, reviewHandler{},
		roleHandler{}, skillHandler{}, scheduledMessageHandler{}, taskHandler{}, workflowHandler{}, workspaceHandler{},
	) {
		registration.Install(adapter)
	}
	return adapter.builtin
}

type mutableExecutableTransport struct {
	transport   builtinExecutableTransport
	invocations int
	observed    builtinTransportCapability
}

func (transport *mutableExecutableTransport) capability(paths ...auditPath) builtinInvokerCapability {
	return transport.transport.capability(paths...)
}

type mutableTestInvoker struct {
	owned *mutableExecutableTransport
}

func (invoker mutableTestInvoker) Capability() builtinInvokerCapability {
	return invoker.owned.capability()
}

func (invoker mutableTestInvoker) Executable() builtinExecutableTransport {
	return invoker.owned
}

func (invoker mutableTestInvoker) Invoke(_ context.Context, _ runtimeAuthorization, _ agentListRequest) (actionResult[agentListResult], error) {
	invoker.owned.invocations++
	invoker.owned.observed = invoker.owned.transport.capability().Transport
	return actionResult[agentListResult]{Data: agentListResult{Agents: []agentExecSnapshot{}}}, nil
}

func TestAssembledExecutableRouteInvokesItsSealedTransportObject(t *testing.T) {
	transport := &mutableExecutableTransport{transport: localReadTransport{}}
	route := builtinRoute[agentListRequest, agentListResult]("agent", "list", mutableTestInvoker{owned: transport})
	require.NoError(t, route.validate())
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	routeAdapter := adapter{catalog: catalog, builtin: make(map[string]builtinInvoker)}
	route.Install(routeAdapter)
	invoker := routeAdapter.builtin["agent list"]
	require.Same(t, transport, invoker.executable)
	_, err = invoker.call(context.Background(), runtimeAuthorization{}, json.RawMessage(`{}`))
	require.NoError(t, err)
	require.Equal(t, 1, transport.invocations)

	transport.transport = remoteReadTransport{}
	require.Error(t, route.validate(), "replacing the owned transport must fail before serving")
	fresh := adapter{catalog: catalog, builtin: make(map[string]builtinInvoker)}
	var recovered any
	func() {
		defer func() { recovered = recover() }()
		route.Install(fresh)
	}()
	require.NotNil(t, recovered, "installation must reject a downgraded object before registration or invocation")
	require.Contains(t, fmt.Sprint(recovered), "capability mismatch")
	require.Equal(t, 1, transport.invocations, "the downgraded object must receive zero calls")
}

type countingExecRoundTripper struct {
	calls atomic.Int32
	base  http.RoundTripper
}

func (transport *countingExecRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
	transport.calls.Add(1)
	return transport.base.RoundTrip(request)
}

type invokerFamilyAgentReader struct {
	calls atomic.Int32
}

func (reader *invokerFamilyAgentReader) ListAgents(context.Context) ([]agentview.Snapshot, error) {
	reader.calls.Add(1)
	return []agentview.Snapshot{{ID: "agt_1", Name: "Agent 1"}}, nil
}
func (reader *invokerFamilyAgentReader) GetAgent(context.Context, string) (agentview.Snapshot, bool, error) {
	reader.calls.Add(1)
	return agentview.Snapshot{ID: "agt_1", Name: "Agent 1"}, true, nil
}

func TestInstalledOwnedInvokerFamiliesExecuteAndRejectReplacement(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/teams":
			_, _ = writer.Write([]byte(`{"teams":[],"hasMore":false}`))
		case "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/projects":
			_, _ = writer.Write([]byte(`{"project":{"id":"prj_1","rootPath":"/tmp/project","name":"project"}}`))
		case "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/managed-agents/start":
			_, _ = writer.Write([]byte(`{"status":"started"}`))
		case "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/schedules":
			_, _ = writer.Write([]byte(`{"id":"sch_1","name":"schedule","status":"active","schedule":{"kind":"after","seconds":1},"template":"hello","humanNotificationMode":"none","validation":{"valid":true}}`))
		case "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/workflow/run":
			_, _ = writer.Write([]byte(`{"run":{"id":"wri_1","planId":"pln_1","planVersionId":"wpv_1","status":"waiting","queuedAt":"2026-08-21T00:00:00Z"}}`))
		default:
			writer.WriteHeader(http.StatusNotFound)
			_, _ = writer.Write([]byte(`{"error":{"code":"not_found","problem":"not found"}}`))
		}
	}))
	t.Cleanup(server.Close)
	serverTransport := server.Client().Transport
	if serverTransport == nil {
		serverTransport = http.DefaultTransport
	}
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	reader := &invokerFamilyAgentReader{}
	newClient := func(counter *countingExecRoundTripper) *apiclient.Client {
		return &apiclient.Client{
			ServerURL: server.URL, AgentID: "agt_1", LocalAgentID: "agt_1", AuthToken: "token", RouteWorkspaceID: "wsp_1",
			HTTPClient: &http.Client{Transport: counter},
		}
	}
	families := []struct {
		name             string
		key              string
		input            json.RawMessage
		build            func(*apiclient.Client, *invokerFamilyAgentReader) builtinRouteRegistration
		replacement      func(*apiclient.Client, *invokerFamilyAgentReader) builtinExecutableTransport
		wantTransport    builtinTransportCapability
		originalCalls    func() int32
		replacementCalls func() int32
	}{
		{
			name: "local read", key: "agent list", input: json.RawMessage(`{}`),
			build: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[agentListRequest, agentListResult]("agent", "list", invokeAgentListObject{localReadInvoker{transport: localReadTransport{source: source}}})
			},
			replacement: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinExecutableTransport {
				return remoteReadTransport{client: client}
			},
			wantTransport: builtinTransportLocalRead,
			originalCalls: func() int32 { return reader.calls.Load() }, replacementCalls: func() int32 { return 0 },
		},
		{
			name: "remote read", key: "team list", input: json.RawMessage(`{}`),
			build: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[teamListInput, teamListOutput]("team", "list", invokeTeamListObject{remoteReadInvoker{transport: remoteReadTransport{client: client}}})
			},
			replacement: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinExecutableTransport {
				return localReadTransport{source: source}
			},
			wantTransport: builtinTransportRemoteRead,
		},
		{
			name: "natural mutation", key: "project create", input: json.RawMessage(`{"rootPath":"/tmp/project","name":"project"}`),
			build: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[projectCreateRequestDTO, projectCreateResponse]("project", "create", invokeProjectCreateObject{naturalMutationInvoker{transport: naturalMutationTransport{client: client}}})
			},
			replacement: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinExecutableTransport {
				return localReadTransport{source: source}
			},
			wantTransport: builtinTransportNaturalMutation,
		},
		{
			name: "request receipt", key: "agent start", input: json.RawMessage(`{"requestId":"start-1","agentId":"agt_1"}`),
			build: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[agentLifecycleRequest, agentStartResponse]("agent", "start", invokeAgentStartObject{requestReceiptInvoker{transport: requestReceiptTransport{client: client}}})
			},
			replacement: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinExecutableTransport {
				return localReadTransport{source: source}
			},
			wantTransport: builtinTransportRequestReceipt,
		},
		{
			name: "idempotency receipt", key: "scheduled-message create", input: json.RawMessage(`{"name":"schedule","schedule":{"kind":"after","seconds":1},"target":"#c","template":"hello","idempotencyKey":"schedule-1"}`),
			build: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[scheduledMessageCreateRequest, scheduledMessageRecord]("scheduled-message", "create", invokeScheduledMessageCreateObject{idempotencyReceiptInvoker{transport: idempotencyReceiptTransport{client: client}}})
			},
			replacement: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinExecutableTransport {
				return localReadTransport{source: source}
			},
			wantTransport: builtinTransportIdempotency,
		},
		{
			name: "Workflow receipt", key: "workflow run", input: json.RawMessage(`{"action":"show","runId":"wri_1"}`),
			build: func(client *apiclient.Client, _ *invokerFamilyAgentReader) builtinRouteRegistration {
				return builtinRoute[workflowRunRequest, workflowRunResult]("workflow", "run", invokeWorkflowRunObject{workflowReceiptInvoker{transport: workflowReceiptTransport{client: client}}})
			},
			replacement: func(_ *apiclient.Client, source *invokerFamilyAgentReader) builtinExecutableTransport {
				return localReadTransport{source: source}
			},
			wantTransport: builtinTransportWorkflow,
		},
	}
	for index := range families {
		testCase := families[index]
		t.Run(testCase.name, func(t *testing.T) {
			originalCounter := &countingExecRoundTripper{base: serverTransport}
			replacementCounter := &countingExecRoundTripper{base: serverTransport}
			originalClient := newClient(originalCounter)
			replacementClient := newClient(replacementCounter)
			route := testCase.build(originalClient, reader)
			require.NoError(t, route.validate())
			routeAdapter := adapter{catalog: catalog, builtin: make(map[string]builtinInvoker)}
			route.Install(routeAdapter)
			installed := routeAdapter.builtin[testCase.key]
			_, err := installed.call(context.Background(), runtimeAuthorization{Server: originalClient}, testCase.input)
			require.NoError(t, err)
			require.Equal(t, testCase.wantTransport, installed.capability.Transport)
			require.Greater(t, originalCounter.calls.Load()+reader.calls.Load(), int32(0), "the installed production operation must invoke its owned transport")
			readerCallsAfterOriginal := reader.calls.Load()

			route.Executable = testCase.replacement(replacementClient, reader)
			require.Error(t, route.validate(), "downgrading the registered transport must fail before install")
			fresh := adapter{catalog: catalog, builtin: make(map[string]builtinInvoker)}
			var recovered any
			func() {
				defer func() { recovered = recover() }()
				route.Install(fresh)
			}()
			require.NotNil(t, recovered, "downgraded registration must fail before touching a fresh adapter")
			require.Contains(t, fmt.Sprint(recovered), "capability mismatch")
			require.Zero(t, replacementCounter.calls.Load(), "replacement HTTP transport must receive zero calls")
			require.Equal(t, readerCallsAfterOriginal, reader.calls.Load(), "replacement local transport must receive zero calls")
			if testCase.originalCalls != nil {
				require.Equal(t, int32(1), testCase.originalCalls(), "local operation must use its original source exactly once")
			}
			if testCase.replacementCalls != nil {
				require.Equal(t, int32(0), testCase.replacementCalls())
			}
		})
	}
}

func TestErrorProjectorDescriptorIncludesSharedProjection(t *testing.T) {
	descriptor := buildErrorProjectorDescriptor()
	require.Len(t, descriptor.HTTPStatuses, 200)
	require.Equal(t, []string{"code", "problem", "hint", "next", "stage", "path", "retryable", "serverCode", "diagnostics", "truncation", "truncated", "metadata"}, descriptor.EnvelopeKeys)
	require.Equal(t, []string{"error"}, descriptor.TopLevelKeys)
	require.Equal(t, []string{"severity", "code", "path", "field", "index", "line", "column", "nodeId", "edgeId", "message"}, descriptor.DiagnosticKeys)
	require.True(t, descriptor.MutationAppliedClearsNext)
	require.False(t, descriptor.MutationAppliedRetryable)
	require.Len(t, descriptor.ExitCodes, len(errorProjectorCodes))
	require.Len(t, descriptor.StderrSamples, 3)
	require.Equal(t, reviewedErrorProjectorDigest, ErrorProjectorDigest())
	require.Equal(t, builtinErrorProjectorDigest(), ErrorProjectorDigest())
}

func TestErrorProjectorDescriptorRejectsDivergence(t *testing.T) {
	descriptor := buildErrorProjectorDescriptor()
	descriptor.EnvelopeKeys = append(descriptor.EnvelopeKeys, "cause")
	require.Error(t, validateErrorProjectorDescriptor(descriptor))
	require.NotEmpty(t, builtinErrorProjectorDigest())
}

func TestBuiltinErrorPolicyIdentitiesUseSharedProjector(t *testing.T) {
	policies := []builtinErrorPolicyIdentity{
		builtinErrorPolicyLocalRead, builtinErrorPolicyLocalGet,
		builtinErrorPolicyRemoteRead, builtinErrorPolicyNaturalMutation,
		builtinErrorPolicyReceiptMutation, builtinErrorPolicyWorkflowMutation,
	}
	for _, policy := range policies {
		t.Run(string(policy), func(t *testing.T) {
			require.True(t, policy.valid())
			codes := policy.codes()
			require.NotEmpty(t, codes)
			for _, code := range codes {
				require.True(t, code.valid())
				require.Positive(t, ErrorExitCode(code))
			}
		})
	}
	for _, policy := range []builtinErrorPolicyIdentity{
		builtinErrorPolicyRemoteRead, builtinErrorPolicyNaturalMutation,
		builtinErrorPolicyReceiptMutation, builtinErrorPolicyWorkflowMutation,
	} {
		require.NotContains(t, policy.codes(), CodeProviderAuthUnavailable)
		require.NotContains(t, policy.codes(), CodeUpstreamFailure)
	}
}

func TestBuiltinRouteAssemblyRejectsDowngradedExecutableTransport(t *testing.T) {
	routes := builtinRouteRegistrations(
		agentHandler{}, teamHandler{}, channelHandler{}, profileHandler{}, projectHandler{}, reviewHandler{},
		roleHandler{}, skillHandler{}, scheduledMessageHandler{}, taskHandler{}, workflowHandler{}, workspaceHandler{},
	)
	byKey := make(map[string]builtinRouteRegistration, len(routes))
	for _, route := range routes {
		byKey[builtinActionKey(route.Domain, route.Action)] = route
	}
	for _, tc := range []struct {
		name      string
		key       string
		executive builtinExecutableTransport
	}{
		{name: "local read to remote read", key: "agent list", executive: remoteReadTransport{}},
		{name: "remote read to local read", key: "team list", executive: localReadTransport{}},
		{name: "natural mutation to receipt", key: "project create", executive: requestReceiptTransport{}},
		{name: "request receipt to natural", key: "agent start", executive: naturalMutationTransport{}},
		{name: "idempotency receipt to request", key: "scheduled-message create", executive: requestReceiptTransport{}},
		{name: "Workflow receipt to natural", key: "workflow plan", executive: naturalMutationTransport{}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			route := byKey[tc.key]
			route.Executable = tc.executive
			err := route.validate()
			require.Error(t, err)
			require.Contains(t, err.Error(), tc.key)
			require.Contains(t, err.Error(), "executable transport")
		})
	}
}

func TestBuiltinInvokerCapabilitiesAreProductionAttested(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	invokers := registeredTestInvokers(t)
	require.NoError(t, validateBuiltinActionInvokers(catalog, invokers))

	for _, tc := range []struct {
		name      string
		key       string
		transport builtinTransportCapability
	}{
		{name: "local read", key: "agent list", transport: builtinTransportRemoteRead},
		{name: "remote read", key: "team list", transport: builtinTransportLocalRead},
		{name: "natural mutation", key: "project create", transport: builtinTransportRequestReceipt},
		{name: "request receipt", key: "agent start", transport: builtinTransportIdempotency},
		{name: "idempotency receipt", key: "scheduled-message create", transport: builtinTransportRequestReceipt},
		{name: "workflow receipt", key: "workflow plan", transport: builtinTransportRequestReceipt},
	} {
		t.Run(tc.name, func(t *testing.T) {
			mutated := cloneBuiltinInvokers(invokers)
			capability := mutated[tc.key].capability
			capability.Transport = tc.transport
			mutated[tc.key] = builtinInvoker{invoke: mutated[tc.key].invoke, capability: capability, executable: mutated[tc.key].executable}
			err := validateBuiltinActionInvokers(catalog, mutated)
			require.Error(t, err)
			require.Contains(t, err.Error(), tc.key)
			require.Contains(t, err.Error(), "capability")
		})
	}
}

func TestBuiltinInvokerCapabilityReplayBoundaryIsFailClosed(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	invokers := registeredTestInvokers(t)
	base := invokers["agent start"].capability
	mutations := map[string]func(*builtinInvokerCapability){
		"retry":           func(capability *builtinInvokerCapability) { capability.Retry = builtinRetryNone },
		"selector":        func(capability *builtinInvokerCapability) { capability.Selector = builtinSelectorNone },
		"digest":          func(capability *builtinInvokerCapability) { capability.Digest = builtinDigestNone },
		"selector paths":  func(capability *builtinInvokerCapability) { capability.SelectorPaths = nil },
		"error policy":    func(capability *builtinInvokerCapability) { capability.ErrorPolicy = builtinErrorPolicyLocalRead },
		"error projector": func(capability *builtinInvokerCapability) { capability.ErrorProjection = "stale-projector" },
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			mutated := cloneBuiltinInvokers(invokers)
			capability := base
			mutate(&capability)
			mutated["agent start"] = builtinInvoker{invoke: mutated["agent start"].invoke, capability: capability, executable: mutated["agent start"].executable}
			err := validateBuiltinActionInvokers(catalog, mutated)
			require.Error(t, err)
			require.Contains(t, err.Error(), "agent start")
		})
	}
}

func cloneBuiltinInvokers(input map[string]builtinInvoker) map[string]builtinInvoker {
	out := make(map[string]builtinInvoker, len(input))
	for key, invoker := range input {
		out[key] = invoker
	}
	return out
}

func TestBuiltinInvokerCoverageRejectsOrdinaryTransportDowngradeAtRouteAssembly(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	invokers := registeredTestInvokers(t)
	invokers["scheduled-message create"] = builtinInvoker{
		invoke: func(_ context.Context, _ runtimeAuthorization, _ json.RawMessage) ([]byte, error) {
			return nil, nil
		},
		capability: naturalMutationCapability().withRoute("scheduled-message", "create"),
		executable: naturalMutationTransport{},
	}
	err = validateBuiltinActionInvokers(catalog, invokers)
	require.Error(t, err)
	require.Contains(t, err.Error(), "scheduled-message create")
	require.Contains(t, err.Error(), "production transport")
}
