package appruntime

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

type internalHandlerFunc func(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error)

func (handler internalHandlerFunc) HandleInternalDelivery(ctx context.Context, envelope InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
	return handler(ctx, envelope)
}

type fakeInternalDispatcherStore struct {
	targets       []serviceactor.RuntimeInternalDispatchTarget
	envelopes     []serviceactor.DeliveryEnvelope
	claim         serviceactor.DeliveryDispatchClaim
	completion    *serviceactor.InternalHandlerDeliveryCompletion
	retry         *serviceactor.DeliveryAttemptRetrySchedule
	completionErr error
	retryAt       time.Time
}

func (store *fakeInternalDispatcherStore) ListRuntimeInternalDispatchTargets(context.Context, serviceactor.RuntimeInternalDispatchTargetList) ([]serviceactor.RuntimeInternalDispatchTarget, error) {
	return append([]serviceactor.RuntimeInternalDispatchTarget(nil), store.targets...), nil
}

func (store *fakeInternalDispatcherStore) ClaimDeliveryEnvelopes(_ context.Context, params serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error) {
	store.claim = params
	return append([]serviceactor.DeliveryEnvelope(nil), store.envelopes...), nil
}

func (store *fakeInternalDispatcherStore) CompleteInternalHandlerDelivery(_ context.Context, params serviceactor.InternalHandlerDeliveryCompletion) (serviceactor.InternalHandlerDeliveryCompletionResult, error) {
	store.completion = &params
	if store.completionErr != nil {
		return serviceactor.InternalHandlerDeliveryCompletionResult{}, store.completionErr
	}
	return serviceactor.InternalHandlerDeliveryCompletionResult{}, nil
}

func (store *fakeInternalDispatcherStore) ScheduleDeliveryAttemptRetry(_ context.Context, params serviceactor.DeliveryAttemptRetrySchedule) (serviceactor.DeliveryAttemptRetryResult, error) {
	store.retry = &params
	retryAt := store.retryAt
	if retryAt.IsZero() {
		retryAt = params.Now.Add(time.Minute)
	}
	return serviceactor.DeliveryAttemptRetryResult{NextRetryAt: retryAt}, nil
}

func TestInternalHandlerRegistryRejectsDuplicatesAndSeals(t *testing.T) {
	t.Parallel()

	registry := NewInternalHandlerRegistry()
	handler := internalHandlerFunc(func(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
		return InternalHandlerOutcome{}, nil
	})
	require.NoError(t, registry.Register("agentteam.approval", handler))
	require.Error(t, registry.Register("agentteam.approval", handler))
	registry.Seal()
	require.Error(t, registry.Register("agentteam.task", handler))
	_, ok := registry.lookup("agentteam.approval")
	require.True(t, ok)
}

func TestInternalDispatcherUsesInternalRouteWithoutExternalCredentials(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 7, 10, 11, 0, 0, 0, time.UTC)
	store := &fakeInternalDispatcherStore{envelopes: []serviceactor.DeliveryEnvelope{{
		Delivery: serviceactor.EventDelivery{
			ID: "svced_1", WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1",
			EventID: "bna_1", DeliveryPolicy: serviceactor.AppRuntimeDeliveryPolicyInternalHandler,
			ServiceConnectionRef: "builtin:agentteam.approval",
		},
		Attempt: serviceactor.DeliveryAttempt{
			ID: "svcea_1", WorkspaceID: "wsp_1", TransportKind: serviceactor.RuntimeTransportKindInternalHandler,
		},
		EventType: "agentteam.app.notification.action", EventAction: "approve", EventPayload: []byte(`{"action":"approve"}`),
	}}}
	registry := NewInternalHandlerRegistry()
	var handled InternalHandlerEnvelope
	require.NoError(t, registry.Register("agentteam.approval", internalHandlerFunc(func(_ context.Context, envelope InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
		handled = envelope
		return InternalHandlerOutcome{Effect: runeffect.TaskDiscussionEffect("decision_1"), ResultJSON: []byte(`{"status":"approved"}`)}, nil
	})))
	registry.Seal()
	dispatcher := NewInternalDispatcher(InternalDispatcherOptions{
		Store: store, Registry: registry, InstanceID: "server-a", Now: func() time.Time { return now },
		LeaseDuration: time.Minute, HandlerTimeout: 5 * time.Second,
	})

	result, err := dispatcher.Dispatch(context.Background(), InternalDispatchParams{
		WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", HandlerKey: "agentteam.approval",
	})
	require.NoError(t, err)
	require.Equal(t, 1, result.Claimed)
	require.Equal(t, 1, result.Completed)
	require.Zero(t, result.Retried)
	require.Equal(t, "bna_1", handled.Delivery.EventID)
	require.Equal(t, serviceactor.RuntimeTransportKindInternalHandler, store.claim.TransportKind)
	require.Equal(t, serviceactor.AppRuntimeDeliveryPolicyInternalHandler, store.claim.DeliveryPolicy)
	require.Equal(t, "builtin:agentteam.approval", store.claim.ServiceConnectionRef)
	require.Empty(t, store.claim.ServiceRuntimeCredentialID)
	require.Empty(t, store.claim.ServiceRuntimeSessionID)
	require.Empty(t, store.claim.CallbackURLSnapshot)
	require.Equal(t, "internal:server-a:agentteam.approval", store.claim.LeaseOwner)
	require.NotNil(t, store.completion)
	require.Equal(t, "agentteam.approval", store.completion.HandlerKey)
	require.Equal(t, runeffect.EffectKindTaskDiscussion, store.completion.Effect.Kind)
	require.Nil(t, store.retry)
}

