package session_test

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

	"galaxy/gateway/internal/session"
)

// stubLookup is the BackendLookup test fake. lookups counts hits;
// records is the canonical source of truth keyed by device_session_id.
type stubLookup struct {
	mu      sync.Mutex
	records map[string]session.Record
	hits    atomic.Int64
	notFound bool
}

func newStubLookup() *stubLookup {
	return &stubLookup{records: make(map[string]session.Record)}
}

func (s *stubLookup) put(rec session.Record) {
	s.mu.Lock()
	s.records[rec.DeviceSessionID] = rec
	s.mu.Unlock()
}

func (s *stubLookup) LookupSession(_ context.Context, deviceSessionID string) (session.Record, error) {
	s.hits.Add(1)
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.notFound {
		return session.Record{}, session.ErrNotFound
	}
	rec, ok := s.records[deviceSessionID]
	if !ok {
		return session.Record{}, session.ErrNotFound
	}
	return rec, nil
}

func TestMemoryCacheLookupHitsCacheAfterFirstFetch(t *testing.T) {
	stub := newStubLookup()
	stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{
		MaxEntries: 10,
		TTL:        time.Hour,
	})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}

	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("second lookup: %v", err)
	}
	if got := stub.hits.Load(); got != 1 {
		t.Fatalf("backend hits = %d, want 1 (cache should serve the second call)", got)
	}
}

func TestMemoryCacheLookupRefreshesOnTTLExpiry(t *testing.T) {
	stub := newStubLookup()
	stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})

	clock := time.Unix(1_000_000, 0)
	now := func() time.Time { return clock }

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{
		MaxEntries: 10,
		TTL:        100 * time.Millisecond,
		Now:        now,
	})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}

	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	clock = clock.Add(200 * time.Millisecond)
	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("post-TTL lookup: %v", err)
	}
	if got := stub.hits.Load(); got != 2 {
		t.Fatalf("backend hits = %d, want 2 (TTL expiry should refetch)", got)
	}
}

func TestMemoryCacheMarkRevokedFlipsCachedRecord(t *testing.T) {
	stub := newStubLookup()
	stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 10, TTL: time.Hour})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}

	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	cache.MarkRevoked("a")
	rec, err := cache.Lookup(context.Background(), "a")
	if err != nil {
		t.Fatalf("post-revoke lookup: %v", err)
	}
	if rec.Status != session.StatusRevoked {
		t.Fatalf("status = %q, want %q", rec.Status, session.StatusRevoked)
	}
	if got := stub.hits.Load(); got != 1 {
		t.Fatalf("backend hits = %d, want 1 (MarkRevoked must not refetch)", got)
	}
}

func TestMemoryCacheMarkAllRevokedForUserFlipsAllSessions(t *testing.T) {
	stub := newStubLookup()
	stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
	stub.put(session.Record{DeviceSessionID: "b", UserID: "u1", Status: session.StatusActive})
	stub.put(session.Record{DeviceSessionID: "c", UserID: "u2", Status: session.StatusActive})

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 10, TTL: time.Hour})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}
	for _, id := range []string{"a", "b", "c"} {
		if _, err := cache.Lookup(context.Background(), id); err != nil {
			t.Fatalf("seed %s: %v", id, err)
		}
	}

	cache.MarkAllRevokedForUser("u1")

	for _, id := range []string{"a", "b"} {
		rec, err := cache.Lookup(context.Background(), id)
		if err != nil {
			t.Fatalf("post-revoke lookup %s: %v", id, err)
		}
		if rec.Status != session.StatusRevoked {
			t.Fatalf("session %s status = %q, want revoked", id, rec.Status)
		}
	}
	rec, err := cache.Lookup(context.Background(), "c")
	if err != nil {
		t.Fatalf("post-revoke lookup c: %v", err)
	}
	if rec.Status != session.StatusActive {
		t.Fatalf("session c status = %q, want active (other user)", rec.Status)
	}
}

func TestMemoryCacheLRUEvictsLeastRecentlyUsed(t *testing.T) {
	stub := newStubLookup()
	stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
	stub.put(session.Record{DeviceSessionID: "b", UserID: "u2", Status: session.StatusActive})
	stub.put(session.Record{DeviceSessionID: "c", UserID: "u3", Status: session.StatusActive})

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 2, TTL: time.Hour})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}

	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("seed a: %v", err)
	}
	if _, err := cache.Lookup(context.Background(), "b"); err != nil {
		t.Fatalf("seed b: %v", err)
	}
	if _, err := cache.Lookup(context.Background(), "c"); err != nil {
		t.Fatalf("seed c: %v", err)
	}
	if got := cache.Len(); got != 2 {
		t.Fatalf("Len = %d, want 2", got)
	}

	hitsBefore := stub.hits.Load()
	if _, err := cache.Lookup(context.Background(), "a"); err != nil {
		t.Fatalf("re-lookup a: %v", err)
	}
	if got := stub.hits.Load(); got != hitsBefore+1 {
		t.Fatalf("backend hits = %d, want +1 (a was evicted)", got-hitsBefore)
	}
}

func TestMemoryCachePropagatesBackendNotFound(t *testing.T) {
	stub := newStubLookup()
	stub.notFound = true

	cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 4, TTL: time.Hour})
	if err != nil {
		t.Fatalf("NewMemoryCache: %v", err)
	}
	_, err = cache.Lookup(context.Background(), "missing")
	if !errors.Is(err, session.ErrNotFound) {
		t.Fatalf("Lookup error = %v, want ErrNotFound", err)
	}
}
