import express from 'express';
import request from 'supertest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocked = vi.hoisted(() => ({
  authMiddleware: vi.fn((req: any, _res: any, next: any) => {
    req.user = { userId: 'user-1' };
    next();
  }),
  createCheckoutSession: vi.fn(),
  verifyTransaction: vi.fn(),
  getPendingOrder: vi.fn(),
  completePendingOrder: vi.fn(),
  validateWebhookSignature: vi.fn(() => true),
  parseWebhookPayload: vi.fn(),
  createPurchase: vi.fn(),
  creditSinglePicks: vi.fn(),
  creditDailyPass: vi.fn(async () => ({ expiresAtUtc: '2026-03-29T06:00:00.000Z' })),
  creditMonthlyPass: vi.fn(),
  findUserById: vi.fn(),
  getPickBalance: vi.fn(),
  markAffiliatePurchaseTracked: vi.fn(),
  isInGracePeriod: vi.fn(() => false),
  createTouristPass: vi.fn(),
  trackAffiliateConversion: vi.fn(),
  sendPurchaseConfirmationEmail: vi.fn(() => Promise.resolve()),
  recordLedgerEntry: vi.fn(),
  poolQuery: vi.fn(),
  poolConnect: vi.fn(),
}));

vi.mock('../../middleware/auth', () => ({ authMiddleware: mocked.authMiddleware }));
vi.mock('../../services/authnet', () => ({
  createCheckoutSession: mocked.createCheckoutSession,
  verifyTransaction: mocked.verifyTransaction,
  getPendingOrder: mocked.getPendingOrder,
  completePendingOrder: mocked.completePendingOrder,
  validateWebhookSignature: mocked.validateWebhookSignature,
  parseWebhookPayload: mocked.parseWebhookPayload,
}));
vi.mock('../../models/purchase', () => ({ createPurchase: mocked.createPurchase }));
vi.mock('../../models/user', () => ({
  creditSinglePicks: mocked.creditSinglePicks,
  creditDailyPass: mocked.creditDailyPass,
  creditMonthlyPass: mocked.creditMonthlyPass,
  findUserById: mocked.findUserById,
  getPickBalance: mocked.getPickBalance,
  markAffiliatePurchaseTracked: mocked.markAffiliatePurchaseTracked,
  isInGracePeriod: mocked.isInGracePeriod,
}));
vi.mock('../../models/tourist-pass', () => ({ createTouristPass: mocked.createTouristPass }));
vi.mock('../../services/affiliate', () => ({ trackAffiliateConversion: mocked.trackAffiliateConversion }));
vi.mock('../../services/email', () => ({ sendPurchaseConfirmationEmail: mocked.sendPurchaseConfirmationEmail }));
vi.mock('../../models/ledger', () => ({ recordLedgerEntry: mocked.recordLedgerEntry }));
vi.mock('../../db', () => ({
  default: {
    query: mocked.poolQuery,
    connect: mocked.poolConnect,
  },
}));