func TestInternalDispatcherRejectsExternalCredentialsBeforeCallingHandler(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 7, 10, 11, 15, 0, 0, time.UTC)
	store := &fakeInternalDispatcherStore{envelopes: []serviceactor.DeliveryEnvelope{{
		Delivery: serviceactor.EventDelivery{ID: "svced_1", WorkspaceID: "wsp_1"},
		Attempt: serviceactor.DeliveryAttempt{
			ID:                         "svcea_1",
			WorkspaceID:                "wsp_1",
			TransportKind:              serviceactor.RuntimeTransportKindInternalHandler,
			ServiceRuntimeCredentialID: "svcrc_external",
		},
		DeliveryToken: "external-delivery-token",
	}}}
	registry := NewInternalHandlerRegistry()
	handled := false
	require.NoError(t, registry.Register("agentteam.approval", internalHandlerFunc(func(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
		handled = true
		return InternalHandlerOutcome{}, nil
	})))
	dispatcher := NewInternalDispatcher(InternalDispatcherOptions{
		Store: store, Registry: registry, Now: func() time.Time { return now },
	})

	result, err := dispatcher.Dispatch(context.Background(), InternalDispatchParams{
		WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", HandlerKey: "agentteam.approval",
	})
	require.NoError(t, err)
	require.Equal(t, 1, result.Retried)
	require.False(t, handled)
	require.Nil(t, store.completion)
	require.NotNil(t, store.retry)
	require.Equal(t, InternalHandlerErrorCompletion, store.retry.ErrorCode)
	require.Equal(t, "internal handler envelope contains external runtime credentials", store.retry.ErrorDetail)
}

func TestInternalDispatcherMissingHandlerUsesStableRetry(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 7, 10, 11, 30, 0, 0, time.UTC)
	retryAt := now.Add(2 * time.Minute)
	store := &fakeInternalDispatcherStore{
		retryAt: retryAt,
		envelopes: []serviceactor.DeliveryEnvelope{{
			Delivery: serviceactor.EventDelivery{ID: "svced_1", WorkspaceID: "wsp_1"},
			Attempt:  serviceactor.DeliveryAttempt{ID: "svcea_1", WorkspaceID: "wsp_1", TransportKind: serviceactor.RuntimeTransportKindInternalHandler},
		}},
	}
	dispatcher := NewInternalDispatcher(InternalDispatcherOptions{Store: store, Registry: NewInternalHandlerRegistry(), Now: func() time.Time { return now }})

	result, err := dispatcher.Dispatch(context.Background(), InternalDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", HandlerKey: "agentteam.missing"})
	require.NoError(t, err)
	require.Equal(t, 1, result.Claimed)
	require.Equal(t, 1, result.Retried)
	require.NotNil(t, store.retry)
	require.Equal(t, InternalHandlerErrorNotRegistered, store.retry.ErrorCode)
	require.Equal(t, retryAt, *result.Items[0].RetryAt)
	require.Nil(t, store.completion)
}

func TestInternalDispatcherHandlerAndCompletionErrorsUseStableRetryCodes(t *testing.T) {
	t.Parallel()

	for name, testCase := range map[string]struct {
		handler       InternalHandler
		completionErr error
		wantCode      string
	}{
		"handler": {
			handler: internalHandlerFunc(func(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
				return InternalHandlerOutcome{}, errors.New("temporary database failure")
			}),
			wantCode: InternalHandlerErrorExecution,
		},
		"completion": {
			handler: internalHandlerFunc(func(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error) {
				return InternalHandlerOutcome{Effect: runeffect.TaskDiscussionEffect("decision_1")}, nil
			}),
			completionErr: errors.New("temporary completion failure"),
			wantCode:      InternalHandlerErrorCompletion,
		},
	} {
		testCase := testCase
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			now := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
			store := &fakeInternalDispatcherStore{
				completionErr: testCase.completionErr,
				envelopes: []serviceactor.DeliveryEnvelope{{
					Delivery: serviceactor.EventDelivery{ID: "svced_1", WorkspaceID: "wsp_1"},
					Attempt:  serviceactor.DeliveryAttempt{ID: "svcea_1", WorkspaceID: "wsp_1", TransportKind: serviceactor.RuntimeTransportKindInternalHandler},
				}},
			}
			registry := NewInternalHandlerRegistry()
			require.NoError(t, registry.Register("agentteam.approval", testCase.handler))
			dispatcher := NewInternalDispatcher(InternalDispatcherOptions{Store: store, Registry: registry, Now: func() time.Time { return now }})
			result, err := dispatcher.Dispatch(context.Background(), InternalDispatchParams{WorkspaceID: "wsp_1", ServiceInstallationID: "svcinst_1", HandlerKey: "agentteam.approval"})
			require.NoError(t, err)
			require.Equal(t, 1, result.Retried)
			require.NotNil(t, store.retry)
			require.Equal(t, testCase.wantCode, store.retry.ErrorCode)
		})
	}
}
