/**
 * Tests for the auth-cache shape + state transitions used by the
 * webServer's /auth/callback, /api/auth/status, and /api/auth/resolve
 * routes.
 *
 * These lock the marketplace-auth bug we fixed in 2026-05: a user who
 * missed the OAuth WS broadcast (page reload, dropped connection)
 * would recover their user account via the polling fallback but lose
 * the JWT, leading to 401 "Authentication required" on every
 * marketplace publish/install/rate call.
 */
import { describe, test, expect } from '@jest/globals';
import {
  recordOAuthResult,
  recordResolvedUser,
  projectAuthStatus,
} from '../authCache.js';

// ─── recordOAuthResult ────────────────────────────────────────────────

describe('recordOAuthResult', () => {
  test('captures user, jwt, hasApiKey + a timestamp on the cache row', () => {
    const row = recordOAuthResult({
      user: { id: 'u1', email: 'a@b.com' },
      jwt: 'eyJhbG...real-jwt',
      hasApiKey: true,
    });
    expect(row.user).toEqual({ id: 'u1', email: 'a@b.com' });
    expect(row.jwt).toBe('eyJhbG...real-jwt');
    expect(row.hasApiKey).toBe(true);
    expect(typeof row.timestamp).toBe('string');
    // Must be a valid ISO datetime
    expect(new Date(row.timestamp).toString()).not.toBe('Invalid Date');
  });

  test('CRITICAL: jwt is on the cache row — this is the contract that prevents 401-on-marketplace', () => {
    // The bug we're guarding against: previously this function
    // implicitly did `{user, hasApiKey, timestamp}` and forgot the JWT.
    // Polling fallback then returned no JWT and marketplace auth broke.
    const row = recordOAuthResult({ user: { id: 'u' }, jwt: 'tok', hasApiKey: true });
    expect(row).toHaveProperty('jwt', 'tok');
  });

  test('non-string jwt collapses to null (defensive against malformed input)', () => {
    expect(recordOAuthResult({ user: {}, jwt: null, hasApiKey: false }).jwt).toBeNull();
    expect(recordOAuthResult({ user: {}, jwt: undefined, hasApiKey: false }).jwt).toBeNull();
    expect(recordOAuthResult({ user: {}, jwt: '', hasApiKey: false }).jwt).toBeNull();
    expect(recordOAuthResult({ user: {}, jwt: 12345, hasApiKey: false }).jwt).toBeNull();
    expect(recordOAuthResult({ user: {}, jwt: { token: 'x' }, hasApiKey: false }).jwt).toBeNull();
  });

  test('hasApiKey coerces to bool', () => {
    expect(recordOAuthResult({ hasApiKey: 1 }).hasApiKey).toBe(true);
    expect(recordOAuthResult({ hasApiKey: 0 }).hasApiKey).toBe(false);
    expect(recordOAuthResult({ hasApiKey: 'yes' }).hasApiKey).toBe(true);
    expect(recordOAuthResult({}).hasApiKey).toBe(false);
  });

  test('null user passes through as null (degraded but valid cache row)', () => {
    expect(recordOAuthResult({ user: null, jwt: 'x', hasApiKey: true }).user).toBeNull();
  });

  test('explicit timestamp wins (useful for deterministic tests)', () => {
    const row = recordOAuthResult({ user: {}, timestamp: '2026-05-01T00:00:00Z' });
    expect(row.timestamp).toBe('2026-05-01T00:00:00Z');
  });
});

// ─── recordResolvedUser ───────────────────────────────────────────────

