/* test/billing.test.js
 *
 * End-to-end billing flow test with mocked Stripe SDK.
 *
 * What this proves:
 *   1. /billing/plans returns the 4 plans
 *   2. POST /billing/checkout creates a Stripe customer + checkout session
 *      and persists the customer ID to the user
 *   3. POST /billing/portal returns a portal URL for an existing customer
 *   4. Webhook checkout.session.completed upgrades the user to the right plan
 *   5. Webhook customer.subscription.updated syncs the plan
 *   6. Webhook customer.subscription.deleted drops to free
 *   7. Webhook invoice.payment_failed marks the user + sends the email
 *   8. Webhook signature verification fails when signature is wrong
 *   9. Webhook replay protection rejects old events
 *  10. Webhook dedup ignores duplicate event ids
 *
 * The Stripe SDK is replaced with a mock that simulates the calls. The
 * server code path is the real code — same imports, same handlers.
 *
 * The mock is shared with test/billingAnnual.test.js so both files can
 * observe the same Stripe session params (lastSessionCreate, etc.).
 */
'use strict';

const assert = require('assert');
const crypto = require('crypto');
const { setupFreshDb, db } = require('./_setup');
const User = require('../models/user');
const stripeMock = require('./_stripeMock');

// Install the shared Stripe mock before any server route loads it.
stripeMock.installMock();

// Now safe to require the routes — they'll import the mock
const billingRoutes = require('../routes/billing');
const webhookRoutes = require('../routes/webhook');
const { getStripePriceId, PLANS } = require('../config/plans');
const sign = stripeMock.sign;

// Env vars used by billing.test.js (legacy single-price setup).
// billingAnnual.test.js overwrites these with _m/_y variants but
// the env override order doesn't matter since each test file sets
// up its own env before its tests run.
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_test_for_billing';
process.env.STRIPE_PRICE_STANDARD = 'price_test_standard';
process.env.STRIPE_PRICE_PRO     = 'price_test_pro';
process.env.STRIPE_PRICE_AGENCY  = 'price_test_agency';
process.env.APP_URL = 'https://shield.test.local';

// ---- Helpers ---------------------------------------------------------
function makeReq({ user, body, method = 'POST', path = '/', rawBody = null, headers = {} }) {
  return { user, body, method, path, headers, app: { get: () => false } };
}
function makeRes() {
  const res = {
    statusCode: 200,
    body: null,
    headers: {},
    status(c) { this.statusCode = c; return this; },
    json(o) { this.body = o; return this; },
    send(o) { this.body = o; return this; },
    set(k, v) { this.headers[k] = v; return this; },
  };
  return res;
}

const TESTS = [];
function test(name, fn) { TESTS.push({ name, fn }); }

// ---- Tests -----------------------------------------------------------

test('GET /billing/plans returns 4 plans with price ids', () => {
  setupFreshDb();
  const r = billingRoutes.stack ? null : null; // sanity: routes loaded
  const req = makeReq({ method: 'GET', path: '/plans', body: {} });
  const res = makeRes();
  // route is a router — call its handler by matching path
  const handler = findHandler(billingRoutes, 'GET', '/plans');
  handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(res.body.plans.length, 5);
  const keys = res.body.plans.map(p => p.key);
  assert.deepStrictEqual(keys, ['free', 'standard', 'pro', 'agency', 'credit_pack_small']);
  // Price ids should be resolved (not null) for paid plans (only
  // those that have their env var set in this test's env block).
  for (const p of res.body.plans) {
    if (p.key === 'free') continue;
    if (!process.env[`STRIPE_PRICE_${p.key.toUpperCase().replace(/_/g, '_')}`]) continue;
    assert.ok(getStripePriceId(p.key), 'price id for ' + p.key);
  }
});

