package app

import (
	"math"
	"reflect"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

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

func TestAppRuntimeDeliveryRequiredScopesByKind(t *testing.T) {
	tests := []struct {
		kind string
		want []string
	}{
		{serviceactor.SubscriptionKindAppMention, []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite}},
		{serviceactor.SubscriptionKindAppNotificationAction, []string{AppNotificationActionScopeRead}},
	}
	for _, tt := range tests {
		got, err := appRuntimeDeliveryRequiredScopes(tt.kind)
		if err != nil {
			t.Fatalf("appRuntimeDeliveryRequiredScopes(%q): %v", tt.kind, err)
		}
		if !reflect.DeepEqual(got, tt.want) {
			t.Fatalf("appRuntimeDeliveryRequiredScopes(%q) = %#v, want %#v", tt.kind, got, tt.want)
		}
	}
	if _, err := appRuntimeDeliveryRequiredScopes(serviceactor.SubscriptionKindPullRequest); err == nil {
		t.Fatal("pull_request unexpectedly accepted as a App runtime delivery kind")
	}
}

func TestAppRuntimeCompletionEffectsAreKindSpecific(t *testing.T) {
	messageReply := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindMessage), EffectRef: "msg_1", MessageID: "msg_1"}
	taskReply := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindTaskDiscussion), EffectRef: "entry_1"}
	messageUpdate := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindMessageUpdate), EffectRef: "btn_1", MessageID: "msg_1"}
	resolved := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindNotificationResolved), EffectRef: "btn_1", MessageID: "msg_1"}
	withdrawn := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindMessageWithdrawal), EffectRef: "btn_1", MessageID: "msg_1"}

	for _, effect := range []serviceactor.DeliveryEffect{messageReply, taskReply} {
		if !appRuntimeEffectAllowedForSubscription(serviceactor.SubscriptionKindAppMention, effect) {
			t.Fatalf("App mention rejected reply effect %+v", effect)
		}
		if appRuntimeEffectAllowedForSubscription(serviceactor.SubscriptionKindAppNotificationAction, effect) {
			t.Fatalf("App notification action accepted reply effect %+v", effect)
		}
	}
	for _, effect := range []serviceactor.DeliveryEffect{messageUpdate, resolved, withdrawn} {
		if !appRuntimeEffectAllowedForSubscription(serviceactor.SubscriptionKindAppNotificationAction, effect) {
			t.Fatalf("App notification action rejected mutation effect %+v", effect)
		}
		if appRuntimeEffectAllowedForSubscription(serviceactor.SubscriptionKindAppMention, effect) {
			t.Fatalf("App mention accepted notification mutation effect %+v", effect)
		}
	}

	malformed := serviceactor.DeliveryEffect{EffectKind: string(runeffect.EffectKindNotificationResolved), EffectRef: "btn_1"}
	if appRuntimeEffectAllowedForSubscription(serviceactor.SubscriptionKindAppNotificationAction, malformed) {
		t.Fatal("App notification action accepted a mutation effect without message id")
	}
}