describe('recordResolvedUser', () => {
  test('CRITICAL: preserves a JWT from a previous OAuth cache row', () => {
    // Resolve happens AFTER /auth/callback when the FE loads with an
    // existing API key. Earlier code overwrote lastAuthResult and
    // silently wiped the OAuth-minted JWT, breaking marketplace auth.
    const prev = recordOAuthResult({ user: { id: 'u1' }, jwt: 'oauth-token', hasApiKey: true });
    const next = recordResolvedUser({ user: { id: 'u1', email: 'a@b' }, prevCache: prev });
    expect(next.jwt).toBe('oauth-token');
    expect(next.user).toEqual({ id: 'u1', email: 'a@b' });
  });

  test('hasApiKey is always true on this path (resolve requires an API key)', () => {
    expect(recordResolvedUser({ user: { id: 'u' }, prevCache: null }).hasApiKey).toBe(true);
  });

  test('no prior JWT → cache row has jwt: null (no marketplace auth available)', () => {
    const next = recordResolvedUser({ user: { id: 'u' }, prevCache: null });
    expect(next.jwt).toBeNull();
  });

  test('prior cache without jwt → null (matches the no-prior case)', () => {
    const prev = { user: { id: 'u' }, hasApiKey: true, timestamp: '2026-01-01T00:00:00Z' };
    const next = recordResolvedUser({ user: { id: 'u' }, prevCache: prev });
    expect(next.jwt).toBeNull();
  });

  test('prior cache with empty/non-string jwt is treated as no jwt', () => {
    for (const badJwt of [null, undefined, '', 0, {}, []]) {
      const prev = { user: {}, hasApiKey: true, jwt: badJwt, timestamp: '2026-01-01T00:00:00Z' };
      expect(recordResolvedUser({ user: {}, prevCache: prev }).jwt).toBeNull();
    }
  });

  test('explicit timestamp wins (deterministic tests)', () => {
    const next = recordResolvedUser({
      user: { id: 'u' },
      prevCache: null,
      timestamp: '2026-05-03T12:00:00Z',
    });
    expect(next.timestamp).toBe('2026-05-03T12:00:00Z');
  });
});

// ─── projectAuthStatus ────────────────────────────────────────────────

describe('projectAuthStatus', () => {
  test('returns null user + null jwt when no cache exists yet', () => {
    const r = projectAuthStatus(null);
    expect(r.user).toBeNull();
    expect(r.jwt).toBeNull();
    expect(r.success).toBe(true);
  });

  test('returns the cached user + jwt when fresh', () => {
    const cache = recordOAuthResult({
      user: { id: 'u1' }, jwt: 'tok', hasApiKey: true,
    });
    const r = projectAuthStatus(cache);
    expect(r.user).toEqual({ id: 'u1' });
    expect(r.jwt).toBe('tok');
    expect(r.authenticated).toBe(true);
  });

  test('CRITICAL: jwt round-trips from recordOAuthResult through projectAuthStatus', () => {
    // The full bug-fix contract: callback → cache → status → FE can read JWT.
    const cache = recordOAuthResult({
      user: { id: 'u' }, jwt: 'the-jwt-value', hasApiKey: true,
    });
    const status = projectAuthStatus(cache);
    expect(status.jwt).toBe('the-jwt-value');
  });

  test('staleness gate: cache predating ?since= reports user=null (and jwt=null)', () => {
    const cache = recordOAuthResult({
      user: { id: 'u' }, jwt: 'old-tok', hasApiKey: true,
      timestamp: '2026-05-01T10:00:00Z',
    });
    // Caller asking about an auth event AFTER the cache was written.
    const r = projectAuthStatus(cache, { since: '2026-05-01T11:00:00Z' });
    expect(r.user).toBeNull();
    expect(r.jwt).toBeNull();
  });

  test('staleness gate: cache POST-DATING ?since= passes through', () => {
    const cache = recordOAuthResult({
      user: { id: 'u' }, jwt: 'fresh-tok', hasApiKey: true,
      timestamp: '2026-05-01T11:00:00Z',
    });
    const r = projectAuthStatus(cache, { since: '2026-05-01T10:00:00Z' });
    expect(r.user).toEqual({ id: 'u' });
    expect(r.jwt).toBe('fresh-tok');
  });

  test('staleness gate: equal timestamps → stale (strict <=)', () => {
    const cache = recordOAuthResult({
      user: { id: 'u' }, jwt: 'tok', hasApiKey: true,
      timestamp: '2026-05-01T10:00:00Z',
    });
    const r = projectAuthStatus(cache, { since: '2026-05-01T10:00:00Z' });
    expect(r.user).toBeNull();
  });

  test('PAIR INVARIANT: when user is null, jwt MUST be null too (never user=null with jwt=set)', () => {
    // Cache exists but is stale → user=null. JWT must follow.
    const cache = recordOAuthResult({
      user: { id: 'u' }, jwt: 'tok',
      timestamp: '2026-05-01T00:00:00Z',
    });
    const r = projectAuthStatus(cache, { since: '2026-05-02T00:00:00Z' });
    expect(r.user).toBeNull();
    expect(r.jwt).toBeNull();
  });

  test('user without jwt (resolve-via-api-key path) returns user but jwt=null', () => {
    // /auth/resolve sets user but no JWT. Marketplace will 401 — correct
    // behavior, not this fix's job to mint a JWT here.
    const cache = recordResolvedUser({ user: { id: 'u' }, prevCache: null });
    const r = projectAuthStatus(cache);
    expect(r.user).toEqual({ id: 'u' });
    expect(r.jwt).toBeNull();
  });

  test('authenticated reflects the live hasApiKey override (not the cache value)', () => {
    // The route passes the apiKeyManager's CURRENT view; cache might be stale.
    const cache = recordOAuthResult({ user: { id: 'u' }, jwt: 'x', hasApiKey: true });
    const r = projectAuthStatus(cache, { hasApiKey: false });
    expect(r.authenticated).toBe(false);
  });

  test('falls back to cache.hasApiKey when no override given', () => {
    const cache = recordOAuthResult({ user: { id: 'u' }, jwt: 'x', hasApiKey: true });
    const r = projectAuthStatus(cache);
    expect(r.authenticated).toBe(true);
  });
});

