/**
 * End-to-end billing integration test.
 *
 * Exercises the FULL purchase → fulfillment → activation → Pro unlock flow:
 *
 *   1. POST /api/v1/billing/checkout   → mock Stripe SDK → session { id, url }
 *   2. Simulate webhook delivery        → bridge.handleWebhook() with a signed
 *                                         checkout.session.completed payload
 *   3. GET  /api/v1/billing/lookup/:sessionId → verify license code returned
 *   4. POST /api/v1/license/activate    → verify code activates, Pro unlocks
 *
 * The bridge and the API billing routes communicate through a SHARED
 * fulfillment-store file (the production IPC channel — a bind-mounted JSON
 * file). This test wires both sides to the same tmp file so the lookup
 * endpoint sees the license the bridge persisted, exactly as in production.
 *
 * The REAL license-keygen + LicenseManager are used (no HMAC mock) so the
 * code generated by the bridge is cryptographically valid and activates
 * through the real LicenseManager.verifyCode() path. Only Stripe's network
 * surface and nodemailer are mocked.
 */

'use strict';

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

// ── jest.mock must be hoisted before any require() ─────────────────────────
// Mock nodemailer so the bridge never opens a real SMTP connection. SMTP is
// left unconfigured (no SMTP_HOST/SMTP_FROM) so deliverCode() falls back to
// dev-console mode — the documented dev/test path where the license is marked
// `delivered` without actually sending email.
jest.mock('nodemailer', () => ({
  createTransport: jest.fn(() => ({ sendMail: jest.fn() })),
}));

// ── Isolated tmp state (set BEFORE requiring the bridge + routes) ──────────
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-e2e-billing-'));

// Shared fulfillment-store file — the IPC channel between bridge and API.
process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE = path.join(TMP, 'stripe-fulfillments.json');
process.env.STRIPE_BRIDGE_STATE_DIR = TMP;
process.env.STRIPE_BRIDGE_EVENTS_FILE = path.join(TMP, 'stripe-events.json');
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_e2e_' + crypto.randomBytes(8).toString('hex');

// Configure Stripe products so the catalog + stripe-client can resolve price IDs.
process.env.STRIPE_SECRET_KEY = 'sk_test_e2e';
process.env.STRIPE_PRICE_PRO_30D = 'price_30d_e2e';
process.env.STRIPE_PRICE_PRO_90D = 'price_90d_e2e';
process.env.STRIPE_PRICE_PRO_180D = 'price_180d_e2e';
process.env.STRIPE_PRICE_PRO_365D = 'price_365d_e2e';
process.env.STRIPE_PUBLIC_ORIGIN = 'https://status.test';

// No SMTP → bridge uses dev-console delivery (license marked delivered, no email).
delete process.env.SMTP_HOST;
delete process.env.SMTP_FROM;

// ── Real license-keygen with a known master secret ─────────────────────────
// We write a real secret file so the bridge's loadSecret() + generateCodes()
// produce HMAC-valid codes that the LicenseManager can verify with the SAME
// secret. This makes the activation step exercise the real cryptographic path.
const E2E_SECRET = crypto.randomBytes(32).toString('hex');
const SECRET_FILE = path.join(TMP, '.license-secret');
fs.writeFileSync(SECRET_FILE, E2E_SECRET, { mode: 0o600 });
process.env.LICENSE_SECRET_FILE = SECRET_FILE;

// Real keygen — no mock. The counter file is isolated to the tmp dir.
process.env.LICENSE_COUNTER_FILE = path.join(TMP, '.license-counter');

// Now require modules (after env + mock setup).
const keygen = require('../../license-keygen');
const catalog = require('../../src/billing/catalog');
const stripeClient = require('../../src/billing/stripe-client');
const bridge = require('../../scripts/stripe-license-bridge');
const billingRoutesFactory = require('../../routes/billing');
const licenseRoutesFactory = require('../../routes/license');
const { LicenseManager } = require('../../src/managers/license-manager');
const { createFulfillmentStore } = require('../../src/billing/fulfillment-store');

