package exec

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

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

func TestActionEndpointRequiresNumericLoopbackAuthorization(t *testing.T) {
	endpoint, err := ActionEndpoint(
		"http://127.0.0.1:43123/api/internal/exec/servers/server%20one/agents/agent%20one",
		"workspace",
		"list",
	)
	require.NoError(t, err)
	require.Equal(
		t,
		"http://127.0.0.1:43123/api/internal/exec/servers/server%20one/agents/agent%20one/workspace/list",
		endpoint,
	)

	for _, value := range []string{
		"https://127.0.0.1:43123/api/internal/exec/servers/server-1/agents/agent-1",
		"http://localhost:43123/api/internal/exec/servers/server-1/agents/agent-1",
		"http://192.0.2.1:43123/api/internal/exec/servers/server-1/agents/agent-1",
		"http://127.0.0.1/api/internal/exec/servers/server-1/agents/agent-1",
		"http://user@127.0.0.1:43123/api/internal/exec/servers/server-1/agents/agent-1",
		"http://127.0.0.1:43123/not-exec",
		"http://127.0.0.1:43123/api/internal/exec/servers/server%2Fone/agents/agent-1",
		"http://127.0.0.1:43123/api/internal/exec/servers/server-1/agents/agent-1?token=bad",
	} {
		t.Run(value, func(t *testing.T) {
			_, endpointErr := ActionEndpoint(value, "workspace", "list")
			require.Error(t, endpointErr)
		})
	}
}

func TestLocalClientNeverForwardsExecAuthorizationThroughRedirect(t *testing.T) {
	var redirected atomic.Bool
	receiver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		redirected.Store(true)
	}))
	defer receiver.Close()

	redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, receiver.URL, http.StatusTemporaryRedirect)
	}))
	defer redirector.Close()

	_, err := (LocalClient{}).Call(
		context.Background(),
		redirector.URL+"/api/internal/exec/servers/server-1/agents/agent-1",
		"secret-token",
		"workspace",
		"list",
		json.RawMessage(`{}`),
	)
	require.Error(t, err)
	require.Contains(t, strings.ToLower(err.Error()), "status 307")
	require.False(t, redirected.Load())
}

func TestLocalClientRejectsNonCanonicalErrorEnvelopeFields(t *testing.T) {
	for _, test := range []struct {
		name string
		body string
	}{
		{name: "unknown top-level field", body: `{"error":{"code":"validation","problem":"bad","cause":"secret"}}`},
		{name: "missing required problem", body: `{"error":{"code":"validation"}}`},
		{name: "explicit false optional field", body: `{"error":{"code":"validation","problem":"bad","retryable":false}}`},
		{name: "unknown diagnostic field", body: `{"error":{"code":"validation","problem":"bad","diagnostics":[{"severity":"error","code":"input.invalid","message":"bad","extra":1}]}}`},
	} {
		t.Run(test.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusBadRequest)
				_, _ = w.Write([]byte(test.body))
			}))
			defer server.Close()
			_, err := (LocalClient{}).Call(context.Background(), server.URL+"/api/internal/exec/servers/server-1/agents/agent-1", "secret-token", "workspace", "list", json.RawMessage(`{}`))
			var typed *Error
			require.ErrorAs(t, err, &typed)
			require.Equal(t, CodeInternal, typed.Code)
			require.Equal(t, "Exec error envelope is invalid", typed.Problem)
		})
	}
}

func TestLocalClientAcceptsExactCanonicalErrorEnvelope(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"error":{"code":"validation","problem":"bad","hint":"fix"}}`))
	}))
	defer server.Close()
	_, err := (LocalClient{}).Call(context.Background(), server.URL+"/api/internal/exec/servers/server-1/agents/agent-1", "secret-token", "workspace", "list", json.RawMessage(`{}`))
	var typed *Error
	require.ErrorAs(t, err, &typed)
	require.Equal(t, CodeValidation, typed.Code)
	require.Equal(t, "bad", typed.Problem)
	require.Equal(t, "fix", typed.Hint)
}

func TestLocalClientForwardsMutationReplaySelectors(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "req-agent-1", r.Header.Get("Idempotency-Key"))
		require.Equal(t, "req-agent-1", r.Header.Get("X-Request-ID"))
		_, _ = w.Write([]byte(`{"data":{"status":"ok"}}`))
	}))
	defer server.Close()

	_, err := (LocalClient{}).Call(
		context.Background(),
		server.URL+"/api/internal/exec/servers/server-1/agents/agent-1",
		"secret-token", "agent", "start", json.RawMessage(`{"requestId":"req-agent-1","agentId":"agent-1"}`),
	)
	require.NoError(t, err)
}

func TestLocalClientBoundsResponseFromLoopbackListener(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"data":"` + strings.Repeat("x", maxSerializedResultBytes) + `"}`))
	}))
	defer server.Close()

	_, err := (LocalClient{}).Call(
		context.Background(),
		server.URL+"/api/internal/exec/servers/server-1/agents/agent-1",
		"secret-token", "workspace", "list", json.RawMessage(`{}`),
	)
	require.Error(t, err)
	require.Contains(t, err.Error(), "result budget")
	var budgetErr *Error
	require.ErrorAs(t, err, &budgetErr)
	require.Equal(t, CodeResultBudgetExceeded, budgetErr.Code)
	require.Contains(t, budgetErr.Next, "workspace list")
}
