package exec

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

	"github.com/stretchr/testify/require"

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

func TestClassifyExtensionUpstreamErrorPreservesStableProviderCodes(t *testing.T) {
	for upstreamCode, expected := range map[string]ErrorCode{
		"conflict":                      CodeConflict,
		"agent_extension_catalog_stale": CodeAgentExtensionCatalogStale,
		"provider_auth_unavailable":     CodeProviderAuthUnavailable,
		"rate_limited":                  CodeRateLimited,
		"upstream_failure":              CodeUpstreamFailure,
	} {
		err := classifyExtensionUpstreamError(&apiclient.APIError{
			StatusCode: 502,
			Method:     "POST",
			Path:       "/redacted",
			Code:       upstreamCode,
			Message:    "provider detail must stay internal",
		})
		var typed *Error
		require.ErrorAs(t, err, &typed)
		require.Equal(t, expected, typed.Code)
		envelope := ApplyErrorBudget(err)
		require.Equal(t, expected, envelope.Code)
		require.NotContains(t, envelope.Problem, "provider detail")
	}
}

func TestClassifyExtensionUpstreamResultBudgetIsActionable(t *testing.T) {
	err := classifyExtensionUpstreamError(apiclient.ErrResponseTooLarge, "gh", "list")
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.Equal(t, CodeResultBudgetExceeded, typed.Code)
	require.Contains(t, typed.Hint, "result_policy=narrow")
	require.Contains(t, typed.Next, "gh list")
}

func TestDecodeExtensionInvokeResultRejectsNonCanonicalResponse(t *testing.T) {
	for name, raw := range map[string]json.RawMessage{
		"missing data":   json.RawMessage(`{"domain":"github","action":"list"}`),
		"unknown field":  json.RawMessage(`{"domain":"github","action":"list","data":{},"secret":"x"}`),
		"trailing value": json.RawMessage(`{"domain":"github","action":"list","data":{}} {}`),
	} {
		t.Run(name, func(t *testing.T) {
			_, err := decodeExtensionInvokeResult(raw)
			require.Error(t, err)
		})
	}
}

func TestInvokeExtensionActionRejectsOversizedSuccessWithoutPartialData(t *testing.T) {
	server := newExtensionInvokeTestServer(
		t,
		`{"domain":"gh","action":"list","data":{"items":"`+strings.Repeat("x", maxSerializedResultBytes)+`"}}`,
	)
	defer server.Close()

	catalog := testRuntimeCatalog(t)
	_, err := invokeExtensionAction(
		context.Background(),
		runtimeAuthorization{
			ProcessGeneration: "process-1",
			RuntimeCatalog:    catalog,
			Server:            execTestRuntimeAPIClient(server),
		},
		mustLookupRuntimeAction(t, catalog, "gh", "list"),
		json.RawMessage(`{}`),
	)
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.Equal(t, CodeResultBudgetExceeded, typed.Code)
	require.NotContains(t, typed.Error(), "xxxxx")
}

func TestInvokeExtensionActionRejectsProviderInternalDomain(t *testing.T) {
	server := newExtensionInvokeTestServer(
		t,
		`{"domain":"provider-internal-name","action":"list","data":{}}`,
	)
	defer server.Close()

	_, err := invokeExtensionAction(
		context.Background(),
		runtimeAuthorization{
			ProcessGeneration: "process-1",
			RuntimeCatalog:    testRuntimeCatalog(t),
			Server:            execTestRuntimeAPIClient(server),
		},
		mustLookupRuntimeAction(t, testRuntimeCatalog(t), "gh", "list"),
		json.RawMessage(`{}`),
	)
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.Equal(t, CodeInternal, typed.Code)
}

func TestInvokeExtensionActionRejectsMismatchedIdentityAndSchema(t *testing.T) {
	for name, result := range map[string]string{
		"extension identity": `{"domain":"github","action":"list","data":{}}`,
		"action identity":    `{"domain":"gh","action":"delete","data":{}}`,
		"output schema":      `{"domain":"gh","action":"list","data":[]}`,
	} {
		t.Run(name, func(t *testing.T) {
			server := newExtensionInvokeTestServer(t, result)
			defer server.Close()
			_, err := invokeExtensionAction(
				context.Background(),
				runtimeAuthorization{
					ProcessGeneration: "process-1",
					RuntimeCatalog:    testRuntimeCatalog(t),
					Server:            execTestRuntimeAPIClient(server),
				},
				mustLookupRuntimeAction(t, testRuntimeCatalog(t), "gh", "list"),
				json.RawMessage(`{}`),
			)
			var typed *Error
			require.ErrorAs(t, err, &typed)
			require.Equal(t, CodeInternal, typed.Code)
		})
	}
}

func newExtensionInvokeTestServer(t *testing.T, result string) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(
		writer http.ResponseWriter,
		request *http.Request,
	) {
		require.Equal(t, http.MethodPost, request.Method)
		writer.Header().Set("Content-Type", "application/json")
		_, err := writer.Write([]byte(result))
		require.NoError(t, err)
	}))
}

func execTestRuntimeAPIClient(server *httptest.Server) *apiclient.Client {
	return &apiclient.Client{
		ServerURL: server.URL, AgentID: "remote-agent", AuthToken: "agent-token",
		RouteWorkspaceID: "ws-1", HTTPClient: server.Client(),
	}
}

func mustLookupRuntimeAction(
	t *testing.T,
	catalog *RuntimeCatalog,
	domain string,
	action string,
) RuntimeAction {
	t.Helper()
	result, ok := catalog.Lookup(domain, action)
	require.True(t, ok)
	return result
}

func TestClassifyExtensionUpstreamErrorSurfacesUnauthorizedByStatus(t *testing.T) {
	// The agent runtime rejects a rotated-out daemon token with HTTP 401 and a
	// body that carries no machine-readable code. Classification must key off
	// the status, not the absent code, and must not report a generic internal
	// failure that hides the token-lifecycle root cause.
	err := classifyExtensionUpstreamError(&apiclient.APIError{
		StatusCode: http.StatusUnauthorized,
		Method:     "POST",
		Path:       "/redacted",
		Message:    "unauthorized",
	})
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.Equal(t, CodePermissionDenied, typed.Code)
	envelope := ApplyErrorBudget(err)
	require.Equal(t, CodePermissionDenied, envelope.Code)
	require.Contains(t, envelope.Problem, "authentication")
}