// ─── End-to-end scenario coverage (the actual bug story) ──────────────

describe('full scenario: OAuth → resolve → polling fallback (the bug we fixed)', () => {
  test('happy path: OAuth callback → poll status → JWT returned', () => {
    // 1. User signs in via OAuth. /auth/callback runs:
    let cache = recordOAuthResult({
      user: { id: 'u1', email: 'alice@example.com' },
      jwt: 'oauth-issued-jwt',
      hasApiKey: true,
    });
    // 2. WebSocket broadcast misses (page reload). FE polls /api/auth/status:
    const status = projectAuthStatus(cache, { hasApiKey: true });
    // 3. Polling response now contains jwt → FE writes to localStorage → marketplace auth works
    expect(status.user.email).toBe('alice@example.com');
    expect(status.jwt).toBe('oauth-issued-jwt');
  });

  test('OAuth then resolve: JWT survives the resolve-overwrite', () => {
    // 1. OAuth login caches everything including the JWT.
    let cache = recordOAuthResult({
      user: { id: 'u1' }, jwt: 'oauth-jwt', hasApiKey: true,
    });
    // 2. Frontend later hits /auth/resolve (e.g. on a fresh page load
    //    where API key is already set). Without the fix this would have
    //    overwritten cache.jwt = undefined.
    cache = recordResolvedUser({
      user: { id: 'u1', email: 'alice@example.com' },
      prevCache: cache,
    });
    // 3. Subsequent /api/auth/status caller still gets the JWT.
    const status = projectAuthStatus(cache);
    expect(status.jwt).toBe('oauth-jwt');
    expect(status.user.email).toBe('alice@example.com');
  });

  test('API-key-only flow: user resolves but no marketplace auth available', () => {
    // No prior OAuth — the user pasted an API key in Settings and the
    // /auth/resolve path linked their account from the backend.
    let cache = recordResolvedUser({ user: { id: 'u' }, prevCache: null });
    const status = projectAuthStatus(cache);
    expect(status.user).toEqual({ id: 'u' });
    expect(status.jwt).toBeNull();
    // Marketplace will 401 with "Authentication required" — the fix
    // for that case is server-to-server JWT minting in /auth/resolve,
    // out of scope for this PR.
  });

  test('polling for a NEW login but cache is from an OLDER one: not the auth we\'re waiting for', () => {
    // A previous user signed out, a new user is mid-login, FE polls.
    const oldCache = recordOAuthResult({
      user: { id: 'old-user' }, jwt: 'old-jwt', hasApiKey: true,
      timestamp: '2026-01-01T00:00:00Z',
    });
    // FE called /api/auth/status?since=<just-now>
    const status = projectAuthStatus(oldCache, { since: '2026-05-03T12:00:00Z' });
    // user=null + jwt=null so the FE keeps polling, doesn't sign in as the wrong user.
    expect(status.user).toBeNull();
    expect(status.jwt).toBeNull();
  });
});
