package uisession

import (
	"sync"
	"testing"
	"time"

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

// TestSubscribeLinearizationVsRevokePreRevoke covers case (a) of P4-BE-18:
// Revoke BEFORE Subscribe — Subscribe returns (zero, ErrSessionClosed)
// and the returned Subscription.Changes is nil/zero-value. Codex5 E2:
// the rev-8 test over-asserted StateEmpty/Revision==0 on the zero-value
// Subscription; rev-9 only asserts the typed error + nil Changes channel,
// which is the actual closed-session contract.
func TestSubscribeLinearizationVsRevokePreRevoke(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-pre-revoke"
	store.Revoke(sessionID, expiresAt)

	sub, err := store.Subscribe(sessionID, expiresAt)
	require.ErrorIs(t, err, ErrSessionClosed)
	require.Nil(t, sub.Changes, "Changes MUST be nil on ErrSessionClosed")
	if sub.Release != nil {
		sub.Release()
	}
}

// TestSubscribeLinearizationVsRevokePreSubscribe covers case (b): Subscribe
// wins, then Revoke closes the Changes channel — no Change value observed.
func TestSubscribeLinearizationVsRevokePreSubscribe(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-pre-subscribe"
	sub, err := store.Subscribe(sessionID, expiresAt)
	require.NoError(t, err)

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		store.Revoke(sessionID, expiresAt)
	}()

	select {
	case _, ok := <-sub.Changes:
		require.False(t, ok, "Revoke MUST close the Changes channel without delivering a Change value")
	case <-time.After(time.Second):
		t.Fatal("Revoke did not close the Changes channel — deadlock")
	}
	wg.Wait()
}

// TestSubscribeLinearizationVsRevokeSymmetric covers case (c): both called
// concurrently with a barrier controlling lock acquisition order. Each
// ordering produces its corresponding outcome; no ordering emits a
// first-frame empty snapshot for a closed session.
func TestSubscribeLinearizationVsRevokeSymmetric(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-symmetric"

	var sub Subscription
	var subscribeErr error
	barrier := make(chan struct{})
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		defer wg.Done()
		<-barrier
		store.Revoke(sessionID, expiresAt)
	}()
	go func() {
		defer wg.Done()
		<-barrier
		sub, subscribeErr = store.Subscribe(sessionID, expiresAt)
	}()

	close(barrier)
	wg.Wait()

	// Either Subscribe won (sub.Changes eventually closes via the racing
	// Revoke) or Revoke won (subscribeErr == ErrSessionClosed and sub.Changes
	// is nil). Both outcomes are valid; the forbidden outcome is a first-frame
	// empty snapshot for a closed session.
	if subscribeErr != nil {
		require.ErrorIs(t, subscribeErr, ErrSessionClosed)
		require.Nil(t, sub.Changes)
		return
	}

	select {
	case _, ok := <-sub.Changes:
		require.False(t, ok, "Revoke-wins path must close Changes without a value")
	case <-time.After(time.Second):
		t.Fatal("Revoke did not close Changes in the Subscribe-wins path — deadlock")
	}
}
