const request = require('supertest');

// Mock Redis
const mockRedis = { get: jest.fn(), set: jest.fn(), hset: jest.fn(), hgetall: jest.fn(), expire: jest.fn() };
jest.mock('../../src/utils/redis', () => ({ getRedisClient: () => mockRedis }));

// Mock Supabase
const mockSupabaseFrom = jest.fn();
const mockSupabaseAuth = { getUser: jest.fn() };
jest.mock('../../src/utils/supabase', () => ({
  getSupabaseClient: () => ({ auth: mockSupabaseAuth, from: mockSupabaseFrom }),
  getSupabaseServiceClient: () => ({ auth: mockSupabaseAuth, from: mockSupabaseFrom }),
}));

// Mock Stripe — singleton handle so tests can override constructEvent
// per-case. The service caches `new Stripe()` once; the mock returns
// the same instance every time so the override applies to the cached
// reference too.
const mockStripeInstance = {
  customers: {
    create: jest.fn().mockResolvedValue({ id: 'cus_test123' }),
  },
  checkout: {
    sessions: {
      create: jest.fn().mockResolvedValue({ url: 'https://checkout.stripe.com/test', id: 'cs_test' }),
    },
  },
  billingPortal: {
    sessions: {
      create: jest.fn().mockResolvedValue({ url: 'https://billing.stripe.com/test' }),
    },
  },
  subscriptions: {
    list: jest.fn().mockResolvedValue({ data: [] }),
  },
  webhooks: {
    constructEvent: jest.fn(),
  },
};
jest.mock('stripe', () => jest.fn(() => mockStripeInstance));

jest.mock('axios');
process.env.ODDS_API_KEY = 'test';
process.env.STRIPE_SECRET_KEY = 'sk_test_xxx';
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_test';
// Session 15 — PRICE_MAP was hardened to drop the fake-string
// fallbacks (would 400 from Stripe in production). The integration
// tests need real-looking Stripe price IDs in env so getPriceId
// doesn't return the unconfigured sentinel and 503 the happy path.
process.env.STRIPE_PRICE_ANALYST = process.env.STRIPE_PRICE_ANALYST || 'price_test_analyst';
process.env.STRIPE_PRICE_DESK = process.env.STRIPE_PRICE_DESK || 'price_test_desk';

const app = require('../../src/app');

const MOCK_USER = {
  id: 'user-1', email: 'test@test.com', tier: 'free',
  scan_count: 0, stripe_customer_id: null, founder_status: false,
};

function setupAuthMocks(user = MOCK_USER) {
  mockSupabaseAuth.getUser.mockResolvedValue({
    data: { user: { id: user.id, email: user.email } }, error: null,
  });
  mockSupabaseFrom.mockImplementation((table) => {
    if (table === 'users') {
      return {
        select: () => ({
          eq: () => ({
            single: () => Promise.resolve({ data: user, error: null }),
          }),
        }),
        update: () => ({
          eq: () => Promise.resolve({ error: null }),
        }),
      };
    }
    return { select: () => ({ eq: () => Promise.resolve({ data: [] }) }) };
  });
}

beforeEach(() => {
  jest.clearAllMocks();
  mockRedis.get.mockResolvedValue(null);
  mockRedis.set.mockResolvedValue('OK');
  mockRedis.hset.mockResolvedValue(1);
  mockRedis.hgetall.mockResolvedValue({});
  mockRedis.expire.mockResolvedValue(1);
});

