package github

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
	"github.com/tidbcloud/AgentTeam/internal/service/prwatch"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type recordingInvoker struct {
	invocations []prwatch.Invocation
	result      json.RawMessage
	err         error
}

func (invoker *recordingInvoker) Invoke(
	_ context.Context,
	invocation prwatch.Invocation,
) (json.RawMessage, error) {
	invoker.invocations = append(invoker.invocations, invocation)
	return append(json.RawMessage(nil), invoker.result...), invoker.err
}

func TestDefinitionIsClosedPreciseFiveActionBuiltin(t *testing.T) {
	definition := Definition()

	require.Equal(t, Provider, definition.Domain)
	require.Equal(t, 1, definition.Version)
	require.Equal(t, "GitHub Pull Request Watches", definition.Name)
	require.Equal(t, []string{
		prwatch.ActionWatchPullRequest,
		prwatch.ActionListWatches,
		prwatch.ActionPauseWatch,
		prwatch.ActionResumeWatch,
		prwatch.ActionUnwatch,
	}, definitionActionNames(definition))

	watch := definition.Actions[0]
	require.Equal(t, "mutation", watch.Safety.Class)
	require.Equal(t, false, watch.InputSchema["additionalProperties"])
	require.ElementsMatch(t, []any{"pullRequest"}, watch.InputSchema["required"])
	require.Equal(t, watchOutputSchema(), watch.OutputSchema)

	list := definition.Actions[1]
	require.Equal(t, "read", list.Safety.Class)
	require.True(t, list.Safety.Idempotent)
	require.Equal(t, false, list.InputSchema["additionalProperties"])
	require.Equal(t, listOutputSchema(), list.OutputSchema)

	for index, action := range definition.Actions[2:] {
		status := []string{"paused", "resumed", "deleted"}[index]
		require.Equal(t, "mutation", action.Safety.Class)
		require.Equal(t, false, action.InputSchema["additionalProperties"])
		require.NotNil(t, action.InputSchema["anyOf"])
		require.Equal(t, mutationOutputSchema(status), action.OutputSchema)
	}
}

func TestExecutorInvokesSharedServiceWithFixedGitHubIdentity(t *testing.T) {
	service := &recordingInvoker{result: json.RawMessage(`{"watchId":"pwt_1","status":"paused"}`)}
	executor := NewExecutor(service)
	result, err := executor.Invoke(context.Background(), registry.BuiltinInvocation{
		WorkspaceID: "wsp_1", AgentID: "agt_1", AgentMemberID: "mem_agent",
		ServiceInstallationID: "ins_github",
		ExtensionName:         "github-primary",
		Action:                prwatch.ActionPauseWatch,
		Input:                 json.RawMessage(`{"watchId":"pwt_1"}`),
	})
	require.NoError(t, err)
	require.Equal(t, "native", executor.Strategy())
	require.JSONEq(t, `{"watchId":"pwt_1","status":"paused"}`, string(result.Data))
	require.Equal(t, []prwatch.Invocation{{
		Provider: "github", WorkspaceID: "wsp_1", AgentID: "agt_1",
		AgentMemberID: "mem_agent", ServiceInstallationID: "ins_github",
		Action: prwatch.ActionPauseWatch,
		Input:  json.RawMessage(`{"watchId":"pwt_1"}`),
	}}, service.invocations)
}

func TestRegisterOwnsRealExecutorAndDefinition(t *testing.T) {
	service := &recordingInvoker{result: json.RawMessage(`{"watches":[]}`)}
	extensionRegistry := registry.New()

	require.NoError(t, Register(extensionRegistry, service))
	registered, ok := extensionRegistry.LookupByIdentity(Provider, 1)
	require.True(t, ok)
	require.Equal(t, registry.Builtin, registered.Kind)
	require.Equal(t, model.GitHubBuiltinExtensionDefinitionDigest, registered.Definition.Digest)
	require.Equal(t, Definition().Domain, registered.Definition.Domain)
	require.ElementsMatch(t, definitionActionNames(Definition()), definitionActionNames(registered.Definition))
	executor, revision, ok := extensionRegistry.BuiltinExecutorFor(
		registered.Definition.Digest,
	)
	require.True(t, ok)
	require.Equal(t, Revision, revision)
	_, isReal := executor.(*Executor)
	require.True(t, isReal)
}

func TestRegisterRejectsMissingSharedService(t *testing.T) {
	extensionRegistry := registry.New()

	require.Error(t, Register(extensionRegistry, nil))
	_, ok := extensionRegistry.LookupByIdentity(Provider, 1)
	require.False(t, ok)
}

func TestExecutorMapsSharedServiceStoreErrorsToInvocationContract(t *testing.T) {
	tests := []struct {
		name string
		err  error
		code extensionflow.InvocationErrorCode
	}{
		{name: "validation", err: storeerr.BadRequest("secret input"), code: extensionflow.InvocationValidation},
		{name: "permission", err: storeerr.Forbidden("secret identity"), code: extensionflow.InvocationPermissionDenied},
		{name: "not found", err: storeerr.NotFound("secret watch"), code: extensionflow.InvocationNotFound},
		{name: "rate limited", err: storeerr.Conflict("pull_request_subscription_rate_limited"), code: extensionflow.InvocationRateLimited},
		{name: "conflict", err: storeerr.Conflict("secret ambiguity"), code: extensionflow.InvocationConflict},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			executor := NewExecutor(&recordingInvoker{err: test.err})
			_, err := executor.Invoke(context.Background(), registry.BuiltinInvocation{
				WorkspaceID: "wsp_1", AgentID: "agt_1", AgentMemberID: "mem_agent",
				ServiceInstallationID: "ins_github",
				Action:                prwatch.ActionListWatches,
				Input:                 json.RawMessage(`{}`),
			})
			require.Error(t, err)
			code, ok := extensionflow.InvocationErrorCodeOf(err)
			require.True(t, ok)
			require.Equal(t, test.code, code)
			require.NotContains(t, err.Error(), "secret")
		})
	}
}

func definitionActionNames(definition registry.Definition) []string {
	names := make([]string, 0, len(definition.Actions))
	for _, action := range definition.Actions {
		names = append(names, action.Name)
	}
	return names
}
