package uisession

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

// TestSlowSubscriberFullChannelAtomicRemoval covers P4-BE-12: when the
// bounded channel is full, Selected/Cleared cause atomic removal + close
// (NOT "guarantee delivered"); Advanced coalesces to latest within the
// same epoch; no cross-boundary coalescing. Tests cover N=1 and N=2.
//
// Codex5 P6: the at-capacity eviction contract DOES close the channel,
// so the deadlock-guarded drain agentteam is appropriate here — the only
// legitimate exit is via the closed-channel branch. The 1s guard fails
// fast on contract violations instead of hanging the test.
func TestSlowSubscriberFullChannelAtomicRemoval(t *testing.T) {
	for _, capacity := range []int{1, 2} {
		t.Run("capacity", func(t *testing.T) {
			testSlowSubscriberAtCapacity(t, capacity)
		})
	}
}

func testSlowSubscriberAtCapacity(t *testing.T, capacity int) {
	now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC)
	store := NewStore(Options{
		Now:                 func() time.Time { return now },
		SubscriptionChannel: capacity,
	})
	expiresAt := now.Add(time.Hour)

	const sessionID = "session-slow"
	const serverID = "server-1"

	_, err := store.Select(sessionID, expiresAt, serverID)
	require.NoError(t, err)

	sub, err := store.Subscribe(sessionID, expiresAt)
	require.NoError(t, err)

	// Fill the channel via AdvanceServerGeneration. Each Advanced carries
	// the highest generation seen in this epoch.
	for i := 0; i < capacity; i++ {
		require.Equal(t, 1, store.AdvanceServerGeneration(serverID))
	}

	// Additional Advanced beyond capacity — coalesces to latest within the
	// same epoch; the receiver MAY observe a revision jump.
	require.Equal(t, 1, store.AdvanceServerGeneration(serverID))

	// Cleared while full — atomic removal + close. The receiver observes
	// closure and the subscriber count returns to 0.
	_, ok := store.Clear(sessionID)
	require.True(t, ok)

	// Deadlock-guarded drain: the at-capacity eviction contract MUST close
	// the channel; the agentteam's only legitimate exit is the closed-channel
	// branch. The 1s guard fails fast on contract violations.
	timeout := time.After(time.Second)
	for {
		select {
		case _, ok := <-sub.Changes:
			if !ok {
				require.Equal(t, 0, subscriberCountLocked(store, sessionID),
					"subscriber registry MUST be empty after at-capacity eviction")
				return
			}
		case <-timeout:
			t.Fatal("channel MUST be closed when Cleared cannot be delivered at capacity")
		}
	}
}

// TestSlowSubscriberNoCrossBoundaryCoalescing covers the second half of
// P4-BE-12: no Advanced is observed after a Selected/Cleared boundary in
// the same drain. Codex5 E3+P6: rev-8 incorrectly expected the channel to
// close after a successful Clear; a normal subscription does NOT close
// just because Clear succeeded. Rev-9 uses a counted drain (we know
// exactly how many events were enqueued) + explicit Release. No 1s
// timeout — the count terminates the drain deterministically.
func TestSlowSubscriberNoCrossBoundaryCoalescing(t *testing.T) {
	now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC)
	store := NewStore(Options{
		Now:                 func() time.Time { return now },
		SubscriptionChannel: 2,
	})
	expiresAt := now.Add(time.Hour)

	const sessionID = "session-no-cross-boundary"
	const serverID = "server-1"

	_, err := store.Select(sessionID, expiresAt, serverID)
	require.NoError(t, err)
	sub, err := store.Subscribe(sessionID, expiresAt)
	require.NoError(t, err)

	// One Advanced within epoch.
	require.Equal(t, 1, store.AdvanceServerGeneration(serverID))
	// Cleared ends the epoch.
	_, ok := store.Clear(sessionID)
	require.True(t, ok)

	// Counted drain: exactly two events were enqueued (Advanced + Cleared).
	// The channel does NOT close automatically — that only happens on
	// at-capacity eviction, which does not apply here.
	var sawCleared bool
	for drained := 0; drained < 2; drained++ {
		select {
		case change := <-sub.Changes:
			if change.Kind == ChangeKindCleared {
				sawCleared = true
				continue
			}
			require.False(t, sawCleared,
				"observed %v after Cleared boundary — cross-boundary coalescing forbidden",
				change.Kind)
		default:
			t.Fatal("drain underran — fewer than 2 events observed in non-full channel")
		}
	}
	require.True(t, sawCleared, "did not observe Cleared within the counted drain")

	// Explicit Release — the channel is still open; the subscriber is
	// retired via the registry's idempotent release path.
	if sub.Release != nil {
		sub.Release()
	}
}