describe('POST /api/stripe/checkout', () => {
  test('creates checkout session and returns URL', async () => {
    setupAuthMocks();
    const res = await request(app)
      .post('/api/stripe/checkout')
      .set('Authorization', 'Bearer valid-token')
      .send({ tier: 'analyst' })
      .expect(200);

    expect(res.body.checkout_url).toBeDefined();
    expect(res.body.session_id).toBeDefined();
  });

  test('returns 400 for invalid tier', async () => {
    setupAuthMocks();
    const res = await request(app)
      .post('/api/stripe/checkout')
      .set('Authorization', 'Bearer valid-token')
      .send({ tier: 'gold' })
      .expect(400);

    expect(res.body.error).toContain('tier');
  });

  test('returns 401 without auth', async () => {
    await request(app)
      .post('/api/stripe/checkout')
      .send({ tier: 'analyst' })
      .expect(401);
  });

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

    test("'africa' is now an accepted tier (validation passes)", async () => {
      setupAuthMocks();
      process.env.STRIPE_PRICE_AFRICA = 'price_africa_test';
      const res = await request(app)
        .post('/api/stripe/checkout')
        .set('Authorization', 'Bearer valid-token')
        .send({ tier: 'africa' });
      // We DON'T assert on the status here — the downstream Stripe
      // mock may produce 200 OR the test's supabase fake may take a
      // different path. The important assertion: the request didn't
      // 400 with "tier must be analyst or desk".
      expect(res.status).not.toBe(400);
    });

    test('returns 503 with code:tier_unconfigured when STRIPE_PRICE_AFRICA is unset', async () => {
      setupAuthMocks();
      delete process.env.STRIPE_PRICE_AFRICA;
      const res = await request(app)
        .post('/api/stripe/checkout')
        .set('Authorization', 'Bearer valid-token')
        .send({ tier: 'africa' })
        .expect(503);
      expect(res.body.code).toBe('tier_unconfigured');
      expect(res.body.error).toMatch(/africa/i);
    });

    test('still rejects unrelated tiers with 400', async () => {
      setupAuthMocks();
      const res = await request(app)
        .post('/api/stripe/checkout')
        .set('Authorization', 'Bearer valid-token')
        .send({ tier: 'gold' })
        .expect(400);
      expect(res.body.error).toMatch(/tier/);
    });
  });
});

describe('POST /api/stripe/portal', () => {
  test('returns portal URL for existing customer', async () => {
    setupAuthMocks({ ...MOCK_USER, stripe_customer_id: 'cus_existing' });
    const res = await request(app)
      .post('/api/stripe/portal')
      .set('Authorization', 'Bearer valid-token')
      .expect(200);

    expect(res.body.portal_url).toBeDefined();
  });

  test('returns 400 when no subscription', async () => {
    setupAuthMocks();
    const res = await request(app)
      .post('/api/stripe/portal')
      .set('Authorization', 'Bearer valid-token')
      .expect(400);

    expect(res.body.error).toContain('No active subscription');
  });
});

describe('GET /api/stripe/status', () => {
  test('returns tier and subscription info', async () => {
    setupAuthMocks({ ...MOCK_USER, tier: 'analyst', founder_status: true });
    const res = await request(app)
      .get('/api/stripe/status')
      .set('Authorization', 'Bearer valid-token')
      .expect(200);

    expect(res.body.tier).toBe('analyst');
    expect(res.body.is_founder).toBe(true);
  });
});

describe('POST /api/stripe/webhook', () => {
  test('returns 400 without signature', async () => {
    await request(app)
      .post('/api/stripe/webhook')
      .send('{}')
      .set('Content-Type', 'application/json')
      .expect(400);
  });

  test('returns 400 when signature header is present but invalid', async () => {
    // Header present, but constructEvent throws — Stripe's real behavior
    // when the signed payload doesn't match the secret. The route must
    // surface 400 and never invoke the event-dispatch path.
    mockStripeInstance.webhooks.constructEvent.mockImplementationOnce(() => {
      throw new Error('No signatures found matching the expected signature');
    });

    const res = await request(app)
      .post('/api/stripe/webhook')
      .set('Content-Type', 'application/json')
      .set('stripe-signature', 't=1700000000,v1=deadbeef')
      .send(Buffer.from('{"id":"evt_forged","type":"checkout.session.completed"}'))
      .expect(400);

    expect(res.body.error).toMatch(/signature/i);
    expect(mockStripeInstance.webhooks.constructEvent).toHaveBeenCalledTimes(1);
  });

  test('valid signature dispatches to handler and returns 200', async () => {
    // Positive case: route → service → 200 with `received: true`. We can't
    // observe the supabase write here without entangling with auth setup;
    // the unit suite (stripeService.test.js) already proves the dispatch
    // wiring per event type. This test pins the route-level contract.
    mockStripeInstance.webhooks.constructEvent.mockImplementationOnce(() => ({
      id: 'evt_ok',
      type: 'invoice.payment_failed', // chosen because it requires no users-table fixture beyond the default
      data: { object: { customer: 'cus_test123' } },
    }));

    const res = await request(app)
      .post('/api/stripe/webhook')
      .set('Content-Type', 'application/json')
      .set('stripe-signature', 't=1700000000,v1=stub')
      .send(Buffer.from('{}'))
      .expect(200);

    expect(res.body.received).toBe(true);
  });
});
