package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

func TestSnapshotRecoverablePostCommitDeliveryIsIndependentFromCallerState(t *testing.T) {
	appMessage := &message.AppMessage{
		ID:       "msg_snapshot",
		Content:  "before",
		Metadata: map[string]any{"phase": "before"},
	}
	delivery := message.MessageDelivery{
		MessageID:  appMessage.ID,
		AppMessage: appMessage,
		Message: message.ReceiveMessage{
			MessageID: appMessage.ID,
			Content:   "before",
			Metadata:  map[string]any{"phase": "before"},
		},
		Recipients: []message.AgentRecipient{{AgentID: "agt_before"}},
		Conversation: &message.DeliveryConversationProjection{
			ConversationID: "channel:chn_snapshot",
			ScopeID:        "chn_snapshot",
			Kind:           message.ConversationKindChannel,
			Projection:     map[string]any{"title": "before"},
		},
	}

	prepared, payload, err := encodeRecoverablePostCommitDelivery(delivery)
	require.NoError(t, err)
	require.NotEmpty(t, payload)
	canonical, err := message.UnmarshalScopeFanoutPayload(payload)
	require.NoError(t, err)
	snapshot := canonical.Delivery
	snapshot.AppMessage = &canonical.AppMessage

	appMessage.Content = "after"
	appMessage.Metadata["phase"] = "after"
	delivery.Message.Content = "after"
	delivery.Message.Metadata["phase"] = "after"
	delivery.Recipients[0].AgentID = "agt_after"
	delivery.Conversation.Projection["title"] = "after"

	require.True(t, prepared.RecoverableStreamProjection)
	require.True(t, snapshot.RecoverableStreamProjection)
	require.NotNil(t, snapshot.AppMessage)
	require.Equal(t, "before", snapshot.AppMessage.Content)
	require.Equal(t, "before", snapshot.AppMessage.Metadata["phase"])
	require.Equal(t, "before", snapshot.Message.Content)
	require.Equal(t, "before", snapshot.Message.Metadata["phase"])
	require.Equal(t, "agt_before", snapshot.Recipients[0].AgentID)
	require.Equal(t, "before", snapshot.Conversation.Projection["title"])
}

func TestSnapshotRecoverablePostCommitDeliveryRejectsFollowupTree(t *testing.T) {
	delivery := message.MessageDelivery{
		MessageID: "msg_parent",
		AppMessage: &message.AppMessage{
			ID: "msg_parent",
		},
		FollowupDeliveries: []message.MessageDelivery{{MessageID: "msg_followup"}},
	}

	_, _, err := encodeRecoverablePostCommitDelivery(delivery)
	require.ErrorContains(t, err, "must not contain caller-owned followup deliveries")
}

func TestSnapshotRecoverablePostCommitDeliveryRejectsTargetedOrIncompleteDelivery(t *testing.T) {
	_, _, err := encodeRecoverablePostCommitDelivery(message.MessageDelivery{})
	require.ErrorContains(t, err, "requires message and AppMessage")

	// Targeted non-Task deliveries have no frozen lease-coordinated path and
	// must keep their explicit rejection.
	_, _, err = encodeRecoverablePostCommitDelivery(message.MessageDelivery{
		MessageID:               "msg_targeted",
		AppMessage:              &message.AppMessage{ID: "msg_targeted"},
		HumanRecipientsTargeted: true,
	})
	require.ErrorContains(t, err, "must use its frozen delivery path")
}

func TestSnapshotRecoverablePostCommitDeliveryAllowsTargetedTaskDelivery(t *testing.T) {
	// A targeted Task delivery routes the first fanout through the frozen
	// lease-coordinated deliverInlineTaskMessage path, so the store-owned
	// recoverable token is allowed to snapshot it.
	delivery := message.MessageDelivery{
		MessageID:               "msg_task_targeted",
		ThreadID:                "tsk_task_targeted",
		HumanRecipientsTargeted: true,
		AppMessage: &message.AppMessage{
			ID:          "msg_task_targeted",
			ChannelType: message.ConversationKindTask,
		},
		Message: message.ReceiveMessage{
			TargetKey: "tsk_task_targeted",
			Source:    message.ContributionSource{Kind: message.ContributionSourceKindProviderEvent, RefType: message.ContributionSourceRefTypeServiceEventDelivery, RefID: "svc_1"},
		},
	}
	prepared, payload, err := encodeRecoverablePostCommitDelivery(delivery)
	require.NoError(t, err)
	require.True(t, prepared.RecoverableStreamProjection)
	require.NotNil(t, prepared.AppMessage)
	require.NotEmpty(t, payload)

	decoded, err := message.UnmarshalScopeFanoutPayload(payload)
	require.NoError(t, err)
	require.Equal(t, "msg_task_targeted", decoded.AppMessage.ID)
	require.Equal(t, "msg_task_targeted", decoded.Delivery.MessageID)
	require.True(t, decoded.Delivery.HumanRecipientsTargeted)
}

