/**
 * DC-057 billing checkout origin resolution tests.
 *
 * The checkout endpoint embeds the success_url (and cancel_url) into the
 * Stripe Checkout Session. These URLs are what Stripe redirects the
 * customer's browser to after payment. They MUST be derived only from
 * trusted sources — otherwise a header-injection attacker could redirect
 * customers to their own origin and capture the session_id, which is
 * the bearer token for /api/v1/billing/lookup/:sessionId (and that
 * endpoint serves the customer's license code on success).
 *
 * The origin is resolved in this priority order:
 *   1. STRIPE_PUBLIC_ORIGIN env var (canonical deployment shape)
 *   2. Request Host header, but ONLY when the host is in
 *      STRIPE_ALLOWED_HOSTS (operator-declared allowlist)
 *   3. undefined (Stripe falls back to its own defaults)
 */

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

// Save the fulfillment-store path so the route module captures the same
// path the route would in production. (Tests below exercise the
// stripe-client, not the fulfillment store, so the lookup endpoint can
// share the same file.)
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'dc057-origin-'));
process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE = path.join(TMP, 'stripe-fulfillments.json');

const billingRoutes = require('../../routes/billing');
const stripeClient = require('../../src/billing/stripe-client');

const REQUIRED_ENV = {
  STRIPE_SECRET_KEY: '«redacted:sk_test_…»',
  STRIPE_PRICE_PRO_30D: 'price_30d_test',
  STRIPE_PRICE_PRO_90D: 'price_90d_test',
  STRIPE_PRICE_PRO_180D: 'price_180d_test',
  STRIPE_PRICE_PRO_365D: 'price_365d_test',
};

function setEnv(overrides = {}) {
  const all = { ...REQUIRED_ENV, ...overrides };
  for (const [k, v] of Object.entries(all)) {
    process.env[k] = v;
  }
}
function clearEnv() {
  for (const k of Object.keys(REQUIRED_ENV)) delete process.env[k];
  delete process.env.STRIPE_PUBLIC_ORIGIN;
  delete process.env.STRIPE_ALLOWED_HOSTS;
  delete process.env.NODE_ENV;
}

function makeApp() {
  const app = express();
  app.use(require('express').json());
  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 postCheckout(req, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const server = req.listen(0, () => {
      const port = server.address().port;
      const data = JSON.stringify(body);
      const headerLines = Object.entries({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers })
        .map(([k, v]) => `${k}: ${v}`).join('\r\n');
      const req2 = http.request({
        hostname: '127.0.0.1', port, path: '/api/v1/billing/checkout', method: 'POST',
        headers: Object.fromEntries(Object.entries({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }).map(([k, v]) => [k.toLowerCase(), v])),
      }, (res) => {
        let buf = '';
        res.on('data', (c) => { buf += c; });
        res.on('end', () => {
          server.close();
          resolve({ status: res.statusCode, headers: res.headers, body: buf ? JSON.parse(buf) : null });
        });
      });
      req2.on('error', reject);
      req2.write(data);
      req2.end();
    });
  });
}

describe('POST /api/v1/billing/checkout — origin resolution (DC-057 security)', () => {
  let app;
  beforeAll(() => {
    app = makeApp();
    setEnv();
  });
  afterEach(() => {
    clearEnv();
    setEnv();
    stripeClient._setStripeSdk(null);
  });

  test('uses STRIPE_PUBLIC_ORIGIN env var (canonical deployment shape)', async () => {
    setEnv({ STRIPE_PUBLIC_ORIGIN: 'https://status.sami' });
    const mockSession = { id: 'cs_test_orig_1', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_1' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' });
    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
    // The captured Stripe params must include the success_url + cancel_url
    // built from the operator-declared origin — NOT from the request's Host
    // header. This is the canonical deployment shape.
    expect(capturedParams.success_url).toBe('https://status.sami/billing/success?session_id={CHECKOUT_SESSION_ID}');
    expect(capturedParams.cancel_url).toBe('https://status.sami/pricing');
  });

  test('rejects Host header injection when STRIPE_ALLOWED_HOSTS is empty', async () => {
    // Attacker sets X-Forwarded-Host: evil.com. The request reaches our
    // endpoint. Without STRIPE_PUBLIC_ORIGIN + without STRIPE_ALLOWED_HOSTS,
    // the origin must be undefined — we MUST NOT trust the attacker header.
    setEnv({ STRIPE_ALLOWED_HOSTS: '' });
    const mockSession = { id: 'cs_test_orig_2', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_2' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' }, {
      'X-Forwarded-Host': 'evil.com',
      'X-Forwarded-Proto': 'https',
    });
    expect(res.status).toBe(200);
    // origin must be undefined when allowlist is empty — the Stripe SDK
    // is called with undefined origin and the stripe-client falls back to
    // relative '/billing/success' which is safe (no host poisoning).
    expect(capturedParams.success_url).toMatch(/^\/billing\/success/);
  });

  test('accepts Host header when STRIPE_ALLOWED_HOSTS includes it', async () => {
    setEnv({ STRIPE_ALLOWED_HOSTS: 'status.sami,dashcaddy.net' });
    const mockSession = { id: 'cs_test_orig_3', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_3' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' }, {
      'X-Forwarded-Host': 'status.sami',
      'X-Forwarded-Proto': 'https',
    });
    expect(res.status).toBe(200);
    expect(capturedParams.success_url).toContain('status.sami');
    expect(capturedParams.success_url).toContain('/billing/success');
  });

  test('rejects Host header when host is NOT in STRIPE_ALLOWED_HOSTS', async () => {
    setEnv({ STRIPE_ALLOWED_HOSTS: 'status.sami' });
    const mockSession = { id: 'cs_test_orig_4', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_4' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' }, {
      'X-Forwarded-Host': 'evil.com',
      'X-Forwarded-Proto': 'https',
    });
    expect(res.status).toBe(200);
    // origin is undefined → relative /billing/success URL (safe).
    expect(capturedParams.success_url).toMatch(/^\/billing\/success/);
    expect(capturedParams.success_url).not.toContain('evil.com');
  });

  test('rejects javascript: scheme injection via STRIPE_PUBLIC_ORIGIN', async () => {
    setEnv({ STRIPE_PUBLIC_ORIGIN: 'javascript:alert(1)' });
    const mockSession = { id: 'cs_test_orig_5', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_5' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' });
    expect(res.status).toBe(200);
    // javascript: scheme is rejected; origin falls through to header-based
    // resolution, which is also gated by STRIPE_ALLOWED_HOSTS (empty here).
    expect(capturedParams.success_url).not.toMatch(/javascript:/);
  });

  test('rejects http:// in production when NODE_ENV=production', async () => {
    setEnv({ STRIPE_PUBLIC_ORIGIN: 'http://status.sami', NODE_ENV: 'production' });
    const mockSession = { id: 'cs_test_orig_6', url: 'https://checkout.stripe.com/c/pay/cs_test_orig_6' };
    let capturedParams;
    stripeClient._setStripeSdk(jest.fn().mockReturnValue({
      checkout: { sessions: { create: jest.fn().mockImplementation(async (params) => {
        capturedParams = params;
        return mockSession;
      }) } },
    }));

    const res = await postCheckout(app, { productId: 'pro-30d' });
    expect(res.status).toBe(200);
    // http:// rejected in production; origin falls back to undefined.
    expect(capturedParams.success_url).not.toMatch(/^http:/);
  });
});
