/**
 * Tests for in-app OAuth auth routes from webServer.js:
 *   - GET  /auth/callback        — portal redirect lands here with token+user
 *   - GET  /api/auth/status      — current auth state (enhanced: returns user, supports ?since=)
 *   - GET  /api/auth/resolve     — resolve user from API key via remote backend /auth/me
 *   - POST /api/auth/signout     — clear stored credentials + cached auth
 *   - GET  /api/health/cloud     — Azure backend health check
 *
 * Uses a real HTTP server + fetch (same harness pattern as imageServing.test.js)
 * so we don't have to construct the entire WebServer object tree.
 */

import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import express from 'express';
import { createServer } from 'http';

/**
 * Build a minimal Express app replicating the auth route handlers from webServer.js.
 * `ctx` is a mutable object that holds the lastAuthResult state (simulating `this`).
 */
function buildAuthApp({ apiKeyManager, broadcastToSession, config, logger, fetchImpl, ctx }) {
  const app = express();
  app.use(express.json());

  // ── /auth/callback ─────────────────────────────────────────────
  app.get('/auth/callback', async (req, res) => {
    try {
      const { token, user: userB64 } = req.query;
      if (!token) {
        return res.status(400).send('<html><body><h2>Authentication failed</h2><p>No token received.</p></body></html>');
      }

      let userInfo = {};
      try {
        userInfo = JSON.parse(Buffer.from(decodeURIComponent(userB64), 'base64').toString('utf8'));
      } catch { /* user info is optional */ }

      const backendBaseUrl = config?.backend?.baseUrl || 'https://autopilot-api.azurewebsites.net';
      let apiKey = null;
      try {
        const keyResponse = await fetchImpl(`${backendBaseUrl}/auth/apikey`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
          },
          body: JSON.stringify({ name: `Loxia App (${new Date().toLocaleDateString()})` })
        });
        const keyData = await keyResponse.json();
        if (keyData.apiKey?.key) apiKey = keyData.apiKey.key;
      } catch (err) {
        logger?.warn?.('Failed to auto-generate API key from OAuth token', { error: err.message });
      }

      if (apiKey && apiKeyManager) {
        await apiKeyManager.setSessionKeys(null, { loxiaApiKey: apiKey });
      }

      ctx.lastAuthResult = {
        user: userInfo,
        hasApiKey: !!apiKey,
        timestamp: new Date().toISOString()
      };

      broadcastToSession(null, {
        type: 'auth_success',
        data: {
          user: userInfo,
          jwt: token,
          hasApiKey: !!apiKey,
          timestamp: new Date().toISOString()
        }
      });

      res.send('<html><body><h2>Authenticated!</h2></body></html>');
    } catch (error) {
      logger?.error?.('Auth callback failed', { error: error.message });
      res.status(500).send('<html><body><h2>Authentication failed</h2><p>' + error.message + '</p></body></html>');
    }
  });

  // ── /api/auth/status ───────────────────────────────────────────
  app.get('/api/auth/status', (req, res) => {
    const hasApiKey = !!(apiKeyManager?.getKeysForRequest?.(null, { platformProvided: true })?.loxiaApiKey);

    let user = ctx.lastAuthResult?.user || null;

    const since = req.query.since;
    if (since && ctx.lastAuthResult?.timestamp) {
      if (new Date(ctx.lastAuthResult.timestamp) <= new Date(since)) {
        user = null;
      }
    }

    res.json({
      success: true,
      authenticated: hasApiKey,
      user,
      timestamp: ctx.lastAuthResult?.timestamp || null
    });
  });

  // ── /api/auth/resolve ──────────────────────────────────────────
  app.get('/api/auth/resolve', async (req, res) => {
    try {
      const loxiaApiKey = apiKeyManager?.getKeysForRequest?.(null, { platformProvided: true })?.loxiaApiKey;
      if (!loxiaApiKey) {
        return res.json({ success: true, user: null, reason: 'no_api_key' });
      }

      const backendBaseUrl = config?.backend?.baseUrl || 'https://autopilot-api.azurewebsites.net';
      const response = await fetchImpl(`${backendBaseUrl}/auth/me`, {
        headers: { 'Authorization': `Bearer ${loxiaApiKey}` }
      });

      if (!response.ok) {
        return res.json({ success: true, user: null, reason: 'backend_error', status: response.status });
      }

      const data = await response.json();
      const user = data.user || data;

      ctx.lastAuthResult = {
        user,
        hasApiKey: true,
        timestamp: new Date().toISOString()
      };

      res.json({ success: true, user });
    } catch (error) {
      logger?.warn?.('Failed to resolve user from API key', { error: error.message });
      res.json({ success: true, user: null, reason: 'fetch_error', error: error.message });
    }
  });

  // ── /api/auth/signout ──────────────────────────────────────────
  app.post('/api/auth/signout', async (req, res) => {
    try {
      if (apiKeyManager) {
        await apiKeyManager.removeSessionKeys(null);
      }
      ctx.lastAuthResult = null;
      broadcastToSession(null, {
        type: 'auth_signout',
        data: { timestamp: new Date().toISOString() }
      });
      res.json({ success: true });
    } catch (error) {
      res.status(500).json({ success: false, error: error.message });
    }
  });

  // ── /api/health/cloud ──────────────────────────────────────────
  app.get('/api/health/cloud', async (req, res) => {
    try {
      const healthUrl = config?.backend?.healthUrl || 'https://autopilot-api.azurewebsites.net/health';
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      const response = await fetchImpl(healthUrl, {
        method: 'GET',
        signal: controller.signal
      });
      clearTimeout(timeout);
      if (!response.ok) {
        return res.json({ success: false, online: false, error: `Backend returned ${response.status}` });
      }
      const data = await response.json();
      res.json({ success: true, online: true, backend: data });
    } catch (error) {
      res.json({ success: false, online: false, error: error.message });
    }
  });

  return app;
}

