/**
 * DC-057 billing lookup endpoint tests.
 *
 * Tests the GET /api/v1/billing/lookup/:sessionId route handler with a
 * real fulfillment store on disk. Covers:
 *
 *   - 404 for unknown sessionId
 *   - processing state (record exists, no code yet)
 *   - pending_email state — license persisted, email failed (SMTP recovery path)
 *   - delivered state
 *   - 404 past the 24h TTL
 *   - Cache-Control: no-store on all responses
 *   - Parameterized PUBLIC_ROUTES entry exists for this path
 */

const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const express = require('express');

const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'dc057-lookup-'));
process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE = path.join(TMP, 'stripe-fulfillments.json');

const billingRoutes = require('../../routes/billing');
const { createFulfillmentStore } = require('../../src/billing/fulfillment-store');

function makeApp() {
  const app = express();
  // Mock asyncHandler that calls the inner fn synchronously.
  function asyncHandler(fn) {
    return (req, res, next) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
  }
  const router = billingRoutes({ asyncHandler });
  app.use('/api/v1/billing', router);
  return app;
}

function seedRecord(sessionId, overrides = {}) {
  const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
  // Plant a record directly via the mutation API.
  return store.claim({
    eventId: overrides.eventId || 'evt_seed',
    sessionId,
    productId: overrides.productId || 'pro-30d',
    durationDays: overrides.durationDays || 30,
    email: overrides.email || 'alice@example.com',
  });
}

describe('GET /api/v1/billing/lookup/:sessionId', () => {
  let app;
  beforeAll(() => {
    app = makeApp();
  });

  function get(sessionId) {
    return new Promise((resolve, reject) => {
      const server = app.listen(0, () => {
        const port = server.address().port;
        const http = require('http');
        http.get(`http://127.0.0.1:${port}/api/v1/billing/lookup/${encodeURIComponent(sessionId)}`, (res) => {
          let body = '';
          res.on('data', (c) => { body += c; });
          res.on('end', () => {
            server.close();
            resolve({ status: res.statusCode, headers: res.headers, body: body ? JSON.parse(body) : null });
          });
        }).on('error', reject);
      });
    });
  }

  test('returns 404 for unknown sessionId', async () => {
    const res = await get('cs_unknown_session');
    expect(res.status).toBe(404);
    expect(res.body).toMatchObject({ success: false });
    expect(res.headers['cache-control']).toBe('no-store');
  });

  test('returns 400 for invalid sessionId (too long)', async () => {
    const longId = 'x'.repeat(300);
    const res = await get(longId);
    expect(res.status).toBe(400);
    expect(res.headers['cache-control']).toBe('no-store');
  });

  test('returns processing state when record has no code yet', async () => {
    const sessionId = `cs_proc_${crypto.randomBytes(4).toString('hex')}`;
    await seedRecord(sessionId);

    const res = await get(sessionId);
    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
    expect(res.body.data).toMatchObject({ status: 'processing', durationDays: 30, productId: 'pro-30d' });
    expect(res.headers['cache-control']).toBe('no-store');
  });

  test('returns pending_email state with the persisted code (SMTP recovery)', async () => {
    const sessionId = `cs_pending_${crypto.randomBytes(4).toString('hex')}`;
    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    await store.claim({ eventId: 'evt_1', sessionId, productId: 'pro-90d', durationDays: 90, email: 'a@b.c' });
    await store.saveLicense({ eventId: 'evt_1', sessionId, code: 'DC-TEST-CODE-90D', codeId: 'cid_1' });
    await store.claimDelivery({ sessionId, ownerToken: 'evt_1' });
    await store.markDeliveryFailed({ sessionId, ownerToken: 'evt_1', error: 'smtp-down' });

    const res = await get(sessionId);
    expect(res.status).toBe(200);
    expect(res.body.data).toMatchObject({
      status: 'pending_email',
      durationDays: 90,
      productId: 'pro-90d',
      code: 'DC-TEST-CODE-90D',
      codeId: 'cid_1',
    });
    expect(res.body.data.lastError).toMatch(/smtp-down/);
  });

  test('returns delivered state with the code + deliveredVia', async () => {
    const sessionId = `cs_delivered_${crypto.randomBytes(4).toString('hex')}`;
    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    await store.claim({ eventId: 'evt_2', sessionId, productId: 'pro-365d', durationDays: 365, email: 'a@b.c' });
    await store.saveLicense({ eventId: 'evt_2', sessionId, code: 'DC-TEST-CODE-365D', codeId: 'cid_2' });
    await store.claimDelivery({ sessionId, ownerToken: 'evt_2' });
    await store.markDelivered({ sessionId, ownerToken: 'evt_2', deliveredVia: 'smtp' });

    const res = await get(sessionId);
    expect(res.status).toBe(200);
    expect(res.body.data).toMatchObject({
      status: 'delivered',
      durationDays: 365,
      productId: 'pro-365d',
      code: 'DC-TEST-CODE-365D',
      codeId: 'cid_2',
      deliveredVia: 'smtp',
    });
  });

  test('returns 404 past the 24h TTL', async () => {
    const sessionId = `cs_old_${crypto.randomBytes(4).toString('hex')}`;
    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    await store.claim({ eventId: 'evt_old', sessionId, productId: 'pro-30d', durationDays: 30, email: 'a@b.c' });
    await store.saveLicense({ eventId: 'evt_old', sessionId, code: 'DC-OLD', codeId: 'cid_old' });
    await store.markDelivered({ sessionId, ownerToken: 'evt_old', deliveredVia: 'smtp' });

    // Manually backdate the record's createdAt to be older than 24h.
    const fs = require('fs');
    const file = process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE;
    const state = JSON.parse(fs.readFileSync(file, 'utf8'));
    const r = state.bySessionId[sessionId];
    r.createdAt = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
    fs.writeFileSync(file, JSON.stringify(state, null, 2));

    const res = await get(sessionId);
    expect(res.status).toBe(404);
  });
});

describe('PUBLIC_ROUTES + CSRF allowlist for billing/lookup', () => {
  const fs = require('fs');
  test('PUBLIC_ROUTES includes /api/v1/billing/lookup/:sessionId', () => {
    const content = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'utilities', 'middleware.js'), 'utf8');
    expect(content).toMatch(/path:\s*['"]\/api\/v1\/billing\/lookup\/:sessionId['"]/);
  });

  test('CSRF excludedPaths includes /api/v1/billing/lookup/:sessionId', () => {
    const content = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'security', 'csrf-protection.js'), 'utf8');
    expect(content).toMatch(/['"]\/api\/v1\/billing\/lookup\/:sessionId['"]/);
  });
});
