package arxiv

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/binding"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
)

type fakeInvoker struct {
	searchRequest SearchRequest
	getRequest    GetRequest
	err           error
}

func (fake *fakeInvoker) Search(
	_ context.Context,
	request SearchRequest,
) (SearchResult, error) {
	fake.searchRequest = request
	return SearchResult{Papers: []Paper{}}, fake.err
}

func (fake *fakeInvoker) Get(
	_ context.Context,
	request GetRequest,
) (PaperResult, error) {
	fake.getRequest = request
	return PaperResult{
		Paper: Paper{ID: request.ID},
	}, fake.err
}

func TestDefinitionAndRegistration(t *testing.T) {
	definition := Definition()
	require.Equal(t, Domain, definition.Domain)
	require.Equal(t, 1, definition.Version)
	require.Len(t, definition.Actions, 2)
	require.ElementsMatch(t, []string{
		ActionSearch,
		ActionGet,
	}, actionNames(definition.Actions))

	reg := registry.New()
	require.NoError(t, Register(reg))
	registered, ok := reg.LookupByIdentity(Domain, 1)
	require.True(t, ok)
	require.Equal(t, registry.Builtin, registered.Kind)
	require.Equal(t, registry.ConnectionNone, registered.ConnectionPolicy)
	require.Equal(
		t,
		"sha256:68e815318b43b9c9dc048fa21b238f3053afe21b9e4cc64342957b1aa6e579fa",
		registered.Definition.Digest,
	)
	require.Empty(t, registered.Binding.Endpoint.BaseURL)
}

func TestExecutorRoutesTwoActions(t *testing.T) {
	invoker := &fakeInvoker{}
	executor := NewExecutor(invoker)
	base := registry.BuiltinInvocation{
		WorkspaceID:   "wsp_1",
		AgentID:       "agent_1",
		AgentMemberID: "member_1",
	}

	search := base
	search.Action = ActionSearch
	search.Input = json.RawMessage(
		`{"query":"au:test","idList":["1605.08386v1"],"offset":2,"limit":3,"sortBy":"submittedDate","sortOrder":"ascending"}`,
	)
	_, err := executor.Invoke(context.Background(), search)
	require.NoError(t, err)
	require.Equal(t, "au:test", invoker.searchRequest.Query)
	require.Equal(t, []string{"1605.08386v1"}, invoker.searchRequest.IDList)
	require.Equal(t, 2, invoker.searchRequest.Offset)
	require.Equal(t, 3, invoker.searchRequest.Limit)

	get := base
	get.Action = ActionGet
	get.Input = json.RawMessage(`{"id":"1605.08386v1"}`)
	_, err = executor.Invoke(context.Background(), get)
	require.NoError(t, err)
	require.Equal(t, "1605.08386v1", invoker.getRequest.ID)

}

func TestSearchInputRequiresQueryOrNonEmptyIDList(t *testing.T) {
	schema := searchInputSchema()
	for _, raw := range []string{
		`{"query":"all:agentteam"}`,
		`{"idList":["1605.08386v1"]}`,
		`{"query":"all:agentteam","idList":[]}`,
		`{"query":"all:agentteam","idList":["1605.08386v1"]}`,
	} {
		_, err := binding.ValidateActionInput(schema, json.RawMessage(raw))
		require.NoError(t, err, raw)
	}
	for _, raw := range []string{
		`{}`,
		`{"idList":[]}`,
		`{"query":""}`,
	} {
		_, err := binding.ValidateActionInput(schema, json.RawMessage(raw))
		require.Error(t, err, raw)
	}
}

func TestExecutorClassifiesErrors(t *testing.T) {
	tests := []struct {
		serviceCode ErrorCode
		wantCode    extensionflow.InvocationErrorCode
	}{
		{ErrorValidation, extensionflow.InvocationValidation},
		{ErrorNotFound, extensionflow.InvocationNotFound},
		{ErrorRateLimited, extensionflow.InvocationRateLimited},
		{ErrorUpstreamFailure, extensionflow.InvocationUpstreamFailure},
	}
	for _, test := range tests {
		t.Run(string(test.serviceCode), func(t *testing.T) {
			invoker := &fakeInvoker{err: &Error{
				Code:    test.serviceCode,
				Message: "secret upstream detail",
			}}
			_, err := NewExecutor(invoker).Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					Action: ActionGet,
					Input:  json.RawMessage(`{"id":"1605.08386v1"}`),
				},
			)
			var invocationErr *extensionflow.InvocationError
			require.ErrorAs(t, err, &invocationErr)
			require.Equal(t, test.wantCode, invocationErr.Code)
			require.NotContains(t, invocationErr.Error(), "secret upstream detail")
		})
	}

	canceled := context.Canceled
	require.True(t, errors.Is(classifyError(canceled), context.Canceled))
}

func actionNames(actions []registry.DefinitionAction) []string {
	names := make([]string, 0, len(actions))
	for _, action := range actions {
		names = append(names, action.Name)
	}
	return names
}