// ── Test app: mounts billing + license routes the same way app.js does ─────
function makeApp(licenseManager) {
  const app = express();
  app.use(express.json());

  function asyncHandler(fn) {
    return (req, res, next) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
  }

  app.use('/api/v1/billing', billingRoutesFactory({ asyncHandler }));
  app.use('/api/v1/license', licenseRoutesFactory({ licenseManager, asyncHandler }));

  // Jest/express error handler — surfaces route errors as JSON so supertest
  // can assert on the body.
  app.use((err, req, res, next) => {
    const status = err.statusCode || 500;
    res.status(status).json({ success: false, error: err.message });
  });

  return app;
}

// ── Helpers ────────────────────────────────────────────────────────────────

/**
 * Build a signed Stripe webhook payload for checkout.session.completed.
 */
function buildSignedWebhook(sessionId, productId, customerEmail, opts = {}) {
  const product = catalog.getProduct(productId);
  const event = {
    id: opts.eventId || `evt_e2e_${crypto.randomBytes(6).toString('hex')}`,
    type: opts.type || 'checkout.session.completed',
    data: {
      object: {
        id: sessionId,
        customer_email: customerEmail,
        customer_details: { email: customerEmail },
        payment_status: 'paid',
        amount_total: product ? product.amountCents : 0,
        currency: 'usd',
        metadata: { productId, product: 'dashcaddy-pro' },
      },
    },
  };
  const rawBody = Buffer.from(JSON.stringify(event));
  const ts = Math.floor(Date.now() / 1000);
  const sig = crypto.createHmac('sha256', process.env.STRIPE_WEBHOOK_SECRET)
    .update(`${ts}.${rawBody}`, 'utf8').digest('hex');
  return { rawBody, signatureHeader: `t=${ts},v1=${sig}`, event };
}

/**
 * Install a mock Stripe SDK that returns a checkout session with a
 * caller-chosen id + url. Captures the params passed to sessions.create().
 */
function installMockStripe(sessionId, sessionUrl) {
  let capturedParams;
  const mockStripe = jest.fn().mockReturnValue({
    checkout: {
      sessions: {
        create: jest.fn().mockImplementation(async (params) => {
          capturedParams = params;
          return { id: sessionId, url: sessionUrl };
        }),
      },
    },
  });
  stripeClient._setStripeSdk(mockStripe);
  return { capturedParams: () => capturedParams };
}

// ── Cleanup ────────────────────────────────────────────────────────────────
afterAll(() => {
  stripeClient._setStripeSdk(null);
  try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (_) { /* best effort */ }
});

// ═══════════════════════════════════════════════════════════════════════════
//  THE END-TO-END FLOW
// ═══════════════════════════════════════════════════════════════════════════

