package uisession

import (
	"testing"
	"time"

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

// subscriberCountLocked is the in-package _test.go seam used by P4-BE-06 and
// P4-BE-12 to observe the Store's internal subscriber count without exporting
// it to production. The helper intentionally does NOT exist on the main head
// because Subscribe/Revoke and the underlying subscriber registry have not
// landed yet — tests referencing it fail-red on the missing seam, not on
// syntax or a fabricated export.
//
// The zero value is returned when the session has no live entry or no live
// subscribers; production code MUST NOT call this helper.
func subscriberCountLocked(s *Store, sessionID string) int {
	if s == nil {
		return 0
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	subs := s.subscribers[sessionID]
	if subs == nil {
		return 0
	}
	return len(subs.entries)
}

// tombstoneExpiresAtLocked is the in-package _test.go seam used by P4-BE-04b
// and P4-BE-19 to observe the recorded tombstone upper bound without
// exporting the tombstone map. Returns (zero, false) when the sessionID has
// no recorded tombstone.
func tombstoneExpiresAtLocked(s *Store, sessionID string) (time.Time, bool) {
	if s == nil {
		return time.Time{}, false
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	exp, ok := s.tombstones[sessionID]
	return exp, ok
}

// hasTombstoneEntry is the in-package _test.go seam used by P4-BE-19(c) to
// prove Prune dropped the tombstone entry (resource reclaimed) without
// exposing the map. Returns true when the tombstone still exists.
func hasTombstoneEntry(s *Store, sessionID string) bool {
	if s == nil {
		return false
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	_, ok := s.tombstones[sessionID]
	return ok
}

func TestSubscribeToNeverTouchedSessionReturnsEmptyState(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)

	sub, err := store.Subscribe("session-fresh", expiresAt)
	require.NoError(t, err)
	require.Equal(t, SubscriptionStateEmpty, sub.Snapshot.State)
	require.Equal(t, uint64(0), sub.Snapshot.Revision)
	require.Equal(t, uint64(0), sub.Snapshot.Generation)
	require.Empty(t, sub.Snapshot.ActiveServerID)
	require.NotNil(t, sub.Changes)

	select {
	case _, ok := <-sub.Changes:
		require.False(t, ok, "Changes must be a non-nil receive-only channel with no buffered value")
	default:
	}

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

func TestSubscribeAfterSelectReturnsSelectedStateAtGenerationOne(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)

	_, err := store.Select("session-selected", expiresAt, "server-1")
	require.NoError(t, err)

	sub, err := store.Subscribe("session-selected", expiresAt)
	require.NoError(t, err)
	require.Equal(t, SubscriptionStateSelected, sub.Snapshot.State)
	require.Equal(t, "server-1", sub.Snapshot.ActiveServerID)
	require.Equal(t, uint64(1), sub.Snapshot.Generation)
	require.Equal(t, uint64(1), sub.Snapshot.Revision)

	select {
	case _, ok := <-sub.Changes:
		require.False(t, ok, "Changes must initially be empty after a post-Select subscribe")
	default:
	}

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

func TestSubscribeAfterClearReturnsClearedStateAtGenerationTwo(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)

	_, err := store.Select("session-cleared", expiresAt, "server-1")
	require.NoError(t, err)
	_, ok := store.Clear("session-cleared")
	require.True(t, ok)

	sub, err := store.Subscribe("session-cleared", expiresAt)
	require.NoError(t, err)
	require.Equal(t, SubscriptionStateCleared, sub.Snapshot.State)
	require.Empty(t, sub.Snapshot.ActiveServerID)
	require.Equal(t, uint64(2), sub.Snapshot.Generation)
	require.Equal(t, uint64(2), sub.Snapshot.Revision)

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