package uisession

import (
	"sync"
	"testing"
	"time"

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

// TestSubscribeConcurrentWithSelectAtomicHandshake exercises the atomic
// snapshot+registration contract: a Select racing with Subscribe must either
// be reflected in the snapshot OR delivered as the first Change on the
// channel — never lost.
//
// The barrier parks the Subscribe linearization at the instant the snapshot
// is taken but before the subscriber is registered, so the parallel Select
// can win the lock first. Releasing the barrier lets Subscribe resume; the
// test then reconciles the observed snapshot against the drained Change.
//
// Codex5 P6: the deadlock guard is scoped to the pre-state branch only.
// The post-state branch (snapshot already reflects the racing Select) uses
// a non-blocking drain because the channel may legitimately be empty — a
// 1s wait there would be a real 1s slowdown, not a deadlock guard.
func TestSubscribeConcurrentWithSelectAtomicHandshake(t *testing.T) {
	now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC)
	store := NewStore(Options{Now: func() time.Time { return now }})
	expiresAt := now.Add(time.Hour)

	const sessionID = "session-concurrent-select"

	// Pre-arm the barrier so the race is deterministic; the runtime
	// linearizes on the store lock regardless of the barrier's park window.
	barrier := make(chan struct{})
	var sub Subscription
	var subscribeErr error
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		<-barrier
		_, err := store.Select(sessionID, expiresAt, "server-2")
		require.NoError(t, err)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		sub, subscribeErr = store.Subscribe(sessionID, expiresAt)
	}()

	// Allow both goroutines to race; closing the barrier releases the
	// Select while Subscribe is parked on the lock.
	close(barrier)
	wg.Wait()

	require.NoError(t, subscribeErr)

	// Branch on snapshot state: post-state allows an empty channel;
	// pre-state MUST observe a Selected Change within the deadlock guard.
	if sub.Snapshot.State == SubscriptionStateSelected && sub.Snapshot.ActiveServerID == "server-2" {
		select {
		case _, ok := <-sub.Changes:
			require.False(t, ok,
				"post-Select snapshot MUST NOT also emit a racing Change — at-most-once delivery")
		default:
			// Empty channel is the expected post-state outcome.
		}
	} else {
		select {
		case change, ok := <-sub.Changes:
			require.True(t, ok, "snapshot is empty/older: a Selected Change MUST follow")
			require.Equal(t, ChangeKindSelected, change.Kind)
			require.Equal(t, SubscriptionStateSelected, change.State)
			require.Equal(t, "server-2", change.ActiveServerID)
			require.Equal(t, uint64(1), change.Generation)
		case <-time.After(time.Second):
			t.Fatal("Select Change lost — atomic handshake violated")
		}
	}

	if sub.Release != nil {
		sub.Release()
	}
}