func TestAppRuntimeClaimKindsAreDerivedFromCurrentGrants(t *testing.T) {
	tests := []struct {
		name   string
		events []string
		scopes []string
		want   []string
	}{
		{name: "mention", events: []string{registry.AppEventMessageMention}, scopes: []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite}, want: []string{serviceactor.SubscriptionKindAppMention}},
		{name: "action", events: []string{registry.AppEventNotificationAction}, scopes: []string{AppNotificationActionScopeRead}, want: []string{serviceactor.SubscriptionKindAppNotificationAction}},
		{name: "both", events: []string{registry.AppEventMessageMention, registry.AppEventNotificationAction}, scopes: []string{AppNotificationActionScopeRead, AppRuntimeScopeMessageWrite, AppMentionScopeRead}, want: []string{serviceactor.SubscriptionKindAppMention, serviceactor.SubscriptionKindAppNotificationAction}},
		{name: "notification writer has no event claim", scopes: []string{AppRuntimeScopeMessageWrite, AppNotificationScopeWrite}, want: []string{}},
		{name: "mention grants cannot manufacture event authority", scopes: []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite}, want: []string{}},
		{name: "partial mention grant", events: []string{registry.AppEventMessageMention}, scopes: []string{AppMentionScopeRead}, want: []string{}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			capability := registry.StoredAppRuntimeCapability{Supported: true, Events: tt.events}
			if got := appRuntimeSubscriptionKinds(capability, tt.scopes); !reflect.DeepEqual(got, tt.want) {
				t.Fatalf("appRuntimeSubscriptionKinds(%#v, %#v) = %#v, want %#v", tt.events, tt.scopes, got, tt.want)
			}
		})
	}
	if !appRuntimeNotificationWriterEligible(registry.StoredAppRuntimeCapability{Notifications: registry.StoredAppNotificationCapability{Supported: true}}, []string{AppNotificationScopeWrite, AppRuntimeScopeMessageWrite}) {
		t.Fatal("notification writer scopes were not recognized as runtime eligibility")
	}
	if appRuntimeNotificationWriterEligible(registry.StoredAppRuntimeCapability{}, []string{AppNotificationScopeWrite, AppRuntimeScopeMessageWrite}) {
		t.Fatal("notification scopes manufactured notification capability authority")
	}
}

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

	first, err := appRuntimeReplyPayloadDigest(" done ", map[string]any{
		"phase":  "complete",
		"result": map[string]any{"b": 2, "a": 1},
	})
	require.NoError(t, err)
	replayed, err := appRuntimeReplyPayloadDigest("done", map[string]any{
		"result": map[string]any{"a": 1, "b": 2},
		"phase":  "complete",
	})
	require.NoError(t, err)
	require.Equal(t, first, replayed)

	changed, err := appRuntimeReplyPayloadDigest("done", map[string]any{"phase": "failed"})
	require.NoError(t, err)
	require.NotEqual(t, first, changed)

	_, err = appRuntimeReplyPayloadDigest("done", map[string]any{"invalid": math.NaN()})
	require.ErrorIs(t, err, ErrBadRequest)
}

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

	require.Equal(t,
		appRuntimeReplyClientMessageID("delivery-1", " progress-1 "),
		appRuntimeReplyClientMessageID(" delivery-1 ", "progress-1"),
	)
	require.NotEqual(t,
		appRuntimeReplyClientMessageID("delivery-1", "progress-1"),
		appRuntimeReplyClientMessageID("delivery-1", "progress-2"),
	)
	require.Equal(t,
		appRuntimeReplyEffectIDKeyPrefix("delivery-1", " progress-1 "),
		appRuntimeReplyEffectIDKeyPrefix(" delivery-1 ", "progress-1"),
	)
	require.NotEqual(t,
		appRuntimeReplyEffectID("delivery-1", "attempt-1", "progress-1"),
		appRuntimeReplyEffectID("delivery-1", "attempt-2", "progress-1"),
	)
	require.Equal(t,
		appNotificationMutationEffectID(" attempt-1 ", " mutation-1 "),
		appNotificationMutationEffectID("attempt-1", "mutation-1"),
	)
	require.NotEqual(t,
		appNotificationMutationEffectID("attempt-1", "mutation-1"),
		appNotificationMutationEffectID("attempt-2", "mutation-1"),
	)
}

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

	first, err := appRuntimeResultJSON(map[string]any{
		"status": "ok",
		"details": map[string]any{
			"b": float64(2),
			"a": float64(1),
		},
	})
	require.NoError(t, err)
	replayed, err := appRuntimeResultJSON(map[string]any{
		"details": map[string]any{
			"a": float64(1),
			"b": float64(2),
		},
		"status": "ok",
	})
	require.NoError(t, err)
	require.True(t, appRuntimeResultJSONEqual(first, replayed))
	require.False(t, appRuntimeResultJSONEqual(first, []byte(`{"status":"failed"}`)))
	require.False(t, appRuntimeResultJSONEqual(
		[]byte(`{"value":9007199254740992}`),
		[]byte(`{"value":9007199254740993}`),
	))

	_, err = appRuntimeResultJSON(map[string]any{"invalid": math.NaN()})
	require.ErrorIs(t, err, ErrBadRequest)

	_, err = appRuntimeResultJSON(map[string]any{"oversized": string(make([]byte, appRuntimeMaxResultBytes))})
	require.ErrorIs(t, err, ErrBadRequest)

	deep := map[string]any{"leaf": true}
	for index := 0; index <= appRuntimeMaxResultValueDepth; index++ {
		deep = map[string]any{"child": deep}
	}
	_, err = appRuntimeResultJSON(deep)
	require.ErrorIs(t, err, ErrBadRequest)
}

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

	_, err := appRuntimeReplyEffectMetadata("reply-1", strings.Repeat("a", 64), map[string]any{
		"invalid": math.NaN(),
	})
	require.ErrorIs(t, err, ErrBadRequest)

	_, err = appRuntimeReplyEffectMetadata("reply-1", strings.Repeat("a", 64), map[string]any{
		"oversized": string(make([]byte, appRuntimeMaxMetadataBytes)),
	})
	require.ErrorIs(t, err, ErrBadRequest)
}
