process.env.STRIPE_SECRET_KEY = 'sk_test_dummy';
// Session 15 — the production fallback strings ('price_analyst_monthly'
// etc.) were dropped because they'd 400 from Stripe in live mode. Tests
// that assert getPriceId returns a string containing 'analyst' /
// 'founder' must now provide the env values BEFORE requiring the
// module (PRICE_MAP is frozen at require time).
process.env.STRIPE_PRICE_ANALYST = 'price_test_analyst_monthly';
process.env.STRIPE_PRICE_ANALYST_FOUNDER = 'price_test_analyst_founder';
process.env.STRIPE_PRICE_DESK = 'price_test_desk_monthly';
process.env.STRIPE_PRICE_DESK_FOUNDER = 'price_test_desk_founder';

// Default mock for the founder-code / price-id tests (no DB interaction).
// Webhook tests below replace the implementation per-test.
const mockSupabaseClient = { current: { from: jest.fn() } };
jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => mockSupabaseClient.current,
}));

const {
  isFounderCodeValid, getPriceId, handleWebhookEvent,
  resolveCheckoutPrice, founderSeatsAvailable, FOUNDER_SEATS_TOTAL, PAYMENT_RETRY_GRACE_MS,
} = require('../../src/services/stripeService');

describe('stripeService', () => {
  describe('isFounderCodeValid', () => {
    test('valid founder code returns true', () => {
      expect(isFounderCodeValid('FOUNDER2026')).toBe(true);
      expect(isFounderCodeValid('VYNDR')).toBe(true);
      expect(isFounderCodeValid('BETONBLK')).toBe(true); // legacy promo, still honored
    });

    test('case insensitive', () => {
      expect(isFounderCodeValid('founder2026')).toBe(true);
    });

    test('invalid code returns false', () => {
      expect(isFounderCodeValid('INVALID')).toBe(false);
    });

    test('null/empty returns false', () => {
      expect(isFounderCodeValid(null)).toBe(false);
      expect(isFounderCodeValid('')).toBe(false);
    });
  });

  // Item B — checkout price is SEAT-GATED (founder while < FOUNDER_SEATS_TOTAL,
  // standard when sold out), reading the same countFounderSeats() truth as the
  // meter. Inject the seat count so we test both ends without hitting Stripe.
  describe('resolveCheckoutPrice — seat-gated founder pricing', () => {
    const seats = (n) => ({ countFounderSeats: async () => n });

    test('seat count 0 → founder price (the advertised $34.99 desk)', async () => {
      const a = await resolveCheckoutPrice('analyst', seats(0));
      expect(a.priceId).toBe('price_test_analyst_founder');
      expect(a.isFounder).toBe(true);
      const d = await resolveCheckoutPrice('desk', seats(0));
      expect(d.priceId).toBe('price_test_desk_founder');
      expect(d.isFounder).toBe(true);
    });

    test('seat count 100 (SOLD OUT) → standard price, isFounder false', async () => {
      const a = await resolveCheckoutPrice('analyst', seats(FOUNDER_SEATS_TOTAL));
      expect(a.priceId).toBe('price_test_analyst_monthly');
      expect(a.isFounder).toBe(false);
      const d = await resolveCheckoutPrice('desk', seats(FOUNDER_SEATS_TOTAL));
      expect(d.priceId).toBe('price_test_desk_monthly');
      expect(d.isFounder).toBe(false);
    });

    test('seat 99 is still founder, seat 100 flips (the boundary)', async () => {
      expect((await resolveCheckoutPrice('desk', seats(99))).isFounder).toBe(true);
      expect((await resolveCheckoutPrice('desk', seats(100))).isFounder).toBe(false);
    });

    test('count unverifiable (null) → honor the advertised founder price, never overcharge', async () => {
      const d = await resolveCheckoutPrice('desk', { countFounderSeats: async () => null });
      expect(d.priceId).toBe('price_test_desk_founder');
      expect(d.isFounder).toBe(true);
    });

    test('the gate reads countFounderSeats truth (same source as the meter)', async () => {
      expect(await founderSeatsAvailable(seats(0))).toBe(true);
      expect(await founderSeatsAvailable(seats(FOUNDER_SEATS_TOTAL))).toBe(false);
    });
  });

  describe('grace period — a decline spans Stripe retries (item B)', () => {
    test('payment_failed grace is 14 days (Stripe Smart Retry window), not 48h', () => {
      expect(PAYMENT_RETRY_GRACE_MS).toBe(14 * 24 * 60 * 60 * 1000);
    });
  });

  describe('getPriceId', () => {
    test('analyst without founder code returns standard price', () => {
      const id = getPriceId('analyst', null);
      expect(id).toContain('analyst');
      expect(id).not.toContain('founder');
    });

    test('analyst with valid founder code returns founder price', () => {
      const id = getPriceId('analyst', 'FOUNDER2026');
      expect(id).toContain('founder');
    });

    test('desk without founder code returns standard price', () => {
      const id = getPriceId('desk', null);
      expect(id).toContain('desk');
      expect(id).not.toContain('founder');
    });

    test('desk with valid founder code returns founder price', () => {
      const id = getPriceId('desk', 'BETONBLK');
      expect(id).toContain('founder');
    });

    test('invalid tier throws', () => {
      expect(() => getPriceId('gold', null)).toThrow('Invalid tier');
    });

    describe('Session 14 — africa tier', () => {
      const original = process.env.STRIPE_PRICE_AFRICA;
      afterAll(() => {
        if (original == null) delete process.env.STRIPE_PRICE_AFRICA;
        else process.env.STRIPE_PRICE_AFRICA = original;
      });

      test('africa returns the configured price ID when set', () => {
        // We can't re-import to pick up the env change after the
        // module loaded its PRICE_MAP at require-time, so this test
        // asserts the contract: getPriceId('africa') returns either
        // a price ID OR the sentinel. The route-layer integration
        // test covers the env-flip → 503 path end-to-end.
        const result = getPriceId('africa', null);
        expect(typeof result).toBe('string');
      });

      test("africa never returns a founder-discounted variant (the tier IS the discount)", () => {
        const a = getPriceId('africa', null);
        const b = getPriceId('africa', 'FOUNDER2026');
        expect(a).toBe(b);
      });

      test('exports PRICE_UNCONFIGURED sentinel', () => {
        const { PRICE_UNCONFIGURED } = require('../../src/services/stripeService');
        expect(typeof PRICE_UNCONFIGURED).toBe('string');
        expect(PRICE_UNCONFIGURED.length).toBeGreaterThan(0);
      });
    });
  });

  describe('handleWebhookEvent', () => {
    // A chainable supabase fake whose final-chain return is configurable per call.
    // Records every update payload so tests can assert grace_period_until etc.
    function makeFake({ findUserById = 'user-1' } = {}) {
      const updates = [];
      const fake = {
        updates,
        from(table) {
          const ctx = { table, filters: [] };
          const proxy = {
            update(patch) {
              ctx.patch = patch;
              ctx.action = 'update';
              updates.push(ctx);
              return proxy;
            },
            select() {
              ctx.action = 'select';
              return proxy;
            },
            eq(col, val) {
              ctx.filters.push([col, val]);
              if (ctx.action === 'update') return Promise.resolve({ error: null });
              return proxy;
            },
            single() {
              return Promise.resolve({ data: findUserById ? { id: findUserById } : null });
            },
          };
          return proxy;
        },
      };
      return fake;
    }

    test('checkout.session.completed updates users + mirrors to user_profiles, clears grace', async () => {
      const fake = makeFake();
      mockSupabaseClient.current = fake;
      await handleWebhookEvent({
        type: 'checkout.session.completed',
        data: { object: { metadata: { user_id: 'u1', tier: 'analyst', is_founder: 'true' }, customer: 'cus_1' } },
      });
      const usersUpdate = fake.updates.find((u) => u.table === 'users');
      const profilesUpdate = fake.updates.find((u) => u.table === 'user_profiles');
      expect(usersUpdate.patch.tier).toBe('analyst');
      expect(usersUpdate.patch.grace_period_until).toBeNull();
      expect(usersUpdate.patch.mfa_setup_prompted).toBe(false);
      expect(profilesUpdate.patch.tier).toBe('analyst');
      expect(profilesUpdate.patch.subscription_status).toBe('active');
      expect(profilesUpdate.patch.founder_pricing).toBe(true);
    });

    test('invoice.payment_failed sets a 14-DAY grace (spans Stripe retries, item B)', async () => {
      const fake = makeFake();
      mockSupabaseClient.current = fake;
      const before = Date.now();
      await handleWebhookEvent({
        type: 'invoice.payment_failed',
        data: { object: { customer: 'cus_2' } },
      });
      const usersUpdate = fake.updates.find((u) => u.table === 'users');
      const profilesUpdate = fake.updates.find((u) => u.table === 'user_profiles');
      const graceTs = new Date(usersUpdate.patch.grace_period_until).getTime();
      const expected = before + 14 * 24 * 60 * 60 * 1000; // a decline is not a cancel
      expect(Math.abs(graceTs - expected)).toBeLessThan(60_000); // within a minute of 14d
      expect(profilesUpdate.patch.subscription_status).toBe('grace_period');
    });

    test('customer.subscription.updated active → flips tier to the new plan + clears grace', async () => {
      // Plan-change flow (portal-driven upgrade/downgrade). Stripe sends
      // the new price on items.data[0].price.id; the service must map it
      // back to a tier via PRICE_MAP and reflect that on the user.
      const fake = makeFake();
      mockSupabaseClient.current = fake;
      const newPriceId = process.env.STRIPE_PRICE_DESK || 'price_desk_monthly';
      await handleWebhookEvent({
        type: 'customer.subscription.updated',
        data: {
          object: {
            customer: 'cus_active',
            status: 'active',
            items: { data: [{ price: { id: newPriceId } }] },
          },
        },
      });
      const usersUpdate = fake.updates.find((u) => u.table === 'users');
      const profilesUpdate = fake.updates.find((u) => u.table === 'user_profiles');
      expect(usersUpdate.patch.tier).toBe('desk');
      expect(usersUpdate.patch.grace_period_until).toBeNull();
      expect(profilesUpdate.patch.tier).toBe('desk');
      expect(profilesUpdate.patch.subscription_status).toBe('active');
    });

    test('customer.subscription.deleted sets grace, does not flip tier immediately', async () => {
      const fake = makeFake();
      mockSupabaseClient.current = fake;
      await handleWebhookEvent({
        type: 'customer.subscription.deleted',
        data: { object: { customer: 'cus_3' } },
      });
      const usersUpdate = fake.updates.find((u) => u.table === 'users');
      expect(usersUpdate.patch.grace_period_until).toBeTruthy();
      // tier is intentionally NOT downgraded here — the grace period gate
      // handles read-time enforcement.
      expect(usersUpdate.patch.tier).toBeUndefined();
    });

    test('payment_failed with unknown customer logs but does not throw', async () => {
      const fake = makeFake({ findUserById: null });
      mockSupabaseClient.current = fake;
      await expect(
        handleWebhookEvent({ type: 'invoice.payment_failed', data: { object: { customer: 'cus_ghost' } } })
      ).resolves.toBeUndefined();
    });
  });
});