test('POST /billing/checkout creates customer + returns session URL', async () => {
  setupFreshDb();
  const u = await makeUser('alice@test.local');
  const req = makeReq({ user: u, body: { plan: 'pro' } });
  const res = makeRes();
  const handler = findHandler(billingRoutes, 'POST', '/checkout');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.ok(res.body.url, 'session url returned');
  assert.ok(res.body.url.startsWith('https://checkout.stripe.com/'));
  // Customer was persisted
  const updated = User.getById(u.id);
  assert.ok(updated.stripe_customer_id, 'customer id persisted');
  assert.ok(updated.stripe_customer_id.startsWith('cus_mock_'));
});

test('POST /billing/checkout rejects unknown plan name', async () => {
  setupFreshDb();
  const u = await makeUser('bob@test.local');
  const req = makeReq({ user: u, body: { plan: 'platinum' } });
  const res = makeRes();
  const handler = findHandler(billingRoutes, 'POST', '/checkout');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 400);
  assert.strictEqual(res.body.error, 'INVALID_PLAN');
});

test('POST /billing/portal requires existing customer', async () => {
  setupFreshDb();
  const u = await makeUser('carol@test.local'); // no stripe_customer_id
  const req = makeReq({ user: u, body: {} });
  const res = makeRes();
  const handler = findHandler(billingRoutes, 'POST', '/portal');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 400);
  assert.strictEqual(res.body.error, 'NO_STRIPE_CUSTOMER');
});

test('POST /billing/portal returns portal URL for customer', async () => {
  setupFreshDb();
  const u = await makeUser('dave@test.local');
  User.setStripeCustomerId(u.id, 'cus_mock_dave');
  // Refresh user to pick up the new stripe_customer_id
  const refreshed = User.getById(u.id);
  const req = makeReq({ user: refreshed, body: {} });
  const res = makeRes();
  const handler = findHandler(billingRoutes, 'POST', '/portal');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.ok(res.body.url.startsWith('https://billing.stripe.com/'));
});

// ---- Webhook tests ---------------------------------------------------

test('webhook: signature verify fails with bad signature', async () => {
  setupFreshDb();
  const payload = JSON.stringify({ id: 'evt_bad', type: 'ping' });
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload,
    body: payload, headers: { 'stripe-signature': 't=1234,v1=deadbeef' },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 400);
});

test('webhook: checkout.session.completed sets customer + subscription', async () => {
  setupFreshDb();
  const u = await makeUser('eve@test.local');
  const event = {
    id: 'evt_cs_' + Date.now(),
    type: 'checkout.session.completed',
    created: Math.floor(Date.now() / 1000),
    data: { object: {
      id: 'cs_test_xyz',
      customer: 'cus_mock_eve',
      subscription: 'sub_mock_eve',
      metadata: { user_id: String(u.id), plan: 'pro' },
    } },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload, body: payload,
    headers: { 'stripe-signature': header },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(res.body.received, true);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.stripe_customer_id, 'cus_mock_eve');
  assert.strictEqual(updated.stripe_subscription_id, 'sub_mock_eve');
  assert.strictEqual(updated.plan, 'pro');
});

test('webhook: customer.subscription.updated syncs plan from price id', async () => {
  setupFreshDb();
  const u = await makeUser('frank@test.local');
  User.setStripeCustomerId(u.id, 'cus_mock_frank');
  // Build a sub object whose price id matches the AGENCY plan
  const agencyPriceId = getStripePriceId('agency');
  const event = {
    id: 'evt_sub_' + Date.now(),
    type: 'customer.subscription.updated',
    created: Math.floor(Date.now() / 1000),
    data: { object: {
      id: 'sub_mock_frank',
      customer: 'cus_mock_frank',
      status: 'active',
      items: { data: [{ price: { id: agencyPriceId } }] },
    } },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload, body: payload,
    headers: { 'stripe-signature': header },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.plan, 'agency');
  assert.strictEqual(updated.stripe_subscription_id, 'sub_mock_frank');
});