func TestEmitPostCommitMessageDetachesFromCanceledRequestContext(t *testing.T) {
	store := &Store{}
	observed := false
	store.SetCommittedMessageFanout(&recordingCommittedMessageFanout{
		query: func(ctx context.Context, _ message.MessageDelivery) {
			observed = true
			require.NoError(t, ctx.Err())
			deadline, ok := ctx.Deadline()
			require.True(t, ok)
			remaining := time.Until(deadline)
			require.Positive(t, remaining)
			require.LessOrEqual(t, remaining, committedMessagePostCommitTimeout)
		},
	})

	requestCtx, cancel := context.WithCancel(context.Background())
	cancel()
	appMessage := &message.AppMessage{ID: "msg_detached"}
	_, payload, err := encodeRecoverablePostCommitDelivery(message.MessageDelivery{
		MessageID:  appMessage.ID,
		AppMessage: appMessage,
	})
	require.NoError(t, err)
	store.emitPostCommitMessage(requestCtx, postCommitMessage{
		workspaceID: "wsp_detached",
		producerID:  committedMessageProducerExternalAppRuntimeReply,
		payload:     payload,
	})

	require.True(t, observed)
}

func TestSetCommittedMessageFanoutRejectsNilBinding(t *testing.T) {
	require.PanicsWithValue(t, "committed message fanout must not be nil", func() {
		(&Store{}).SetCommittedMessageFanout(nil)
	})
}

func TestSetCommittedMessageFanoutBindsOnce(t *testing.T) {
	store := &Store{}
	store.SetCommittedMessageFanout(&recordingCommittedMessageFanout{})
	require.PanicsWithValue(t, "committed message fanout is already configured", func() {
		store.SetCommittedMessageFanout(&recordingCommittedMessageFanout{})
	})
}

func TestAppServiceMessageContractRegistryIsClosed(t *testing.T) {
	tests := []struct {
		contract   appServiceMessageContract
		producerID committedMessageProducerID
		owner      appServiceMessagePostCommitOwner
	}{
		{appServiceMessageAppNotification, committedMessageProducerAppNotification, appServiceMessageStoreOwnedRecoverable},
		{appServiceMessageExternalAppRuntimeReply, committedMessageProducerExternalAppRuntimeReply, appServiceMessageStoreOwnedRecoverable},
		{appServiceMessageProviderEvent, committedMessageProducerProviderEvent, appServiceMessageCallerOwned},
		{appServiceMessageKnowledgeQA, committedMessageProducerKnowledgeQA, appServiceMessageCallerOwned},
	}
	for _, test := range tests {
		producerID, owner, ok := test.contract.descriptor()
		require.True(t, ok)
		require.Equal(t, test.producerID, producerID)
		require.Equal(t, test.owner, owner)
	}

	_, _, ok := appServiceMessageContract(0).descriptor()
	require.False(t, ok)
	require.True(t, committedMessageProducerAppNotification.ownsRecoverablePostCommitFanout())
	require.True(t, committedMessageProducerExternalAppRuntimeReply.ownsRecoverablePostCommitFanout())
	require.True(t, committedMessageProducerPlatformServiceAccountBroadcast.ownsRecoverablePostCommitFanout())
	require.False(t, committedMessageProducerProviderEvent.ownsRecoverablePostCommitFanout())
	require.False(t, committedMessageProducerKnowledgeQA.ownsRecoverablePostCommitFanout())
	require.True(t, messageAppendCallerOwned.valid())
	require.True(t, messageAppendAppServiceContracted.valid())
	require.True(t, messageAppendStoreOwnedRecoverable.valid())
	require.False(t, messageAppendLifecycle(0).valid())
}
