package auth

import (
	"context"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/google/uuid"
)

func TestCacheGetAddRemove(t *testing.T) {
	c := NewCache()
	if c.Ready() {
		t.Fatalf("fresh cache should not be Ready before Warm")
	}
	if c.Size() != 0 {
		t.Fatalf("fresh cache size = %d, want 0", c.Size())
	}

	id := uuid.New()
	uid := uuid.New()
	s := Session{DeviceSessionID: id, UserID: uid, Status: SessionStatusActive}
	c.Add(s)
	if c.Size() != 1 {
		t.Fatalf("size after Add = %d, want 1", c.Size())
	}
	got, ok := c.Get(id)
	if !ok || got.DeviceSessionID != id {
		t.Fatalf("Get after Add: ok=%v session=%+v", ok, got)
	}

	c.Remove(id)
	if c.Size() != 0 {
		t.Fatalf("size after Remove = %d, want 0", c.Size())
	}
	if _, ok := c.Get(id); ok {
		t.Fatalf("Get after Remove returned a hit")
	}

	// Remove on already-evicted entry is a no-op.
	c.Remove(id)
}

func TestCacheRemoveByUser(t *testing.T) {
	c := NewCache()
	uid := uuid.New()
	other := uuid.New()
	c.Add(Session{DeviceSessionID: uuid.New(), UserID: uid, Status: SessionStatusActive})
	c.Add(Session{DeviceSessionID: uuid.New(), UserID: uid, Status: SessionStatusActive})
	c.Add(Session{DeviceSessionID: uuid.New(), UserID: other, Status: SessionStatusActive})

	removed := c.RemoveByUser(uid)
	if len(removed) != 2 {
		t.Fatalf("RemoveByUser removed %d, want 2", len(removed))
	}
	if c.Size() != 1 {
		t.Fatalf("size after RemoveByUser = %d, want 1", c.Size())
	}
	if got := c.RemoveByUser(uid); got != nil {
		t.Fatalf("RemoveByUser on empty user returned %v, want nil", got)
	}
}

func TestCacheWarmFlipsReady(t *testing.T) {
	// Constructing a Cache and calling Warm against a Store without a real
	// database is awkward — the e2e test exercises Warm against Postgres.
	// Here we manually populate to confirm Ready toggles.
	c := NewCache()
	if c.Ready() {
		t.Fatalf("Ready before Warm")
	}
	// Simulate a successful Warm by setting ready and inserting via Add.
	c.ready.Store(true)
	if !c.Ready() {
		t.Fatalf("Ready did not flip after store")
	}
}

func TestCacheConcurrentGetAddRemove(t *testing.T) {
	c := NewCache()

	const writers = 4
	const readers = 4
	const opsPerWorker = 1000

	uid := uuid.New()
	ids := make([]uuid.UUID, opsPerWorker)
	for i := range ids {
		ids[i] = uuid.New()
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	var stop atomic.Bool
	var wg sync.WaitGroup

	for range writers {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := range opsPerWorker {
				if stop.Load() {
					return
				}
				c.Add(Session{DeviceSessionID: ids[i], UserID: uid, Status: SessionStatusActive})
				c.Remove(ids[i])
			}
		}()
	}

	for range readers {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := range opsPerWorker {
				if stop.Load() {
					return
				}
				_, _ = c.Get(ids[i%len(ids)])
			}
		}()
	}

	done := make(chan struct{})
	go func() { wg.Wait(); close(done) }()
	select {
	case <-done:
	case <-ctx.Done():
		stop.Store(true)
		<-done
		t.Fatalf("cache concurrency test timed out")
	}

	// After all goroutines finish, the cache must be empty (every Add
	// is paired with a Remove).
	if c.Size() != 0 {
		t.Fatalf("cache size after concurrent run = %d, want 0", c.Size())
	}
}