function makeApiKeyManager(initialKey = null) {
  let storedKey = initialKey;
  return {
    setSessionKeys: jest.fn(async (sessionId, keys) => { storedKey = keys.loxiaApiKey; }),
    removeSessionKeys: jest.fn(async () => { storedKey = null; }),
    getKeysForRequest: jest.fn(() => storedKey ? { loxiaApiKey: storedKey } : {}),
    _peek: () => storedKey
  };
}

function encodeUser(obj) {
  return encodeURIComponent(Buffer.from(JSON.stringify(obj)).toString('base64'));
}

async function startServer(app) {
  const server = createServer(app);
  await new Promise(resolve => server.listen(0, resolve));
  const port = server.address().port;
  return { server, baseUrl: `http://127.0.0.1:${port}` };
}

describe('Auth Routes (webServer.js)', () => {
  let mockFetch;
  let broadcasts;
  let broadcastToSession;
  let logger;
  let server;
  let ctx;

  beforeEach(() => {
    mockFetch = jest.fn();
    broadcasts = [];
    broadcastToSession = jest.fn((sessionId, msg) => broadcasts.push({ sessionId, msg }));
    logger = { warn: jest.fn(), error: jest.fn(), info: jest.fn() };
    server = null;
    ctx = { lastAuthResult: null };
  });

  afterEach(() => {
    if (server) server.close();
  });

  // ── /auth/callback ───────────────────────────────────────────────
  describe('GET /auth/callback', () => {
    it('returns 400 HTML when token is missing', async () => {
      const apiKeyManager = makeApiKeyManager();
      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const res = await fetch(`${baseUrl}/auth/callback`);
      expect(res.status).toBe(400);
      const text = await res.text();
      expect(text).toContain('No token received');
      expect(mockFetch).not.toHaveBeenCalled();
      expect(broadcastToSession).not.toHaveBeenCalled();
    });

    it('decodes user info, generates API key, caches auth result, and broadcasts', async () => {
      const apiKeyManager = makeApiKeyManager();
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ apiKey: { key: 'lx_generated_key_123' } })
      });

      const app = buildAuthApp({
        apiKeyManager, broadcastToSession,
        config: { backend: { baseUrl: 'https://test-backend.example.com' } },
        logger, fetchImpl: mockFetch, ctx
      });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const userInfo = { id: 'user-1', email: 'test@example.com', firstName: 'Test' };
      const res = await fetch(`${baseUrl}/auth/callback?token=jwt-abc&user=${encodeUser(userInfo)}`);

      expect(res.status).toBe(200);
      expect(await res.text()).toContain('Authenticated!');

      // Verify API key was stored
      expect(apiKeyManager.setSessionKeys).toHaveBeenCalledWith(null, { loxiaApiKey: 'lx_generated_key_123' });

      // Verify broadcast
      const broadcast = broadcasts[0];
      expect(broadcast.msg.type).toBe('auth_success');
      expect(broadcast.msg.data.user).toEqual(userInfo);
      expect(broadcast.msg.data.jwt).toBe('jwt-abc');
      expect(broadcast.msg.data.hasApiKey).toBe(true);

      // Verify lastAuthResult was cached
      expect(ctx.lastAuthResult).toBeDefined();
      expect(ctx.lastAuthResult.user).toEqual(userInfo);
      expect(ctx.lastAuthResult.hasApiKey).toBe(true);
      expect(ctx.lastAuthResult.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
    });

    it('still caches and broadcasts when API key generation fails', async () => {
      const apiKeyManager = makeApiKeyManager();
      mockFetch.mockRejectedValueOnce(new Error('Backend down'));

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      await fetch(`${baseUrl}/auth/callback?token=jwt-x&user=${encodeUser({ id: 'u1' })}`);
      expect(ctx.lastAuthResult.hasApiKey).toBe(false);
      expect(ctx.lastAuthResult.user).toEqual({ id: 'u1' });
    });
  });

  // ── /api/auth/status (enhanced) ──────────────────────────────────
  describe('GET /api/auth/status', () => {
    it('returns authenticated=false and user=null when no key and no cached auth', async () => {
      const apiKeyManager = makeApiKeyManager(null);
      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/status`)).json();
      expect(body.success).toBe(true);
      expect(body.authenticated).toBe(false);
      expect(body.user).toBeNull();
      expect(body.timestamp).toBeNull();
    });

    it('returns cached user after a successful auth callback', async () => {
      const apiKeyManager = makeApiKeyManager('lx_k');
      ctx.lastAuthResult = { user: { email: 'cached@test.com' }, hasApiKey: true, timestamp: '2026-01-01T00:00:00.000Z' };

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/status`)).json();
      expect(body.authenticated).toBe(true);
      expect(body.user.email).toBe('cached@test.com');
      expect(body.timestamp).toBe('2026-01-01T00:00:00.000Z');
    });

    it('?since= returns user when auth happened AFTER the given timestamp', async () => {
      const apiKeyManager = makeApiKeyManager('lx_k');
      ctx.lastAuthResult = { user: { email: 'new@test.com' }, hasApiKey: true, timestamp: '2026-04-12T12:00:00.000Z' };

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      // Auth happened AFTER since → user should be returned
      const body = await (await fetch(`${baseUrl}/api/auth/status?since=2026-04-12T11:59:00.000Z`)).json();
      expect(body.user.email).toBe('new@test.com');
    });

    it('?since= returns user=null when auth happened BEFORE the given timestamp', async () => {
      const apiKeyManager = makeApiKeyManager('lx_k');
      ctx.lastAuthResult = { user: { email: 'old@test.com' }, hasApiKey: true, timestamp: '2026-04-12T10:00:00.000Z' };

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      // Auth happened BEFORE since → user should NOT be returned
      const body = await (await fetch(`${baseUrl}/api/auth/status?since=2026-04-12T11:00:00.000Z`)).json();
      expect(body.user).toBeNull();
      expect(body.authenticated).toBe(true); // Still has key
    });
  });

  // ── /api/auth/resolve ────────────────────────────────────────────
  describe('GET /api/auth/resolve', () => {
    it('returns user=null with reason when no API key stored', async () => {
      const apiKeyManager = makeApiKeyManager(null);
      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/resolve`)).json();
      expect(body.success).toBe(true);
      expect(body.user).toBeNull();
      expect(body.reason).toBe('no_api_key');
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it('calls backend /auth/me and returns + caches user when successful', async () => {
      const apiKeyManager = makeApiKeyManager('lx_my_key');
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ user: { id: 'u1', email: 'resolved@test.com', firstName: 'Resolved' } })
      });

      const app = buildAuthApp({
        apiKeyManager, broadcastToSession,
        config: { backend: { baseUrl: 'https://api.example.com' } },
        logger, fetchImpl: mockFetch, ctx
      });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/resolve`)).json();
      expect(body.success).toBe(true);
      expect(body.user.email).toBe('resolved@test.com');

      // Verify backend was called with the API key
      expect(mockFetch).toHaveBeenCalledTimes(1);
      expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/auth/me');
      expect(mockFetch.mock.calls[0][1].headers.Authorization).toBe('Bearer lx_my_key');

      // Verify cached
      expect(ctx.lastAuthResult.user.email).toBe('resolved@test.com');
      expect(ctx.lastAuthResult.hasApiKey).toBe(true);
    });

    it('returns user=null when backend returns non-OK (e.g. 401 invalid key)', async () => {
      const apiKeyManager = makeApiKeyManager('lx_bad_key');
      mockFetch.mockResolvedValueOnce({ ok: false, status: 401 });

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/resolve`)).json();
      expect(body.user).toBeNull();
      expect(body.reason).toBe('backend_error');
      expect(body.status).toBe(401);
    });

    it('returns user=null when fetch throws (network error)', async () => {
      const apiKeyManager = makeApiKeyManager('lx_key');
      mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/resolve`)).json();
      expect(body.user).toBeNull();
      expect(body.reason).toBe('fetch_error');
      expect(body.error).toBe('ECONNREFUSED');
    });

    it('uses default backend URL when config missing', async () => {
      const apiKeyManager = makeApiKeyManager('lx_key');
      mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ user: { id: '1' } }) });

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      await fetch(`${baseUrl}/api/auth/resolve`);
      expect(mockFetch.mock.calls[0][0]).toBe('https://autopilot-api.azurewebsites.net/auth/me');
    });
  });

  // ── /api/auth/signout ────────────────────────────────────────────
  describe('POST /api/auth/signout', () => {
    it('clears API key, cached auth, and broadcasts auth_signout', async () => {
      const apiKeyManager = makeApiKeyManager('lx_signed_in');
      ctx.lastAuthResult = { user: { email: 'a@b.com' }, hasApiKey: true, timestamp: '2026-01-01T00:00:00Z' };

      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/auth/signout`, { method: 'POST' })).json();
      expect(body.success).toBe(true);
      expect(apiKeyManager.removeSessionKeys).toHaveBeenCalledWith(null);
      expect(apiKeyManager._peek()).toBeNull();
      expect(ctx.lastAuthResult).toBeNull();
      expect(broadcasts[0].msg.type).toBe('auth_signout');
    });

    it('returns 500 when removeSessionKeys throws', async () => {
      const apiKeyManager = { removeSessionKeys: jest.fn().mockRejectedValue(new Error('vault locked')) };
      const app = buildAuthApp({ apiKeyManager, broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const res = await fetch(`${baseUrl}/api/auth/signout`, { method: 'POST' });
      expect(res.status).toBe(500);
      const body = await res.json();
      expect(body.error).toBe('vault locked');
    });
  });

  // ── /api/health/cloud ────────────────────────────────────────────
  describe('GET /api/health/cloud', () => {
    it('returns online=true with backend payload when health 200', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true, json: () => Promise.resolve({ status: 'OK', uptime: 123 })
      });

      const app = buildAuthApp({
        apiKeyManager: makeApiKeyManager(), broadcastToSession,
        config: { backend: { healthUrl: 'https://api.example.com/health' } },
        logger, fetchImpl: mockFetch, ctx
      });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/health/cloud`)).json();
      expect(body.success).toBe(true);
      expect(body.online).toBe(true);
      expect(body.backend.status).toBe('OK');
    });

    it('returns online=false when backend returns non-OK', async () => {
      mockFetch.mockResolvedValueOnce({ ok: false, status: 503 });

      const app = buildAuthApp({ apiKeyManager: makeApiKeyManager(), broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/health/cloud`)).json();
      expect(body.online).toBe(false);
      expect(body.error).toContain('503');
    });

    it('returns online=false when fetch throws', async () => {
      mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));

      const app = buildAuthApp({ apiKeyManager: makeApiKeyManager(), broadcastToSession, config: {}, logger, fetchImpl: mockFetch, ctx });
      const { server: s, baseUrl } = await startServer(app);
      server = s;

      const body = await (await fetch(`${baseUrl}/api/health/cloud`)).json();
      expect(body.online).toBe(false);
      expect(body.error).toBe('ECONNREFUSED');
    });
  });
});
