package uisession

import (
	"testing"
	"time"

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

// TestLeaseExpiryAndPruneDeterminism covers P4-BE-19: advancing the fake
// clock is NOT sufficient — tests MUST also call Prune(). The same
// sessionID cannot be revived; only a BRAND-NEW sessionID never seen by
// the Store returns SubscriptionStateEmpty, rev=0, gen=0.
func TestLeaseExpiryAndPruneDeterminism(t *testing.T) {
	now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC)
	store := NewStore(Options{Now: func() time.Time { return now }})

	const sessionID = "session-lease"

	// (a) Inline expired lease → ErrSessionExpired.
	_, err := store.Subscribe(sessionID, now.Add(-1*time.Second))
	require.ErrorIs(t, err, ErrSessionExpired)

	// (b) Live session expires naturally; Prune closes the channel. No
	// tombstone is recorded because the session expired without Revoke.
	expiresAt := now.Add(time.Hour)
	sub, err := store.Subscribe(sessionID, expiresAt)
	require.NoError(t, err)

	now = now.Add(time.Hour).Add(time.Second)
	store.Prune()

	select {
	case _, ok := <-sub.Changes:
		require.False(t, ok, "Prune MUST close Changes after lease expiry")
	default:
		t.Fatal("Prune did not close Changes — advancing the clock alone is insufficient")
	}

	require.False(t, hasTombstoneEntry(store, sessionID), "natural expiry MUST NOT record a tombstone")

	// Same sessionID + same expired lease → ErrSessionExpired (linearization).
	_, err = store.Subscribe(sessionID, expiresAt)
	require.ErrorIs(t, err, ErrSessionExpired)

	// (c) Tombstone path: Revoke a different live session, advance clock,
	// call Prune, observe the tombstone entry was dropped.
	const revokedID = "session-revoke-prune"
	revokeExpires := now.Add(time.Hour)
	_, err = store.Select(revokedID, revokeExpires, "server-1")
	require.NoError(t, err)
	store.Revoke(revokedID, revokeExpires)
	require.True(t, hasTombstoneEntry(store, revokedID))

	now = revokeExpires.Add(time.Second)
	store.Prune()
	require.False(t, hasTombstoneEntry(store, revokedID), "Prune MUST drop the expired tombstone")

	// The OLD sessionID cannot be revived — Subscribe on it with the
	// original expired lease returns ErrSessionExpired (entry deleted;
	// linearization check).
	_, err = store.Subscribe(revokedID, revokeExpires)
	require.ErrorIs(t, err, ErrSessionExpired)

	// Only a BRAND-NEW sessionID + valid future lease returns Empty.
	const brandNewID = "session-brand-new-lease"
	fresh, err := store.Subscribe(brandNewID, now.Add(time.Hour))
	require.NoError(t, err)
	require.Equal(t, SubscriptionStateEmpty, fresh.Snapshot.State)
	require.Equal(t, uint64(0), fresh.Snapshot.Revision)
	require.Equal(t, uint64(0), fresh.Snapshot.Generation)
	if fresh.Release != nil {
		fresh.Release()
	}
}