describe('end-to-end billing flow: checkout → webhook → lookup → activate → Pro', () => {
  const PRODUCT_ID = 'pro-90d';
  const CUSTOMER_EMAIL = 'alice@example.com';
  const SESSION_ID = `cs_e2e_${crypto.randomBytes(6).toString('hex')}`;
  const CHECKOUT_URL = `https://checkout.stripe.com/c/pay/${SESSION_ID}`;

  let app;
  let licenseManager;
  let activationCode; // captured during the flow

  beforeAll(() => {
    // Real LicenseManager, configured with the same secret the bridge uses.
    licenseManager = new LicenseManager(
      {
        store: jest.fn().mockResolvedValue(undefined),
        retrieve: jest.fn().mockResolvedValue(null),
        delete: jest.fn().mockResolvedValue(undefined),
      },
      path.join(TMP, 'config.json'),
      { info: () => {}, warn: () => {}, error: () => {} }
    );
    // loadSecret reads the file and stores it as masterSecretHash for verifyCode().
    licenseManager.loadSecret(SECRET_FILE);
    app = makeApp(licenseManager);
  });

  // ── Step 1: POST /api/v1/billing/checkout ──────────────────────────────
  test('Step 1: checkout creates a Stripe session via the mock SDK', async () => {
    const stripe = installMockStripe(SESSION_ID, CHECKOUT_URL);

    const res = await request(app)
      .post('/api/v1/billing/checkout')
      .send({ productId: PRODUCT_ID, customerEmail: CUSTOMER_EMAIL })
      .expect(200);

    expect(res.body.success).toBe(true);
    expect(res.body.data.id).toBe(SESSION_ID);
    expect(res.body.data.url).toBe(CHECKOUT_URL);

    // The mock Stripe SDK was called with the correct product + metadata.
    const params = stripe.capturedParams();
    expect(params.mode).toBe('payment');
    expect(params.metadata.productId).toBe(PRODUCT_ID);
    expect(params.line_items[0].price).toBe('price_90d_e2e');
    expect(params.customer_email).toBe(CUSTOMER_EMAIL);
  });

  // ── Step 2: Simulate Stripe webhook delivery ───────────────────────────
  test('Step 2: webhook generates + persists + delivers the license', async () => {
    const { rawBody, signatureHeader, event } = buildSignedWebhook(
      SESSION_ID, PRODUCT_ID, CUSTOMER_EMAIL
    );

    const result = await bridge.handleWebhook({ rawBody, signatureHeader });

    expect(result.status).toBe(200);
    expect(result.body.delivered).toBe(true);
    expect(result.body.productId).toBe(PRODUCT_ID);
    expect(result.body.durationDays).toBe(90);
    expect(result.body.codeId).toBeTruthy();
    expect(result.body.deliveredVia).toBe('dev-console');

    // Capture the code for subsequent steps.
    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    const record = store.readBySession(SESSION_ID);
    expect(record).toBeTruthy();
    expect(record.status).toBe('delivered');
    expect(record.code).toBeTruthy();
    activationCode = record.code;
  });

  // ── Step 3: GET /api/v1/billing/lookup/:sessionId ──────────────────────
  test('Step 3: lookup returns the delivered license code', async () => {
    const res = await request(app)
      .get(`/api/v1/billing/lookup/${SESSION_ID}`)
      .expect(200);

    expect(res.body.success).toBe(true);
    expect(res.body.data.status).toBe('delivered');
    expect(res.body.data.code).toBe(activationCode);
    expect(res.body.data.codeId).toBeTruthy();
    expect(res.body.data.productId).toBe(PRODUCT_ID);
    expect(res.body.data.durationDays).toBe(90);
    expect(res.body.data.deliveredVia).toBe('dev-console');
    // Bearer-style secret — must never be cached.
    expect(res.headers['cache-control']).toBe('no-store');
  });

  // ── Step 4: POST /api/v1/license/activate → Pro unlock ─────────────────
  test('Step 4: activate the license → Pro tier unlocks', async () => {
    expect(activationCode).toBeTruthy();

    const res = await request(app)
      .post('/api/v1/license/activate')
      .send({ code: activationCode })
      .expect(200);

    expect(res.body.success).toBe(true);
    expect(res.body.license).toBeDefined();
    expect(res.body.license.active).toBe(true);
    expect(res.body.license.tier).toBe('premium');
    expect(res.body.license.durationDays).toBe(90);
    expect(res.body.license.expired).toBe(false);

    // The LicenseManager itself now reports Pro (this is what gates features
    // elsewhere in the app via licenseManager.isPro()).
    expect(licenseManager.isPro()).toBe(true);
    expect(licenseManager.hasFeature('sso')).toBe(true);
  });

  // ── Bonus: GET /api/v1/license/status reflects the active Pro license ──
  test('Step 5: license status confirms Pro is active', async () => {
    const res = await request(app)
      .get('/api/v1/license/status')
      .expect(200);

    expect(res.body.success).toBe(true);
    expect(res.body.license.active).toBe(true);
    expect(res.body.license.tier).toBe('premium');
    expect(res.body.license.expired).toBe(false);
    expect(res.body.license.features).toEqual(
      expect.arrayContaining(['sso', 'recipes', 'swarm'])
    );
  });
});

// ═══════════════════════════════════════════════════════════════════════════
//  Additional e2e scenarios
// ═══════════════════════════════════════════════════════════════════════════

describe('e2e: lookup returns 404 before webhook delivers the license', () => {
  test('lookup before webhook → 404 not found', async () => {
    const app = makeApp(null);
    const sessionId = `cs_notyet_${crypto.randomBytes(4).toString('hex')}`;
    const res = await request(app)
      .get(`/api/v1/billing/lookup/${sessionId}`)
      .expect(404);
    expect(res.body.success).toBe(false);
  });
});

