/**
 * DC-057 bridge HTTP /lookup/:sessionId endpoint tests.
 *
 * Tests the bridge's own GET /lookup/:sessionId endpoint (separate from
 * the API route). The bridge endpoint is for out-of-band operator use —
 * the production customer lookup goes through routes/billing.js (covered
 * by billing-lookup.test.js). But the bridge must still serve /lookup/*
 * correctly for operator workflows and incident recovery.
 */

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

const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'dc057-bridge-http-'));
process.env.STRIPE_BRIDGE_STATE_DIR = TMP;
process.env.STRIPE_BRIDGE_EVENTS_FILE = path.join(TMP, 'stripe-events.json');
process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE = path.join(TMP, 'stripe-fulfillments.json');
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_test_' + crypto.randomBytes(8).toString('hex');
delete process.env.SMTP_HOST;
delete process.env.SMTP_FROM;

jest.mock('../../license-keygen', () => {
  // Use the built-in Date + Math.random instead of crypto so the jest.mock
  // factory stays in scope (jest.mock factory bodies cannot reference
  // outer-scope identifiers like `crypto`).
  const mockRandom = () => Math.random().toString(16).slice(2, 10).toUpperCase();
  let mockCounter = 0;
  return {
    VALID_DURATIONS: [30, 90, 180, 365],
    loadSecret: () => 'mock-secret',
    generateCodes: jest.fn(({ durationDays, count }) => {
      const codes = [];
      for (let i = 0; i < count; i++) {
        codes.push({
          code: `DC-TEST-${durationDays}D-${mockRandom()}`,
          codeId: `cid_${Date.now()}_${i}_${++mockCounter}`,
        });
      }
      return codes;
    }),
  };
});
jest.mock('nodemailer', () => ({
  createTransport: () => ({ sendMail: jest.fn() }),
}));

const { createFulfillmentStore } = require('../../src/billing/fulfillment-store');
const bridge = require('../../scripts/stripe-license-bridge');

let server;
let port;

beforeAll((done) => {
  // Use the bridge's own createServer() factory so the test exercises the
  // SAME request dispatcher the production server uses (no duplicated
  // route decoding / status mapping in test code).
  server = bridge.createServer();
  server.listen(0, () => {
    port = server.address().port;
    done();
  });
});

afterAll((done) => {
  server.close(done);
});

function get(path) {
  return new Promise((resolve, reject) => {
    http.get(`http://127.0.0.1:${port}${path}`, (res) => {
      let body = '';
      res.on('data', (c) => { body += c; });
      res.on('end', () => {
        resolve({ status: res.statusCode, headers: res.headers, body: body ? JSON.parse(body) : null });
      });
    }).on('error', reject);
  });
}

describe('bridge GET /lookup/:sessionId', () => {
  test('returns 404 for unknown sessionId', async () => {
    const res = await get('/lookup/cs_unknown_session');
    expect(res.status).toBe(404);
    expect(res.body).toEqual({ status: 'not_found' });
    expect(res.headers['cache-control']).toBe('no-store');
  });

  test('returns 400 for malformed percent-encoded sessionId', async () => {
    // %ZZ is not valid hex.
    const res = await get('/lookup/cs_%ZZ_bad');
    expect(res.status).toBe(400);
    expect(res.body.reason).toBe('invalid-session-id');
  });

  test('returns delivered state with code + deliveredVia for planted record', async () => {
    const sessionId = `cs_test_delivered_${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-30d', durationDays: 30, email: 'a@b.c' });
    await store.saveLicense({ eventId: 'evt_1', sessionId, code: 'DC-X', codeId: 'cid_1' });
    await store.claimDelivery({ sessionId, ownerToken: 'evt_1' });
    await store.markDelivered({ sessionId, ownerToken: 'evt_1', deliveredVia: 'smtp' });

    const res = await get(`/lookup/${encodeURIComponent(sessionId)}`);
    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      status: 'delivered',
      durationDays: 30,
      productId: 'pro-30d',
      code: 'DC-X',
      codeId: 'cid_1',
      deliveredVia: 'smtp',
    });
    expect(res.headers['cache-control']).toBe('no-store');
  });

  test('returns pending_email state (SMTP recovery)', async () => {
    const sessionId = `cs_test_pending_${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-90d', durationDays: 90, email: 'a@b.c' });
    await store.saveLicense({ eventId: 'evt_2', sessionId, code: 'DC-Y', codeId: 'cid_2' });
    await store.claimDelivery({ sessionId, ownerToken: 'evt_2' });
    await store.markDeliveryFailed({ sessionId, ownerToken: 'evt_2', error: 'smtp-down' });

    const res = await get(`/lookup/${encodeURIComponent(sessionId)}`);
    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      status: 'pending_email',
      durationDays: 90,
      productId: 'pro-90d',
      code: 'DC-Y',
      codeId: 'cid_2',
    });
    expect(res.body.lastError).toMatch(/smtp-down/);
  });

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

    // Backdate createdAt to be older than 24h.
    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(`/lookup/${encodeURIComponent(sessionId)}`);
    expect(res.status).toBe(404);
    expect(res.body).toEqual({ status: 'expired' });
  });
});
