package im

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
	"github.com/tidbcloud/AgentTeam/internal/unread"
)

type taskChangeLifecycleGuardFunc func(
	ctx context.Context,
	workspaceID string,
	scopeIDs []string,
	publish func() error,
) (bool, error)

func (guard taskChangeLifecycleGuardFunc) WithActiveTaskScopes(
	ctx context.Context,
	workspaceID string,
	scopeIDs []string,
	publish func() error,
) (bool, error) {
	return guard(ctx, workspaceID, scopeIDs, publish)
}

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

	hub := NewStreamEventHub()
	subscription := hub.SubscribeStreamEvents(context.Background(), "wsp_1", "mem_reader", "usr_reader")
	defer subscription.Close()
	require.True(t, hub.SubscribeVisibleScopeEvents(subscription, "wsp_1", "mem_reader", "chn_1"))

	var mu sync.Mutex
	var guardedScopeIDs [][]string
	guard := taskChangeLifecycleGuardFunc(func(
		_ context.Context,
		workspaceID string,
		scopeIDs []string,
		publish func() error,
	) (bool, error) {
		require.Equal(t, "wsp_1", workspaceID)
		mu.Lock()
		guardedScopeIDs = append(guardedScopeIDs, append([]string(nil), scopeIDs...))
		mu.Unlock()
		if len(scopeIDs) == 1 && scopeIDs[0] == "scp_task_2" {
			require.NoError(t, publish())
			return true, nil
		}
		return false, nil
	})
	bridge := NewTaskChangeBridge(NewLiveEventEmitter(hub, nil, nil)).
		WithTaskScopeLifecycleGuard(guard)

	created := taskboard.NewTaskCreatedEvent(
		taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
		"chn_1",
		[]taskboard.Task{
			{ID: "tsk_1", TaskScopeID: "scp_task_1", ChannelID: "chn_1"},
			{ID: "tsk_2", TaskScopeID: "scp_task_2", ChannelID: "chn_1"},
		},
	)
	bridge.HandleTaskChange(context.Background(), created)
	bridge.HandleTaskChange(context.Background(), taskboard.NewTaskUpdatedEvent(
		taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
		taskboard.Task{ID: "tsk_1", TaskScopeID: "scp_task_1", ChannelID: "chn_1"},
	))

	event := receiveHubEvent(t, subscription)
	require.Equal(t, liveEventTypeTaskCreated, event.Type)
	payloadTasks, ok := event.Payload["tasks"].([]dto.Task)
	require.True(t, ok)
	require.Len(t, payloadTasks, 1)
	require.Equal(t, "tsk_2", payloadTasks[0].ID)
	requireNoHubEvent(t, subscription)
	mu.Lock()
	defer mu.Unlock()
	require.Equal(t, [][]string{{"scp_task_1"}, {"scp_task_2"}, {"scp_task_1"}}, guardedScopeIDs)
}

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

	hub := NewStreamEventHub()
	subscription := hub.SubscribeStreamEvents(context.Background(), "wsp_1", "mem_reader", "usr_reader")
	defer subscription.Close()
	require.True(t, hub.SubscribeVisibleScopeEvents(subscription, "wsp_1", "mem_reader", "chn_1"))

	type contextKey struct{}
	parent := context.WithValue(context.Background(), contextKey{}, "caller-context")
	ctx, cancel := context.WithCancel(parent)
	cancel()
	entered := make(chan struct{})
	release := make(chan struct{})
	guard := taskChangeLifecycleGuardFunc(func(
		receivedCtx context.Context,
		workspaceID string,
		scopeIDs []string,
		publish func() error,
	) (bool, error) {
		require.Equal(t, "caller-context", receivedCtx.Value(contextKey{}))
		require.NoError(t, receivedCtx.Err(), "post-commit publication must survive request cancellation")
		_, hasDeadline := receivedCtx.Deadline()
		require.True(t, hasDeadline, "detached post-commit publication must remain bounded")
		require.Equal(t, "wsp_1", workspaceID)
		require.Equal(t, []string{"scp_task_1"}, scopeIDs)
		close(entered)
		<-release
		require.NoError(t, publish())
		return true, nil
	})
	bridge := NewTaskChangeBridge(NewLiveEventEmitter(hub, nil, nil)).
		WithTaskScopeLifecycleGuard(guard)

	done := make(chan struct{})
	go func() {
		defer close(done)
		bridge.HandleTaskChange(ctx, taskboard.NewTaskUpdatedEvent(
			taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
			taskboard.Task{ID: "tsk_1", TaskScopeID: "scp_task_1", ChannelID: "chn_1"},
		))
	}()

	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Fatal("lifecycle guard was not entered")
	}
	requireNoHubEvent(t, subscription)
	close(release)

	event := receiveHubEvent(t, subscription)
	require.Equal(t, liveEventTypeTaskUpdated, event.Type)
	payloadTask, ok := event.Payload["task"].(dto.Task)
	require.True(t, ok)
	require.Equal(t, "tsk_1", payloadTask.ID)
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("Task update callback did not complete")
	}
}

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

	hub := NewStreamEventHub()
	subscription := hub.SubscribeStreamEvents(context.Background(), "wsp_1", "mem_reader", "usr_reader")
	defer subscription.Close()
	require.True(t, hub.SubscribeVisibleScopeEvents(subscription, "wsp_1", "mem_reader", "chn_1"))

	var guardCalls int
	guard := taskChangeLifecycleGuardFunc(func(
		_ context.Context,
		_ string,
		_ []string,
		_ func() error,
	) (bool, error) {
		guardCalls++
		return false, nil
	})
	bridge := NewTaskChangeBridge(NewLiveEventEmitter(hub, nil, nil)).
		WithTaskScopeLifecycleGuard(guard)

	bridge.HandleTaskChange(context.Background(), taskboard.NewTaskDeletedEvent(
		taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
		"chn_1",
		"tsk_1",
	))

	event := receiveHubEvent(t, subscription)
	require.Equal(t, liveEventTypeTaskDeleted, event.Type)
	require.Equal(t, "tsk_1", event.Payload["taskId"])
	require.Zero(t, guardCalls)
}

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

	var guardedScopeIDs []string
	guard := taskChangeLifecycleGuardFunc(func(
		_ context.Context,
		_ string,
		scopeIDs []string,
		publish func() error,
	) (bool, error) {
		guardedScopeIDs = append([]string(nil), scopeIDs...)
		return true, publish()
	})
	bridge := NewTaskChangeBridge(NewLiveEventEmitter(NewStreamEventHub(), nil, nil)).
		WithTaskScopeLifecycleGuard(guard)

	bridge.HandleTaskChange(context.Background(), taskboard.NewTaskUpdatedEvent(
		taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
		taskboard.Task{ID: "tsk_rolling", ChannelID: "chn_1"},
	))

	require.Equal(t, []string{"tsk_rolling"}, guardedScopeIDs)
}

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

	runtime, err := unread.New(unread.Config{})
	require.NoError(t, err)
	require.NoError(t, runtime.WarmScopeRoster("wsp_1", "tsk_1", unread.ScopeTypeTask, []unread.RosterMember{
		{MemberID: "mem_author", UserID: "usr_author", Visibility: "public"},
		{MemberID: "mem_reader", UserID: "usr_reader", Visibility: "public"},
	}))
	_, err = runtime.ApplyMessage(unread.MessageStored{
		WorkspaceID:        "wsp_1",
		ScopeID:            "tsk_1",
		ScopeType:          unread.ScopeTypeTask,
		Seq:                1,
		AuthorMemberID:     "mem_author",
		RecipientMemberIDs: []string{"mem_reader"},
	})
	require.NoError(t, err)

	hub := NewStreamEventHub()
	subscription := hub.SubscribeStreamEvents(context.Background(), "wsp_1", "mem_reader", "usr_reader")
	defer subscription.Close()
	require.True(t, hub.SubscribeVisibleScopeEvents(subscription, "wsp_1", "mem_reader", "chn_1"))

	emitter := NewLiveEventEmitter(hub, nil, nil)
	bridge := NewTaskChangeBridge(emitter).WithUnreadRuntime(runtime, emitter)
	bridge.HandleTaskChange(context.Background(), taskboard.NewTaskDeletedEvent(
		taskboard.NewUserServiceContext("wsp_1", "usr_author", "Author"),
		"chn_1",
		"tsk_1",
	))

	unreadEvent := receiveHubEvent(t, subscription)
	require.Equal(t, liveEventTypeUnreadUpdated, unreadEvent.Type)
	require.Equal(t, "tsk_1", unreadEvent.ScopeID)
	require.Equal(t, false, unreadEvent.Payload["hasUnread"])
	require.Equal(t, int64(0), unreadEvent.Payload["count"])

	deletedEvent := receiveHubEvent(t, subscription)
	require.Equal(t, liveEventTypeTaskDeleted, deletedEvent.Type)
	require.Equal(t, "tsk_1", deletedEvent.Payload["taskId"])

	late, err := runtime.ApplyMessage(unread.MessageStored{
		WorkspaceID:        "wsp_1",
		ScopeID:            "tsk_1",
		ScopeType:          unread.ScopeTypeTask,
		Seq:                2,
		AuthorMemberID:     "mem_author",
		RecipientMemberIDs: []string{"mem_reader"},
	})
	require.NoError(t, err)
	require.Zero(t, late.AccountedMembers)
	require.Empty(t, runtime.ScopeStates("wsp_1", "mem_reader", []string{"tsk_1"}))
}