describe('e2e: each catalog product flows through to a valid activatable license', () => {
  // Use a fresh app + licenseManager per product to avoid activation conflicts.
  for (const product of catalog.PRODUCTS) {
    test(`product ${product.id} (${product.durationDays}d) activates and unlocks Pro`, async () => {
      const sessionId = `cs_e2e_${product.id}_${crypto.randomBytes(4).toString('hex')}`;
      const email = `buyer_${product.id}@example.com`;

      const lm = new LicenseManager(
        {
          store: jest.fn().mockResolvedValue(undefined),
          retrieve: jest.fn().mockResolvedValue(null),
          delete: jest.fn().mockResolvedValue(undefined),
        },
        path.join(TMP, `config-${product.id}.json`),
        { info: () => {}, warn: () => {}, error: () => {} }
      );
      lm.loadSecret(SECRET_FILE);
      const app = makeApp(lm);

      // Checkout
      installMockStripe(sessionId, `https://checkout.stripe.com/c/pay/${sessionId}`);
      const checkoutRes = await request(app)
        .post('/api/v1/billing/checkout')
        .send({ productId: product.id, customerEmail: email })
        .expect(200);
      expect(checkoutRes.body.data.id).toBe(sessionId);

      // Webhook
      const { rawBody, signatureHeader } = buildSignedWebhook(sessionId, product.id, email);
      const whResult = await bridge.handleWebhook({ rawBody, signatureHeader });
      expect(whResult.status).toBe(200);
      expect(whResult.body.delivered).toBe(true);
      expect(whResult.body.durationDays).toBe(product.durationDays);

      // Lookup
      const lookupRes = await request(app)
        .get(`/api/v1/billing/lookup/${sessionId}`)
        .expect(200);
      expect(lookupRes.body.data.status).toBe('delivered');
      expect(lookupRes.body.data.code).toBeTruthy();
      const code = lookupRes.body.data.code;

      // Activate → Pro
      const activateRes = await request(app)
        .post('/api/v1/license/activate')
        .send({ code })
        .expect(200);
      expect(activateRes.body.license.tier).toBe('premium');
      expect(activateRes.body.license.durationDays).toBe(product.durationDays);
      expect(lm.isPro()).toBe(true);
    });
  }
});

describe('e2e: webhook idempotency — duplicate delivery reuses the same license', () => {
  test('a second webhook for the same session does not mint a new code', async () => {
    const sessionId = `cs_e2e_dedup_${crypto.randomBytes(4).toString('hex')}`;
    const productId = 'pro-30d';
    const email = 'dedup@example.com';

    // First delivery.
    const payload1 = buildSignedWebhook(sessionId, productId, email);
    const r1 = await bridge.handleWebhook({
      rawBody: payload1.rawBody,
      signatureHeader: payload1.signatureHeader,
    });
    expect(r1.status).toBe(200);
    expect(r1.body.delivered).toBe(true);

    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    const firstCode = store.readBySession(sessionId).code;
    expect(firstCode).toBeTruthy();

    // Same eventId (Stripe retry) → layer-1 idempotency, no regeneration.
    const r2 = await bridge.handleWebhook({
      rawBody: payload1.rawBody,
      signatureHeader: payload1.signatureHeader,
    });
    expect(r2.status).toBe(200);
    expect(r2.body.deduplicated).toBe(true);

    const secondCode = store.readBySession(sessionId).code;
    expect(secondCode).toBe(firstCode);
  });
});

describe('e2e: the license code generated by the bridge verifies via the real keygen', () => {
  test('bridge-generated code is cryptographically valid', async () => {
    const sessionId = `cs_e2e_crypto_${crypto.randomBytes(4).toString('hex')}`;
    const { rawBody, signatureHeader } = buildSignedWebhook(sessionId, 'pro-365d', 'crypto@example.com');
    const result = await bridge.handleWebhook({ rawBody, signatureHeader });
    expect(result.status).toBe(200);

    const store = createFulfillmentStore({ filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE });
    const code = store.readBySession(sessionId).code;

    // verifyCode with the SAME secret the bridge used — this is exactly what
    // LicenseManager._validateOffline does during activation.
    const verification = keygen.verifyCode(E2E_SECRET, code);
    expect(verification.valid).toBe(true);
    expect(verification.durationDays).toBe(365);
    expect(verification.expired).toBe(false);
  });
});
