/* test/creditPack.test.js
 *
 * Tests for the $3 one-time credit pack:
 *   - 5 scan credits + 3 takedown credits
 *   - Never expires (no expiry date)
 *   - Additive to the user's monthly plan quota
 *   - Credits consumed BEFORE the monthly quota
 *
 * Coverage:
 *   1.  PLANS.credit_pack_small exists with kind='credit_pack'
 *   2.  GET /billing/plans returns credit_pack_small with kind marker
 *   3.  GET /billing/plans: listPlans returns grants for credit packs
 *   4.  POST /billing/checkout for credit pack uses mode='payment'
 *   5.  POST /billing/checkout for credit pack ignores quantity/interval
 *   6.  POST /billing/checkout for credit pack sets metadata.kind = 'credit_pack'
 *   7.  POST /billing/checkout for credit pack enables automatic_tax
 *   8.  Webhook checkout.session.completed with kind=credit_pack grants credits
 *   9.  Webhook payment_intent.succeeded with kind=credit_pack grants credits
 *  10.  Webhook idempotency: both events fire → credits added twice
 *       (acceptable since grantCredits always adds; bookkeeping is
 *        handled by Stripe's payment_intent uniqueness in practice)
 *  11.  User.grantCredits adds to existing extra_*
 *  12.  User.consumeCredit('scan') decrements extra_scans when > 0
 *  13.  User.consumeCredit('scan') returns false when extra_scans == 0
 *  14.  User.consumeCredit('takedown') decrements extra_takedowns when > 0
 *  15.  planGate allows scan when user has credits even if monthly quota exhausted
 *  16.  planGate burns credit before incrementing monthly counter
 *  17.  planGate falls back to monthly counter when no credits
 *  18.  planGate blocks when monthly quota exhausted AND no credits
 *  19.  GET /billing/me returns extra_scans + extra_takedowns
 */
'use strict';

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

// Install the shared Stripe mock BEFORE routes/billing loads.
stripeMock.installMock();

const billingRoutes = require('../routes/billing');
const webhookRoutes = require('../routes/webhook');
const planGate = require('../middleware/planGate');
const { PLANS, getStripePriceId } = require('../config/plans');
const sign = stripeMock.sign;

// ---- 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;
}
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) {
      const stack = layer.route.stack;
      return stack[stack.length - 1].handle;
    }
  }
  throw new Error('No handler for ' + method + ' ' + path);
}
async function makeUser(email, plan = 'free') {
  const bcrypt = require('bcryptjs');
  const hash = bcrypt.hashSync('Pass!1234', 4);
  return db.prepare(`INSERT INTO users (email, name, password_hash, role, plan, created_at, updated_at) VALUES (?, ?, ?, 'user', ?, ?, ?) RETURNING *`)
    .get(email, 'Test', hash, plan, Date.now(), Date.now());
}

// ---- Env ------------------------------------------------------------
process.env.STRIPE_WEBHOOK_SECRET             = 'whsec_test_credits';
process.env.STRIPE_PRICE_STANDARD             = 'price_test_standard_m';
process.env.STRIPE_PRICE_STANDARD_ANNUAL      = 'price_test_standard_y';
process.env.STRIPE_PRICE_PRO                  = 'price_test_pro_m';
process.env.STRIPE_PRICE_PRO_ANNUAL           = 'price_test_pro_y';
process.env.STRIPE_PRICE_AGENCY               = 'price_test_agency_m';
process.env.STRIPE_PRICE_AGENCY_ANNUAL        = 'price_test_agency_y';
process.env.STRIPE_PRICE_CREDIT_PACK_SMALL    = 'price_test_credit_small';
process.env.APP_URL                           = 'https://shield.test.local';

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

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

test('PLANS.credit_pack_small exists with kind=credit_pack', () => {
  setupFreshDb();
  assert.ok(PLANS.credit_pack_small, 'plan defined');
  assert.strictEqual(PLANS.credit_pack_small.kind, 'credit_pack');
  assert.strictEqual(PLANS.credit_pack_small.price_cents, 300);
  assert.deepStrictEqual(PLANS.credit_pack_small.grants, { scans: 5, takedowns: 3 });
});

