package exec

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

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

type fakeAuthorizer struct {
	authorized runtimeAuthorization
	err        error
	token      string
	serverID   string
	agentID    string
	calls      int
	releases   int
}

func (f *fakeAuthorizer) acquire(_ context.Context, token, serverID, agentID string) (runtimeAuthorization, func(), error) {
	f.calls++
	f.token, f.serverID, f.agentID = token, serverID, agentID
	if f.err != nil {
		return runtimeAuthorization{}, nil, f.err
	}
	return f.authorized, func() { f.releases++ }, nil
}

func TestRegisterJSONAuthorizesBeforeReadingBodyAndPassesScopedContext(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{authorized: runtimeAuthorization{
		ServerID: "server-1", AgentID: "agent-1", Server: &apiclient.Client{},
	}}
	adapter := adapter{authorizer: authorizer}
	var got actionScopedJSON[workspaceListRequest]
	r := gin.New()
	registerJSON(r.Group(""), adapter, "workspace", "list",
		func(_ context.Context, request actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			got = request
			return actionResult[workspaceListResponse]{Text: "ok"}, nil
		})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", bytes.NewBufferString(`{"kind":"summary"}`))
	req.Header.Set("Authorization", "Bearer runtime-token")
	r.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	require.Equal(t, 1, authorizer.calls)
	require.Equal(t, 1, authorizer.releases)
	require.Equal(t, "runtime-token", authorizer.token)
	require.Equal(t, "server-1", authorizer.serverID)
	require.Equal(t, "agent-1", authorizer.agentID)
	require.Equal(t, "summary", got.Body.Kind)
	require.Equal(t, "server-1", got.Authorized.ServerID)
}

func TestRegisterJSONRejectsUnauthorizedRequestBeforeReadingOversizedBody(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{err: token.ErrAgentTokenInactive}
	called := false
	r := gin.New()
	registerJSON(r.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			called = true
			return actionResult[workspaceListResponse]{}, nil
		})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", strings.NewReader(strings.Repeat("x", maxActionBodyBytes+1)))
	req.Header.Set("Authorization", "Bearer rejected")
	r.ServeHTTP(rec, req)

	require.Equal(t, http.StatusForbidden, rec.Code)
	require.Equal(t, 1, authorizer.calls)
	require.False(t, called)
}

func TestRegisterJSONMapsUnexpectedAuthorizerFailureToBoundedInternalError(t *testing.T) {
	gin.SetMode(gin.TestMode)
	secret := "unexpected database failure with bearer-secret"
	authorizer := &fakeAuthorizer{err: errors.New(secret)}
	r := gin.New()
	registerJSON(r.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			return actionResult[workspaceListResponse]{}, nil
		})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/servers/untrusted/agents/untrusted/workspace/list", strings.NewReader(`{}`))
	req.Header.Set("Authorization", "Bearer rejected")
	r.ServeHTTP(rec, req)

	require.Equal(t, http.StatusInternalServerError, rec.Code)
	require.JSONEq(t, `{"error":{"code":"internal","problem":"internal error"}}`, rec.Body.String())
	require.NotContains(t, rec.Body.String(), secret)
}

func TestRegisterJSONMapsAuthorizedAuthorizationCapacityToConflict(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{err: errExecAuthorizationCapacity}
	router := gin.New()
	registerJSON(router.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			return actionResult[workspaceListResponse]{}, nil
		})
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(
		http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", strings.NewReader(`{}`),
	)
	request.Header.Set("Authorization", "Bearer runtime-token")
	router.ServeHTTP(recorder, request)
	require.Equal(t, http.StatusConflict, recorder.Code)
	require.JSONEq(t, `{"error":{"code":"conflict","problem":"Exec runtime authorization is busy"}}`, recorder.Body.String())
}

func TestRegisterJSONRejectsMissingBearerWithoutAuthorizerCall(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{}
	r := gin.New()
	registerJSON(r.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			return actionResult[workspaceListResponse]{}, nil
		})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", bytes.NewBufferString(`{}`))
	r.ServeHTTP(rec, req)

	require.Equal(t, http.StatusForbidden, rec.Code)
	require.Zero(t, authorizer.calls)
}

