package token

import (
	"testing"
	"time"

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

func TestUISessionCreateScopesLeaseAndCapsExpiry(t *testing.T) {
	resetSessionsForTest(t)
	before := time.Now()
	issue, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	require.NotEmpty(t, issue.Token)
	require.Equal(t, "instance-a", issue.Metadata.InstanceID)
	require.Equal(t, "window-a", issue.Metadata.WindowID)
	require.NotEmpty(t, issue.Metadata.LogicalSessionID)
	require.Equal(t, uint64(1), issue.Metadata.Generation)
	require.Equal(t, UISessionDispositionCreated, issue.Disposition)
	require.WithinDuration(t, before.Add(uiSessionMaxDuration), issue.Metadata.ExpiresAt, time.Second)
	require.LessOrEqual(t, issue.Metadata.ExpiresAt.Sub(before), uiSessionMaxDuration+time.Second)
	require.True(t, ValidateSession(issue.Token))
}

func TestUISessionRenewKeepsReplacedBearerValidUntilItsOriginalExpiry(t *testing.T) {
	resetSessionsForTest(t)
	first, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	second, err := RenewUISession(first.Token, "instance-a", "window-a")
	require.NoError(t, err)
	require.NotEqual(t, first.Token, second.Token)
	require.Equal(t, uint64(2), second.Metadata.Generation)
	require.Equal(t, first.Metadata.LogicalSessionID, second.Metadata.LogicalSessionID)
	require.Equal(t, UISessionDispositionRotated, second.Disposition)
	require.True(t, ValidateSession(first.Token))
	require.True(t, ValidateSession(second.Token))
	firstSession, ok := SessionInfo(first.Token)
	require.True(t, ok)
	require.Equal(t, second.Metadata.ExpiresAt, firstSession.ExpiresAt)

	_, err = RenewUISession(first.Token, "instance-a", "window-a")
	require.ErrorIs(t, err, ErrUISessionInvalid)
	_, err = RenewUISession(second.Token, "instance-a", "window-b")
	require.ErrorIs(t, err, ErrUISessionMismatch)

	sessionMu.Lock()
	expired := sessionKeys[first.Token]
	expired.ExpiresAt = time.Now().Add(-time.Second)
	sessionKeys[first.Token] = expired
	sessionMu.Unlock()
	require.False(t, ValidateSession(first.Token))
	require.True(t, ValidateSession(second.Token))
}

func TestUISessionEnsureIsIdempotentAndReissuesExpiredLease(t *testing.T) {
	resetSessionsForTest(t)
	first, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	require.Equal(t, UISessionDispositionCreated, first.Disposition)

	unchanged, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	require.Equal(t, first.Token, unchanged.Token)
	require.Equal(t, UISessionDispositionUnchanged, unchanged.Disposition)

	sessionMu.Lock()
	expired := sessionKeys[first.Token]
	expired.ExpiresAt = time.Now().Add(-time.Minute)
	sessionKeys[first.Token] = expired
	sessionMu.Unlock()

	reissued, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	require.NotEqual(t, first.Token, reissued.Token)
	require.Equal(t, first.Metadata.LogicalSessionID, reissued.Metadata.LogicalSessionID)
	require.Equal(t, first.Metadata.Generation+1, reissued.Metadata.Generation)
	require.Equal(t, UISessionDispositionReissued, reissued.Disposition)
	require.False(t, ValidateSession(first.Token))
	require.True(t, ValidateSession(reissued.Token))

	sessionMu.Lock()
	for token, record := range sessionKeys {
		record.ExpiresAt = time.Now().Add(-uiSessionRecoveryDuration - time.Minute)
		sessionKeys[token] = record
	}
	sessionMu.Unlock()

	recreated, err := EnsureUISession("instance-a", "window-a")
	require.NoError(t, err)
	require.NotEqual(t, reissued.Metadata.LogicalSessionID, recreated.Metadata.LogicalSessionID)
	require.Equal(t, uint64(1), recreated.Metadata.Generation)
	require.Equal(t, UISessionDispositionCreated, recreated.Disposition)
}

func TestUISessionWindowAndInstanceRevoke(t *testing.T) {
	resetSessionsForTest(t)
	windowA1, _ := EnsureUISession("instance-a", "window-a")
	windowA2, _ := RenewUISession(windowA1.Token, "instance-a", "window-a")
	windowB, _ := EnsureUISession("instance-a", "window-b")
	instanceB, _ := EnsureUISession("instance-b", "window-a")

	revoked := RevokeUISessionWindow("instance-a", "window-a")
	require.Len(t, revoked, 2)
	require.False(t, ValidateSession(windowA1.Token))
	require.False(t, ValidateSession(windowA2.Token))
	require.True(t, ValidateSession(windowB.Token))
	require.True(t, ValidateSession(instanceB.Token))

	revoked = RevokeUISessionInstance("instance-a")
	require.Len(t, revoked, 1)
	require.False(t, ValidateSession(windowB.Token))
	require.True(t, ValidateSession(instanceB.Token))
}

func resetSessionsForTest(t *testing.T) {
	t.Helper()
	sessionMu.Lock()
	previous := sessionKeys
	sessionKeys = make(map[string]sessionRecord)
	sessionMu.Unlock()
	t.Cleanup(func() {
		sessionMu.Lock()
		sessionKeys = previous
		sessionMu.Unlock()
	})
}
