package invocation

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

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	invocationlang "github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
)

func TestInvokeManualDerivesAllIdentityAndScopeFromHumanRequest(t *testing.T) {
	now := time.Date(2026, 7, 21, 7, 8, 9, 123456000, time.UTC)
	fake := invocationFixture(now)
	handler := NewHandler(fake)
	request := humanRequest(t, http.MethodPost, "/workflow/plans/pln_1/invocations", map[string]string{"planId": "pln_1"})
	request.Gin.Request.Header.Set(idempotencyHeader, "invoke-call-1")

	response, err := handler.InvokeManual(api.WorkspaceScopedJSON[dto.WorkflowInvocationCreateRequest]{
		WorkspaceScopedRequest: request,
		Body: dto.WorkflowInvocationCreateRequest{
			PlanVersionID: "plv_1",
			Input:         json.RawMessage(`{"word":"画蛇添足"}`),
		},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.NotNil(t, fake.manualInput)
	require.Equal(t, "wsp_1", fake.manualInput.WorkspaceID)
	require.Equal(t, "pln_1", fake.manualInput.PlanID)
	require.Equal(t, "plv_1", fake.manualInput.PlanVersionID)
	require.Equal(t, Principal{UserID: "usr_1", MemberID: "mem_1", Role: "owner"}, fake.manualInput.Principal)
	require.Equal(t, Actor{Type: invocationlang.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, fake.manualInput.InvokedBy)
	require.Equal(t, "invoke-call-1", fake.manualInput.IdempotencyKey)
	require.JSONEq(t, `{"word":"画蛇添足"}`, string(fake.manualInput.Input))

	body := response.Body.(dto.WorkflowInvocationResponse)
	require.Equal(t, "wiv_1", body.ID)
	require.Empty(t, body.BindingID)
	require.Nil(t, body.BindingRevision)
	require.Equal(t, dto.WorkflowInvocationVersionRefResponse{ID: "plv_1"}, body.PlanVersion)
	require.Empty(t, body.BindingSourceID)
	require.Zero(t, body.BindingActivation)
	require.Equal(t, "manual", body.Source.Kind)
	require.JSONEq(t, `{"sourceKind":"manual"}`, string(body.Source.Provenance))
	require.Equal(t, "usr_owner", body.ExecutionOwner.UserID)
	require.Equal(t, "mem_owner", body.AuthorMemberID)
	require.Equal(t, "wri_1", body.RunID)
	require.Equal(t, invocationlang.RunStatusQueued, body.RunStatus)
	require.False(t, body.Replayed)
}

func TestInvokeManualReplayIsVisibleAndStillReturnsCreatedReceipt(t *testing.T) {
	fake := invocationFixture(time.Now().UTC())
	fake.accept.Replayed = true
	handler := NewHandler(fake)
	request := humanRequest(t, http.MethodPost, "/workflow/plans/pln_1/invocations", map[string]string{"planId": "pln_1"})
	request.Gin.Request.Header.Set(idempotencyHeader, "invoke-call-1")
	response, err := handler.InvokeManual(api.WorkspaceScopedJSON[dto.WorkflowInvocationCreateRequest]{
		WorkspaceScopedRequest: request,
		Body:                   dto.WorkflowInvocationCreateRequest{Input: json.RawMessage(`{}`)},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.True(t, response.Body.(dto.WorkflowInvocationResponse).Replayed)
	require.Equal(t, "pln_1", fake.manualInput.PlanID)
	require.Empty(t, fake.manualInput.PlanVersionID, "omitting planVersionId must select the Plan default in the service")
}

func TestInvokeManualRequiresInputAndIdempotencyKey(t *testing.T) {
	fake := invocationFixture(time.Now().UTC())
	handler := NewHandler(fake)
	request := humanRequest(t, http.MethodPost, "/workflow/plans/pln_1/invocations", map[string]string{"planId": "pln_1"})
	_, err := handler.InvokeManual(api.WorkspaceScopedJSON[dto.WorkflowInvocationCreateRequest]{WorkspaceScopedRequest: request})
	requireInvocationHTTPError(t, err, http.StatusBadRequest, "invocation_invalid_request")
	require.Nil(t, fake.manualInput)

	request = humanRequest(t, http.MethodPost, "/workflow/plans/pln_1/invocations", map[string]string{"planId": "pln_1"})
	_, err = handler.InvokeManual(api.WorkspaceScopedJSON[dto.WorkflowInvocationCreateRequest]{
		WorkspaceScopedRequest: request,
		Body:                   dto.WorkflowInvocationCreateRequest{Input: json.RawMessage(`{}`)},
	})
	requireInvocationHTTPError(t, err, http.StatusBadRequest, "invocation_idempotency_key_required")
	require.Nil(t, fake.manualInput)
}

func TestInvokeManualRejectsOversizedInputAs413(t *testing.T) {
	fake := invocationFixture(time.Now().UTC())
	handler := NewHandler(fake)
	request := humanRequest(t, http.MethodPost, "/workflow/plans/pln_1/invocations", map[string]string{"planId": "pln_1"})
	request.Gin.Request.Header.Set(idempotencyHeader, "invoke-call-1")
	_, err := handler.InvokeManual(api.WorkspaceScopedJSON[dto.WorkflowInvocationCreateRequest]{
		WorkspaceScopedRequest: request,
		Body:                   dto.WorkflowInvocationCreateRequest{Input: json.RawMessage(`{"x":"` + strings.Repeat("x", invocationlang.MaxInputBytes) + `"}`)},
	})
	requireInvocationHTTPError(t, err, http.StatusRequestEntityTooLarge, "invocation_request_too_large")
	require.Nil(t, fake.manualInput)
}

func TestListAndGetRemainWorkspaceScoped(t *testing.T) {
	now := time.Date(2026, 7, 21, 7, 8, 9, 0, time.UTC)
	fake := invocationFixture(now)
	fake.page = Page{Items: []Summary{{Invocation: fake.accept.Invocation, RunStatus: fake.accept.Run.Status}}, NextCursor: "next"}
	handler := NewHandler(fake)

	listRequest := humanRequest(t, http.MethodGet, "/workflow/invocations?planId=pln_1&limit=25&cursor=cur", nil)
	listResponse, err := handler.List(listRequest)
	require.NoError(t, err)
	require.Equal(t, &ListInput{WorkspaceID: "wsp_1", PlanID: "pln_1", Limit: 25, Cursor: "cur"}, fake.listInput)
	listBody := listResponse.Body.(dto.WorkflowInvocationListResponse)
	require.Len(t, listBody.Items, 1)
	require.Equal(t, invocationlang.RunStatusQueued, listBody.Items[0].RunStatus)
	require.Nil(t, listBody.Items[0].Input)
	require.Nil(t, listBody.Items[0].Source.Provenance)
	require.Equal(t, "next", listBody.NextCursor)

	getRequest := humanRequest(t, http.MethodGet, "/workflow/invocations/wiv_1", map[string]string{"invocationId": "wiv_1"})
	getResponse, err := handler.Get(getRequest)
	require.NoError(t, err)
	require.Equal(t, "wsp_1", fake.getWorkspaceID)
	require.Equal(t, "wiv_1", fake.getInvocationID)
	require.Equal(t, "wri_1", getResponse.Body.(dto.WorkflowInvocationResponse).RunID)
}

func TestListRequiresPlanIDAndValidLimit(t *testing.T) {
	fake := invocationFixture(time.Now().UTC())
	handler := NewHandler(fake)
	request := humanRequest(t, http.MethodGet, "/workflow/invocations", nil)
	_, err := handler.List(request)
	requireInvocationHTTPError(t, err, http.StatusBadRequest, "invocation_invalid_request")
	require.Nil(t, fake.listInput)

	request = humanRequest(t, http.MethodGet, "/workflow/invocations?planId=pln_1&limit=101", nil)
	_, err = handler.List(request)
	requireInvocationHTTPError(t, err, http.StatusBadRequest, "invocation_invalid_request")
	require.Nil(t, fake.listInput)
}

func invocationFixture(now time.Time) *fakeService {
	identitySnapshot := invocationlang.ExecutionIdentitySnapshot{
		OwnerUserID: "usr_owner", OwnerMemberID: "mem_owner", PrincipalMemberID: "mem_owner", AuthorMemberID: "mem_owner",
	}
	actor := Actor{Type: invocationlang.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	input := json.RawMessage(`{"word":"画蛇添足"}`)
	inputDigest := invocationlang.InputDigest(input)
	value := Invocation{
		ID: "wiv_1", RunID: "wri_1", WorkspaceID: "wsp_1",
		PlanID: "pln_1", PlanVersionID: "plv_1",
		SourceKind: invocationlang.SourceKindManual, SourceOccurrenceRef: "manual:sha256:one", SourceOccurredAt: now,
		InputJSON: input, InputDigest: inputDigest, SourceProvenanceJSON: json.RawMessage(`{"sourceKind":"manual"}`),
		Identity: identitySnapshot, InvokedBy: actor, IdempotencyKey: "invoke-call-1", RequestDigest: "digest", AcceptedAt: now,
	}
	run := Run{
		ID: "wri_1", WorkspaceID: "wsp_1", InvocationID: "wiv_1",
		PlanID: "pln_1", PlanVersionID: "plv_1", Status: invocationlang.RunStatusQueued,
		InputJSON: value.InputJSON, InputDigest: inputDigest, Identity: identitySnapshot, QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	return &fakeService{
		accept: AcceptResult{Invocation: value, Run: run},
		record: Record{Invocation: value, Run: run},
	}
}

func humanRequest(t *testing.T, method string, path string, params map[string]string) api.WorkspaceScopedRequest {
	t.Helper()
	ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
	ctx.Request = httptest.NewRequest(method, path, http.NoBody)
	for key, value := range params {
		ctx.Params = append(ctx.Params, gin.Param{Key: key, Value: value})
	}
	return api.WorkspaceScopedRequest{
		AuthenticatedRequest: api.AuthenticatedRequest{Gin: ctx, User: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}}},
		Workspace: workspace.Summary{
			WorkspaceID: "wsp_1", MemberID: "mem_1", MemberUserID: "usr_1", WorkspaceRole: "owner",
		},
	}
}

func requireInvocationHTTPError(t *testing.T, err error, status int, code string) {
	t.Helper()
	response, ok := classifyError(err)
	require.True(t, ok)
	require.Equal(t, status, response.Status)
	require.Equal(t, code, response.Code)
	require.Equal(t, "invocation", response.Stage)
}