func TestRegisterJSONRejectsOversizedBearerBeforeAuthorizerCall(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{}
	r := gin.New()
	registerJSON(r.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			return actionResult[workspaceListResponse]{}, nil
		})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", bytes.NewBufferString(`{}`))
	req.Header.Set("Authorization", "Bearer "+strings.Repeat("x", maxExecBearerBytes+1))
	r.ServeHTTP(rec, req)

	require.Equal(t, http.StatusForbidden, rec.Code)
	require.Zero(t, authorizer.calls)
}

func TestRegisterJSONRejectsOversizeUnknownAndTrailingJSON(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cases := []struct {
		name string
		body string
	}{
		{name: "oversize", body: `{"query":"` + strings.Repeat("x", maxActionBodyBytes) + `"}`},
		{name: "unknown field", body: `{"unexpected":true}`},
		{name: "trailing JSON", body: `{} {}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			authorizer := &fakeAuthorizer{authorized: runtimeAuthorization{Server: &apiclient.Client{}}}
			called := false
			r := gin.New()
			registerJSON(r.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
				func(_ context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
					called = true
					return actionResult[workspaceListResponse]{}, nil
				})
			rec := httptest.NewRecorder()
			req := httptest.NewRequest(http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", strings.NewReader(tc.body))
			req.Header.Set("Authorization", "Bearer runtime-token")
			r.ServeHTTP(rec, req)
			require.Equal(t, http.StatusBadRequest, rec.Code)
			require.Contains(t, rec.Body.String(), `"code":"validation"`)
			require.False(t, called)
			require.Equal(t, 1, authorizer.releases)
		})
	}
}

func TestRegisterJSONBoundsAuthenticatedBodyReadOnServerConnection(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stop := make(chan struct{})
	authorizer := &fakeAuthorizer{authorized: runtimeAuthorization{
		Server: &apiclient.Client{}, revoked: stop,
	}}
	called := false
	router := gin.New()
	registerJSON(router.Group(""), adapter{
		authorizer: authorizer,
		setReadDeadline: func(writer http.ResponseWriter, deadline time.Time) error {
			return http.NewResponseController(writer).SetReadDeadline(deadline)
		},
		bodyReadTimeout: 50 * time.Millisecond,
	}, "workspace", "list", func(
		_ context.Context,
		_ actionScopedJSON[workspaceListRequest],
	) (actionResult[workspaceListResponse], error) {
		called = true
		return actionResult[workspaceListResponse]{}, nil
	})
	server := httptest.NewServer(router)
	defer server.Close()

	address := strings.TrimPrefix(server.URL, "http://")
	connection, err := net.Dial("tcp", address)
	require.NoError(t, err)
	defer connection.Close()
	require.NoError(t, connection.SetReadDeadline(time.Now().Add(5*time.Second)))
	_, err = fmt.Fprintf(
		connection,
		"POST /servers/server-1/agents/agent-1/workspace/list HTTP/1.1\r\n"+
			"Host: %s\r\nAuthorization: Bearer runtime-token\r\n"+
			"Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{",
		address,
	)
	require.NoError(t, err)

	response, err := http.ReadResponse(bufio.NewReader(connection), &http.Request{Method: http.MethodPost})
	require.NoError(t, err)
	defer response.Body.Close()
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.False(t, called)
	require.Equal(t, 1, authorizer.releases)
}

func TestRegisterJSONCancelsHandlerWhenAuthorizationIsRevoked(t *testing.T) {
	gin.SetMode(gin.TestMode)
	revoked := make(chan struct{})
	authorizer := &fakeAuthorizer{authorized: runtimeAuthorization{
		Server: &apiclient.Client{}, revoked: revoked,
	}}
	entered := make(chan struct{})
	router := gin.New()
	registerJSON(router.Group(""), adapter{authorizer: authorizer}, "workspace", "list",
		func(ctx context.Context, _ actionScopedJSON[workspaceListRequest]) (actionResult[workspaceListResponse], error) {
			close(entered)
			<-ctx.Done()
			return actionResult[workspaceListResponse]{}, ctx.Err()
		})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(
		http.MethodPost, "/servers/server-1/agents/agent-1/workspace/list", strings.NewReader(`{}`),
	)
	request.Header.Set("Authorization", "Bearer runtime-token")
	done := make(chan struct{})
	go func() {
		router.ServeHTTP(recorder, request)
		close(done)
	}()
	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Fatal("handler did not start")
	}
	close(revoked)
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("runtime authorization revocation did not cancel the handler")
	}
	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"problem":"Exec action scope mismatch"`)
	require.Equal(t, 1, authorizer.releases)
}

type typedRouteInput struct {
	Name string `json:"name" jsonschema:"minLength=1"`
}

type typedRouteOutput struct {
	ID string `json:"id"`
}

func TestRegisterJSONValidatesSchemaAndReturnsTypedEnvelope(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizer := &fakeAuthorizer{authorized: runtimeAuthorization{
		ServerID: "server-1", AgentID: "agent-1", Server: &apiclient.Client{},
	}}
	descriptor, err := describe(actionSpec[typedRouteInput, typedRouteOutput]{
		Meta: actionMeta{
			Domain: "typed", Name: "create", Summary: "create typed value", Permission: permissionAgentScopedMutation,
		},
	})
	require.NoError(t, err)
	catalog, err := newDescriptorCatalog(descriptor)
	require.NoError(t, err)

	called := false
	r := gin.New()
	registerJSON(
		r.Group("/api/internal/exec"),
		adapter{authorizer: authorizer, catalog: catalog},
		"typed",
		"create",
		func(_ context.Context, request actionScopedJSON[typedRouteInput]) (actionResult[typedRouteOutput], error) {
			called = true
			return actionResult[typedRouteOutput]{
				Data: typedRouteOutput{ID: "value-1"},
				Text: "created",
			}, nil
		},
	)

	invalid := httptest.NewRecorder()
	invalidRequest := httptest.NewRequest(
		http.MethodPost,
		"/api/internal/exec/servers/server-1/agents/agent-1/typed/create",
		bytes.NewBufferString(`{"name":""}`),
	)
	invalidRequest.Header.Set("Authorization", "Bearer runtime-token")
	r.ServeHTTP(invalid, invalidRequest)
	require.Equal(t, http.StatusBadRequest, invalid.Code)
	require.Contains(t, invalid.Body.String(), `"code":"validation"`)
	require.False(t, called)

	valid := httptest.NewRecorder()
	validRequest := httptest.NewRequest(
		http.MethodPost,
		"/api/internal/exec/servers/server-1/agents/agent-1/typed/create",
		bytes.NewBufferString(`{"name":"value"}`),
	)
	validRequest.Header.Set("Authorization", "Bearer runtime-token")
	r.ServeHTTP(valid, validRequest)
	require.Equal(t, http.StatusOK, valid.Code)
	require.JSONEq(t, `{"data":{"id":"value-1"},"text":"created"}`, valid.Body.String())
	require.True(t, called)
	require.Equal(t, 2, authorizer.releases)
}

func TestDecodeValidatedWorkflowActionRejectsRawStructuralViolations(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)

	nested := `{}`
	for range workflowActionMaxJSONNesting {
		nested = `{"next":` + nested + `}`
	}
	cases := map[string]struct {
		raw     string
		problem string
	}{
		"nested duplicate": {
			raw:     `{"action":"start","planId":"pln_1","input":{"key":1,"key":2}}`,
			problem: `duplicate JSON field "key"`,
		},
		"excessive nesting": {
			raw:     `{"action":"start","planId":"pln_1","input":` + nested + `}`,
			problem: "JSON nesting exceeds limit",
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			_, decodeErr := decodeValidatedActionInput[workflowRunRequest](
				catalog, "workflow", "run", []byte(tc.raw),
			)
			require.Error(t, decodeErr)
			var execErr *Error
			require.ErrorAs(t, decodeErr, &execErr)
			require.Equal(t, CodeValidation, execErr.Code)
			require.ErrorContains(t, execErr.Cause, tc.problem)
		})
	}
}

func TestDynamicRouteReturnsNotFoundForUnknownAuthorizedAction(t *testing.T) {
	gin.SetMode(gin.TestMode)
	authorizations := newExecAuthorizationTestStore(t)
	lease, err := authorizations.Create(testRuntimeAuthorizationConfig())
	require.NoError(t, err)
	r := gin.New()
	RegisterRoutes(r.Group("/api/internal/exec"), RouteDependencies{Authorizations: authorizations})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/api/internal/exec/servers/server-1/agents/agent-1/nope/nope", bytes.NewBufferString(`{}`))
	req.Header.Set("Authorization", "Bearer "+lease.Token)
	r.ServeHTTP(rec, req)
	require.Equal(t, http.StatusNotFound, rec.Code)
}