test('webhook: customer.subscription.deleted drops user to free', async () => {
  setupFreshDb();
  const u = await makeUser('gina@test.local');
  User.setStripeCustomerId(u.id, 'cus_mock_gina');
  User.setStripeSubscriptionId(u.id, 'sub_mock_gina', 'pro', 'month', 1);
  const event = {
    id: 'evt_del_' + Date.now(),
    type: 'customer.subscription.deleted',
    created: Math.floor(Date.now() / 1000),
    data: { object: { id: 'sub_mock_gina', customer: 'cus_mock_gina' } },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload, body: payload,
    headers: { 'stripe-signature': header },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.plan, 'free');
});

test('webhook: invoice.payment_failed marks user + sends email', async () => {
  setupFreshDb();
  const u = await makeUser('hank@test.local');
  User.setStripeCustomerId(u.id, 'cus_mock_hank');
  const event = {
    id: 'evt_pf_' + Date.now(),
    type: 'invoice.payment_failed',
    created: Math.floor(Date.now() / 1000),
    data: { object: { id: 'in_mock', customer: 'cus_mock_hank' } },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload, body: payload,
    headers: { 'stripe-signature': header },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 200);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.payment_failed, 1, 'payment_failed flag set');
});

test('webhook: replay protection rejects events older than 5 min', async () => {
  setupFreshDb();
  const event = {
    id: 'evt_old_' + Date.now(),
    type: 'ping',
    created: Math.floor(Date.now() / 1000) - 600,  // 10 min old
    data: { object: {} },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  const req = makeReq({
    method: 'POST', path: '/', rawBody: payload, body: payload,
    headers: { 'stripe-signature': header },
  });
  const res = makeRes();
  const handler = findHandler(webhookRoutes, 'POST', '/');
  await handler(req, res);
  assert.strictEqual(res.statusCode, 400);
  assert.ok(/too old/i.test(res.body));
});

test('webhook: dedup ignores duplicate event ids', async () => {
  setupFreshDb();
  const u = await makeUser('ivy@test.local');
  const event = {
    id: 'evt_dup_' + Date.now(),
    type: 'checkout.session.completed',
    created: Math.floor(Date.now() / 1000),
    data: { object: {
      id: 'cs_dup',
      customer: 'cus_mock_ivy',
      subscription: 'sub_mock_ivy',
      metadata: { user_id: String(u.id), plan: 'pro' },
    } },
  };
  const payload = JSON.stringify(event);
  const { header } = sign(payload, process.env.STRIPE_WEBHOOK_SECRET);
  // Send it twice
  for (let i = 0; i < 2; i++) {
    const req = makeReq({
      method: 'POST', path: '/', rawBody: payload, body: payload,
      headers: { 'stripe-signature': header },
    });
    const res = makeRes();
    const handler = findHandler(webhookRoutes, 'POST', '/');
    await handler(req, res);
    if (i === 0) assert.strictEqual(res.statusCode, 200);
    if (i === 1) {
      assert.strictEqual(res.body.duplicate, true);
    }
  }
});

// ---- Helpers for test infra ------------------------------------------

async function makeUser(email) {
  const bcrypt = require('bcryptjs');
  const hash = bcrypt.hashSync('Pass!1234', 4);
  return await new Promise((resolve, reject) => {
    const r = db.prepare(`INSERT INTO users (email, name, password_hash, role, plan, created_at, updated_at) VALUES (?, ?, ?, 'user', 'free', ?, ?) RETURNING *`)
      .get(email, 'Test', hash, Date.now(), Date.now());
    resolve(r);
  });
}

function findHandler(router, method, path) {
  for (const layer of router.stack) {
    if (!layer.route) continue;
    const methods = Object.keys(layer.route.methods).map(m => m.toUpperCase());
    if (methods.includes(method) && layer.route.path === path) {
      // Skip auth middleware; use the actual route handler (last in the stack)
      const stack = layer.route.stack;
      return stack[stack.length - 1].handle;
    }
  }
  throw new Error('No handler for ' + method + ' ' + path);
}

module.exports = TESTS;