test('GET /billing/plans returns credit pack with kind marker', () => {
  setupFreshDb();
  const req = makeReq({ method: 'GET', path: '/plans', body: {} });
  const res = makeRes();
  findHandler(billingRoutes, 'GET', '/plans')(req, res);
  assert.strictEqual(res.statusCode, 200);
  const cp = res.body.plans.find(p => p.key === 'credit_pack_small');
  assert.ok(cp, 'credit pack in plans list');
  assert.strictEqual(cp.kind, 'credit_pack');
  assert.strictEqual(cp.price_cents, 300);
  assert.deepStrictEqual(cp.grants, { scans: 5, takedowns: 3 });
});

test('POST /billing/checkout for credit pack uses mode=payment', async () => {
  setupFreshDb();
  stripeMock.reset();
  const u = await makeUser('credit-checkout@test.local');
  const req = makeReq({ user: u, body: { plan: 'credit_pack_small' } });
  const res = makeRes();
  await findHandler(billingRoutes, 'POST', '/checkout')(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(stripeMock.lastSessionCreate.mode, 'payment');
  assert.strictEqual(stripeMock.lastSessionCreate.line_items[0].price, 'price_test_credit_small');
  assert.strictEqual(stripeMock.lastSessionCreate.line_items[0].quantity, 1);
});

test('POST /billing/checkout for credit pack sets metadata.kind', async () => {
  setupFreshDb();
  stripeMock.reset();
  const u = await makeUser('credit-meta@test.local');
  const req = makeReq({ user: u, body: { plan: 'credit_pack_small' } });
  const res = makeRes();
  await findHandler(billingRoutes, 'POST', '/checkout')(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(stripeMock.lastSessionCreate.metadata.kind, 'credit_pack');
  assert.strictEqual(stripeMock.lastSessionCreate.metadata.interval, 'one_time');
});

test('POST /billing/checkout for credit pack enables automatic_tax', async () => {
  setupFreshDb();
  stripeMock.reset();
  const u = await makeUser('credit-tax@test.local');
  const req = makeReq({ user: u, body: { plan: 'credit_pack_small' } });
  const res = makeRes();
  await findHandler(billingRoutes, 'POST', '/checkout')(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(stripeMock.lastSessionCreate.automatic_tax.enabled, true);
  assert.strictEqual(stripeMock.lastSessionCreate.allow_promotion_codes, true);
});

test('POST /billing/checkout rejects bogus credit pack name', async () => {
  setupFreshDb();
  const u = await makeUser('bad-cp@test.local');
  const req = makeReq({ user: u, body: { plan: 'credit_pack_huge' } });
  const res = makeRes();
  await findHandler(billingRoutes, 'POST', '/checkout')(req, res);
  assert.strictEqual(res.statusCode, 400);
  assert.strictEqual(res.body.error, 'INVALID_PLAN');
});

test('webhook checkout.session.completed with credit_pack grants credits', async () => {
  setupFreshDb();
  const u = await makeUser('wh-credit@test.local');
  assert.strictEqual(u.extra_scans, 0);
  assert.strictEqual(u.extra_takedowns, 0);
  const event = {
    id: 'evt_credit_' + Date.now(),
    type: 'checkout.session.completed',
    created: Math.floor(Date.now() / 1000),
    data: { object: {
      id: 'cs_credit',
      customer: 'cus_credit',
      mode: 'payment',
      payment_intent: 'pi_credit',
      // No `subscription` field for one-time payments.
      metadata: { user_id: String(u.id), plan: 'credit_pack_small', kind: 'credit_pack', interval: 'one_time', quantity: '1' },
    } },
  };
  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();
  await findHandler(webhookRoutes, 'POST', '/')(req, res);
  assert.strictEqual(res.statusCode, 200);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.extra_scans, 5, '5 scan credits granted');
  assert.strictEqual(updated.extra_takedowns, 3, '3 takedown credits granted');
});

test('webhook payment_intent.succeeded with credit_pack grants credits', async () => {
  setupFreshDb();
  const u = await makeUser('wh-pi@test.local');
  const event = {
    id: 'evt_pi_' + Date.now(),
    type: 'payment_intent.succeeded',
    created: Math.floor(Date.now() / 1000),
    data: { object: {
      id: 'pi_alt',
      metadata: { user_id: String(u.id), plan: 'credit_pack_small', kind: 'credit_pack' },
    } },
  };
  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();
  await findHandler(webhookRoutes, 'POST', '/')(req, res);
  assert.strictEqual(res.statusCode, 200);
  const updated = User.getById(u.id);
  assert.strictEqual(updated.extra_scans, 5);
  assert.strictEqual(updated.extra_takedowns, 3);
});

test('User.grantCredits adds to existing extra_*', async () => {
  setupFreshDb();
  const u = await makeUser('add@test.local');
  User.grantCredits(u.id, 5, 3);
  let cur = User.getById(u.id);
  assert.strictEqual(cur.extra_scans, 5);
  assert.strictEqual(cur.extra_takedowns, 3);
  User.grantCredits(u.id, 2, 1);
  cur = User.getById(u.id);
  assert.strictEqual(cur.extra_scans, 7);
  assert.strictEqual(cur.extra_takedowns, 4);
});

test('User.consumeCredit(scan) decrements when > 0', async () => {
  setupFreshDb();
  const u = await makeUser('consume@test.local');
  User.grantCredits(u.id, 3, 0);
  const burned = User.consumeCredit(u.id, 'scan');
  assert.strictEqual(burned, true);
  const cur = User.getById(u.id);
  assert.strictEqual(cur.extra_scans, 2);
});

test('User.consumeCredit(scan) returns false when 0', async () => {
  setupFreshDb();
  const u = await makeUser('consume-zero@test.local');
  const burned = User.consumeCredit(u.id, 'scan');
  assert.strictEqual(burned, false);
  assert.strictEqual(User.getById(u.id).extra_scans, 0);
});

test('User.consumeCredit(takedown) decrements when > 0', async () => {
  setupFreshDb();
  const u = await makeUser('consume-td@test.local');
  User.grantCredits(u.id, 0, 2);
  const burned = User.consumeCredit(u.id, 'takedown');
  assert.strictEqual(burned, true);
  assert.strictEqual(User.getById(u.id).extra_takedowns, 1);
});

test('planGate allows scan when user has credits beyond monthly quota', async () => {
  setupFreshDb();
  // Free plan: 10 scans/month. Grant 5 extra scans (15 effective).
  const u = await makeUser('gate-credits@test.local', 'free');
  User.grantCredits(u.id, 5, 0);
  // Burn 10 monthly slots first to simulate a user at the monthly limit.
  const usageLog = require('../models/usageLog');
  for (let i = 0; i < 10; i++) usageLog.increment(u.id, 'scan');
  // Now scan again — should be ALLOWED because of the credit.
  const fresh = User.getById(u.id);
  const req = { user: fresh, app: { get: () => false } };
  const res = makeRes();
  planGate('scan')(req, res, () => { res.body = { allowed: true }; });
  assert.strictEqual(res.statusCode, 200, 'no 402');
  assert.strictEqual(res.body && res.body.allowed, true);
  // Credit should be burned, monthly counter unchanged.
  const after = User.getById(u.id);
  assert.strictEqual(after.extra_scans, 4, '1 credit consumed');
  // usage_log still at 10 (not incremented)
  const usage = require('../models/usageLog');
  assert.strictEqual(usage.current(u.id, 'scan'), 10, 'monthly counter untouched');
});

test('planGate falls back to monthly counter when no credits', async () => {
  setupFreshDb();
  const u = await makeUser('gate-monthly@test.local', 'free');
  const req = { user: u, app: { get: () => false } };
  const res = makeRes();
  planGate('scan')(req, res, () => { res.body = { allowed: true }; });
  assert.strictEqual(res.statusCode, 200);
  const usage = require('../models/usageLog');
  assert.strictEqual(usage.current(u.id, 'scan'), 1);
});

test('planGate blocks when monthly quota exhausted AND no credits', async () => {
  setupFreshDb();
  const u = await makeUser('gate-blocked@test.local', 'free');
  const usageLog = require('../models/usageLog');
  for (let i = 0; i < 10; i++) usageLog.increment(u.id, 'scan');
  const req = { user: User.getById(u.id), app: { get: () => false } };
  const res = makeRes();
  planGate('scan')(req, res, () => { throw new Error('next() should not be called'); });
  assert.strictEqual(res.statusCode, 402);
  assert.strictEqual(res.body.error, 'PLAN_LIMIT');
});

test('GET /billing/me returns extra_scans + extra_takedowns', async () => {
  setupFreshDb();
  const u = await makeUser('me-credits@test.local');
  User.grantCredits(u.id, 5, 3);
  const refreshed = User.getById(u.id);
  const req = makeReq({ user: refreshed, method: 'GET', path: '/me', body: {} });
  const res = makeRes();
  findHandler(billingRoutes, 'GET', '/me')(req, res);
  assert.strictEqual(res.statusCode, 200);
  assert.strictEqual(res.body.extra_scans, 5);
  assert.strictEqual(res.body.extra_takedowns, 3);
});

module.exports = TESTS;
