/**
 * DC-055 + DC-057 billing/stripe-client tests.
 *
 * Strategy: inject a mock Stripe SDK via _setStripeSdk so no real network
 * calls ever happen. Cover the key behaviors of the one-time payment flow:
 *
 *  1. Configuration validation — missing STRIPE_SECRET_KEY fails loudly with 503.
 *  2. productId validation — unknown productId returns 400 INVALID_PRODUCT_ID.
 *  3. Product not configured — Stripe Price ID env var unset returns 503.
 *  4. Happy path — creates a session with mode:payment + correct price ID + URLs.
 *  5. Stripe SDK errors — surface as 502 to the customer, not 500.
 *  6. Metadata contract — emits metadata.productId that the bridge can read back.
 *  7. payment_intent_data — also carries productId metadata for downstream consumers.
 *  8. Catalog drives everything — _resolveProduct reads the catalog, not env.
 */

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

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];
}

function makeMockStripe(sessionsCreateImpl) {
  const sessions = { create: jest.fn().mockImplementation(sessionsCreateImpl) };
  return jest.fn().mockReturnValue({ checkout: { sessions } });
}

describe('billing/stripe-client', () => {
  afterEach(() => {
    clearEnv();
    stripeClient._setStripeSdk(null);
    jest.restoreAllMocks();
  });

  test('throws STRIPE_NOT_CONFIGURED when STRIPE_SECRET_KEY is missing', async () => {
    setEnv({ STRIPE_SECRET_KEY: '' });
    await expect(
      stripeClient.createCheckoutSession({ productId: 'pro-30d', origin: 'https://status.sami' })
    ).rejects.toMatchObject({
      code: 'STRIPE_NOT_CONFIGURED',
      statusCode: 503,
      missing: expect.arrayContaining(['STRIPE_SECRET_KEY']),
    });
  });

  test('throws STRIPE_NOT_CONFIGURED when 30d product Stripe Price ID is missing', async () => {
    setEnv({ STRIPE_PRICE_PRO_30D: '' });
    await expect(
      stripeClient.createCheckoutSession({ productId: 'pro-30d', origin: 'https://status.sami' })
    ).rejects.toMatchObject({
      code: 'STRIPE_NOT_CONFIGURED',
      missing: expect.arrayContaining(['STRIPE_PRICE_PRO_30D']),
      productId: 'pro-30d',
    });
  });

  test('throws INVALID_PRODUCT_ID when productId is missing', async () => {
    setEnv();
    await expect(
      stripeClient.createCheckoutSession({ productId: '', origin: 'https://status.sami' })
    ).rejects.toMatchObject({ code: 'INVALID_PRODUCT_ID', statusCode: 400, field: 'productId' });
  });

  test('throws INVALID_PRODUCT_ID when productId is unknown', async () => {
    setEnv();
    await expect(
      stripeClient.createCheckoutSession({ productId: 'pro-1000d', origin: 'https://status.sami' })
    ).rejects.toMatchObject({
      code: 'INVALID_PRODUCT_ID',
      statusCode: 400,
      field: 'productId',
    });
  });

  test('happy path: pro-30d creates session with mode=payment + correct params', async () => {
    setEnv();
    const mockSession = { id: 'cs_test_abc123', url: 'https://checkout.stripe.com/c/pay/cs_test_abc123' };
    const mockStripe = makeMockStripe(async (params) => {
      // DC-057: one-time payment, NOT subscription.
      expect(params.mode).toBe('payment');
      expect(params.line_items).toEqual([{ price: 'price_30d_test', quantity: 1 }]);
      expect(params.success_url).toBe('https://status.sami/billing/success?session_id={CHECKOUT_SESSION_ID}');
      expect(params.cancel_url).toBe('https://status.sami/pricing');
      // The bridge reads this metadata back to map session → product → duration.
      expect(params.metadata).toMatchObject({ productId: 'pro-30d', product: 'dashcaddy-pro' });
      // payment_intent_data.metadata mirrors it for downstream Stripe→bridge consumers.
      expect(params.payment_intent_data).toBeDefined();
      expect(params.payment_intent_data.metadata).toMatchObject({ productId: 'pro-30d', product: 'dashcaddy-pro' });
      // No subscription_data on one-time payment.
      expect(params.subscription_data).toBeUndefined();
      return mockSession;
    });
    stripeClient._setStripeSdk(mockStripe);

    const result = await stripeClient.createCheckoutSession({
      productId: 'pro-30d',
      origin: 'https://status.sami',
    });
    expect(result).toEqual({ id: 'cs_test_abc123', url: mockSession.url });
    expect(mockStripe).toHaveBeenCalledWith('«redacted:sk_test_…»');
  });

  test('happy path: pro-365d uses 365d price ID', async () => {
    setEnv();
    const mockStripe = makeMockStripe(async (params) => {
      expect(params.line_items[0].price).toBe('price_365d_test');
      expect(params.metadata.productId).toBe('pro-365d');
      return { id: 'cs_365_xyz', url: 'https://checkout.stripe.com/c/pay/cs_365_xyz' };
    });
    stripeClient._setStripeSdk(mockStripe);

    const result = await stripeClient.createCheckoutSession({
      productId: 'pro-365d',
      origin: 'https://status.sami',
    });
    expect(result.id).toBe('cs_365_xyz');
  });

  test('forwards customerEmail when provided', async () => {
    setEnv();
    const mockStripe = makeMockStripe(async (params) => {
      expect(params.customer_email).toBe('alice@example.com');
      return { id: 'cs_emailed', url: 'https://checkout.stripe.com/c/pay/cs_emailed' };
    });
    stripeClient._setStripeSdk(mockStripe);

    await stripeClient.createCheckoutSession({
      productId: 'pro-90d',
      customerEmail: 'alice@example.com',
      origin: 'https://status.sami',
    });
  });

  test('omits customer_email when not provided (no undefined leakage to Stripe)', async () => {
    setEnv();
    const mockStripe = makeMockStripe(async (params) => {
      expect('customer_email' in params).toBe(false);
      return { id: 'cs_no_email', url: 'https://checkout.stripe.com/c/pay/cs_no_email' };
    });
    stripeClient._setStripeSdk(mockStripe);

    await stripeClient.createCheckoutSession({
      productId: 'pro-30d',
      origin: 'https://status.sami',
    });
  });

  test('uses STRIPE_SUCCESS_URL override when set', async () => {
    setEnv({ STRIPE_SUCCESS_URL: 'https://custom.example.com/thanks' });
    const mockStripe = makeMockStripe(async (params) => {
      expect(params.success_url).toBe('https://custom.example.com/thanks');
      return { id: 'cs_custom', url: 'x' };
    });
    stripeClient._setStripeSdk(mockStripe);

    await stripeClient.createCheckoutSession({ productId: 'pro-30d', origin: 'https://status.sami' });
  });

  test('uses STRIPE_CANCEL_URL override when set', async () => {
    setEnv({ STRIPE_CANCEL_URL: 'https://custom.example.com/back' });
    const mockStripe = makeMockStripe(async (params) => {
      expect(params.cancel_url).toBe('https://custom.example.com/back');
      return { id: 'cs_cancel', url: 'x' };
    });
    stripeClient._setStripeSdk(mockStripe);

    await stripeClient.createCheckoutSession({ productId: 'pro-30d', origin: 'https://status.sami' });
  });

  test('works with relative origin (no host header)', async () => {
    setEnv();
    const mockStripe = makeMockStripe(async () => ({ id: 'x', url: 'x' }));
    stripeClient._setStripeSdk(mockStripe);

    const result = await stripeClient.createCheckoutSession({ productId: 'pro-30d' });
    expect(result.id).toBe('x');
  });

  test('each catalog product drives a different price ID', async () => {
    setEnv();
    for (const product of catalog.PRODUCTS) {
      const mockStripe = makeMockStripe(async (params) => {
        expect(params.line_items[0].price).toBe(REQUIRED_ENV[product.priceEnv]);
        expect(params.metadata.productId).toBe(product.id);
        return { id: `cs_${product.id}`, url: 'x' };
      });
      stripeClient._setStripeSdk(mockStripe);
      await stripeClient.createCheckoutSession({ productId: product.id, origin: 'https://status.sami' });
    }
  });
});

describe('billing/stripe-client — _resolveProduct unit', () => {
  test('resolves known productId with configured price', () => {
    setEnv();
    const result = stripeClient._resolveProduct('pro-30d');
    expect(result.product.id).toBe('pro-30d');
    expect(result.priceId).toBe('price_30d_test');
  });

  test('returns INVALID_PRODUCT_ID error for unknown productId', () => {
    setEnv();
    expect(() => stripeClient._resolveProduct('pro-1000d')).toThrow();
    try { stripeClient._resolveProduct('pro-1000d'); } catch (e) {
      expect(e.code).toBe('INVALID_PRODUCT_ID');
      expect(e.statusCode).toBe(400);
    }
  });

  test('returns STRIPE_NOT_CONFIGURED error when product price is unset', () => {
    setEnv({ STRIPE_PRICE_PRO_180D: '' });
    try { stripeClient._resolveProduct('pro-180d'); } catch (e) {
      expect(e.code).toBe('STRIPE_NOT_CONFIGURED');
      expect(e.statusCode).toBe(503);
      expect(e.missing).toContain('STRIPE_PRICE_PRO_180D');
    }
  });
});
