package uisession

import (
	"testing"
	"time"

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

// TestStrictlyMonotonicSemantics covers P4-BE-17: each successful mutation
// increments rev/gen; snapshot/reconnect observations may equal but never
// regress; failed mutations do not change counters; a brand-new sessionID
// starts at SubscriptionStateEmpty, rev=0, gen=0.
func TestStrictlyMonotonicSemantics(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-monotonic"

	// (a) Select(S1) → gen=1, rev=1.
	_, err := store.Select(sessionID, expiresAt, "server-1")
	require.NoError(t, err)
	assertMonotonicState(t, store, sessionID, "server-1", 1)

	// (b) Select(S2) on same session → gen=2, rev=2.
	_, err = store.Select(sessionID, expiresAt, "server-2")
	require.NoError(t, err)
	assertMonotonicState(t, store, sessionID, "server-2", 2)

	// (c) Clear → gen=3, rev=3.
	_, ok := store.Clear(sessionID)
	require.True(t, ok)

	// (d) Clear again → gen=4, rev=4.
	_, ok = store.Clear(sessionID)
	require.True(t, ok)

	// (e) stale ClearIfGeneration(gen=2) → returns false, NO counter change.
	require.False(t, store.ClearIfGeneration(sessionID, 2))

	// (f) AdvanceServerGeneration(server-2) while session is cleared —
	// no-op because ActiveServerID is "".
	require.Equal(t, 0, store.AdvanceServerGeneration("server-2"))

	// Re-select on server-2 so Advance applies.
	_, err = store.Select(sessionID, expiresAt, "server-2")
	require.NoError(t, err)
	require.Equal(t, 1, store.AdvanceServerGeneration("server-2"))

	// (g) snapshot/reconnect via release + Subscribe again — must match
	// authoritative values, never regress.
	sub, err := store.Subscribe(sessionID, expiresAt)
	require.NoError(t, err)
	require.Equal(t, "server-2", sub.Snapshot.ActiveServerID)
	require.GreaterOrEqual(t, sub.Snapshot.Generation, uint64(5))
	require.Equal(t, sub.Snapshot.Generation, sub.Snapshot.Revision)
	sub.Release()

	// (h) Revoke — tombstone recorded; subsequent Select/Subscribe return
	// ErrSessionClosed; counters do not increment.
	store.Revoke(sessionID, expiresAt)
	_, err = store.Select(sessionID, expiresAt, "server-1")
	require.ErrorIs(t, err, ErrSessionClosed)
	_, err = store.Subscribe(sessionID, expiresAt)
	require.ErrorIs(t, err, ErrSessionClosed)

	// (i) brand-new sessionID never seen: Subscribe returns Empty, rev=0,
	// gen=0.
	const brandNewID = "session-brand-new"
	freshSub, err := store.Subscribe(brandNewID, expiresAt)
	require.NoError(t, err)
	require.Equal(t, SubscriptionStateEmpty, freshSub.Snapshot.State)
	require.Equal(t, uint64(0), freshSub.Snapshot.Revision)
	require.Equal(t, uint64(0), freshSub.Snapshot.Generation)
	if freshSub.Release != nil {
		freshSub.Release()
	}
}

func assertMonotonicState(t *testing.T, store *Store, sessionID, serverID string, generation uint64) {
	t.Helper()
	snapshot, ok := store.Get(sessionID)
	require.True(t, ok)
	require.Equal(t, serverID, snapshot.ActiveServerID)
	require.Equal(t, generation, snapshot.Generation)
}