describe('/purchase contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mocked.poolQuery.mockResolvedValue({ rows: [] });
    mocked.findUserById.mockResolvedValue({
      id: 'user-1',
      email: 'test@example.com',
      email_verified: true,
      is_weatherman: false,
      monthly_pass_expires: null,
      affiliate_tracking_id: null,
      affiliate_purchase_tracked_at: null,
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });
  });

  it('returns verified=false for unpaid transactions and does not credit forecasts', async () => {
    const { default: router } = await import('../purchase');
    mocked.verifyTransaction.mockResolvedValue({ paid: false });

    const app = express();
    app.use(express.json());
    app.use('/', router);

    const res = await request(app).post('/verify').send({ transactionId: 'txn-1', invoiceNumber: 'INV-1' });

    expect(res.status).toBe(200);
    expect(res.body).toEqual({ verified: false });
    expect(mocked.createPurchase).not.toHaveBeenCalled();
    expect(mocked.creditSinglePicks).not.toHaveBeenCalled();
    expect(mocked.creditDailyPass).not.toHaveBeenCalled();
    expect(mocked.creditMonthlyPass).not.toHaveBeenCalled();
  });

  it('uses the existing purchase record instead of crediting twice on duplicate verify callbacks', async () => {
    const { default: router } = await import('../purchase');
    mocked.verifyTransaction.mockResolvedValue({
      paid: true,
      transactionId: 'txn-1',
      invoiceNumber: 'INV-1',
      amountCents: 499,
      responseCode: '1',
    });
    mocked.getPendingOrder.mockResolvedValue({
      user_id: 'user-1',
      product_type: 'daily_pass',
      picks_granted: 999,
      status: 'pending',
    });

    const clientQuery = vi.fn(async (query: string) => {
      if (query === 'BEGIN' || query === 'COMMIT' || query === 'ROLLBACK') {
        return { rows: [] };
      }
      if (query.includes('pg_advisory_xact_lock')) {
        return { rows: [{ pg_advisory_xact_lock: null }] };
      }
      if (query.includes('SELECT * FROM rm_pending_orders')) {
        return {
          rows: [{
            invoice_number: 'INV-1',
            user_id: 'user-1',
            product_type: 'daily_pass',
            picks_granted: 999,
            amount_cents: 499,
            client_timezone: 'America/New_York',
            purchase_ip: '127.0.0.1',
            attribution: null,
            status: 'pending',
          }],
        };
      }
      if (query.includes('FROM rm_purchases')) {
        return {
          rows: [{
            id: 'purchase-1',
            user_id: 'user-1',
            product_type: 'daily_pass',
            picks_granted: 999,
          }],
        };
      }
      if (query.includes('UPDATE rm_pending_orders')) {
        return { rows: [] };
      }
      throw new Error(`Unexpected query: ${query}`);
    });

    mocked.poolConnect.mockResolvedValue({
      query: clientQuery,
      release: vi.fn(),
    });

    const app = express();
    app.use(express.json());
    app.use('/', router);

    const res = await request(app).post('/verify').send({ transactionId: 'txn-1', invoiceNumber: 'INV-1' });

    expect(res.status).toBe(200);
    expect(res.body).toEqual({ verified: true, picksGranted: 999, productType: 'daily_pass' });
    expect(mocked.createPurchase).not.toHaveBeenCalled();
    expect(mocked.creditSinglePicks).not.toHaveBeenCalled();
    expect(mocked.creditDailyPass).not.toHaveBeenCalled();
    expect(mocked.creditMonthlyPass).not.toHaveBeenCalled();
  });

  it('returns canonical and deprecated balance fields from entitlements', async () => {
    const { default: router } = await import('../purchase');
    mocked.getPickBalance.mockResolvedValue({
      single_picks: 1,
      daily_pass_picks: 4,
      daily_pass_valid: true,
      daily_free_forecasts: 3,
      tourist_pass_expires_at: '2026-03-29T06:00:00.000Z',
      tourist_pass_timezone: 'America/New_York',
      email_verified: true,
      next_reset_at: '2026-03-29T04:00:00.000Z',
    });

    const app = express();
    app.use(express.json());
    app.use('/', router);

    const res = await request(app).get('/entitlements');

    expect(res.status).toBe(200);
    expect(res.body.forecastBalance).toBe(8);
    expect(res.body.forecast_balance).toBe(8);
    expect(res.body.plan).toBe('TOURIST');
    expect(res.body.totalAvailable).toBe(8);
  });

  it('returns monthly pass expiry metadata for Rain Man users', async () => {
    const { default: router } = await import('../purchase');
    mocked.findUserById.mockResolvedValue({
      id: 'user-1',
      email: 'rain@example.com',
      email_verified: true,
      is_weatherman: true,
      monthly_pass_expires: '2026-04-15T04:00:00.000Z',
      affiliate_tracking_id: null,
      affiliate_purchase_tracked_at: null,
    });
    mocked.getPickBalance.mockResolvedValue({
      single_picks: 0,
      daily_pass_picks: 0,
      daily_pass_valid: false,
      daily_free_forecasts: 0,
      email_verified: true,
      next_reset_at: '2026-03-29T04:00:00.000Z',
      tourist_pass_expires_at: null,
      tourist_pass_timezone: null,
    });

    const app = express();
    app.use(express.json());
    app.use('/', router);

    const res = await request(app).get('/entitlements');

    expect(res.status).toBe(200);
    expect(res.body.plan).toBe('RAINMAN');
    expect(res.body.monthlyPassExpiresAt).toBe('2026-04-15T04:00:00.000Z');
    expect(res.body.monthly_pass_expires_at).toBe('2026-04-15T04:00:00.000Z');
    expect(res.body.monthlyPassAutoRenews).toBe(false);
  });
});
