import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { createHmac } from 'crypto';
import { NovasisPayWebhooksService } from './novasis-pay-webhooks.service';

const EMPRESA_ID = '00000000-0000-0000-0000-000000000001';
const MERCHANT_ID = 'mer_test_123';
const SECRET = 'whsec_test_supersecret';

function sign(rawBody: string, secret: string, tsSec = Math.floor(Date.now() / 1000)) {
  const signed = `${tsSec}.${rawBody}`;
  const hex = createHmac('sha256', secret).update(signed).digest('hex');
  return `t=${tsSec},v1=${hex}`;
}

function buildPayload(overrides: Partial<{ intentId: string; eventType: string; eventId: string }> = {}) {
  const body = {
    id: overrides.eventId ?? 'evt_1',
    event_type: overrides.eventType ?? 'payment_intent.succeeded',
    created_at: new Date().toISOString(),
    data: {
      payment_intent_id: overrides.intentId ?? 'pi_123',
      merchant_id: MERCHANT_ID,
      external_reference: null,
      amount: '50000',
      currency: 'PYG',
      country: 'PY',
      status: 'paid',
    },
  };
  return JSON.stringify(body);
}

describe('NovasisPayWebhooksService', () => {
  const createService = () => {
    const prisma = {
      novasis_pay_config: { findFirst: jest.fn() },
      novasis_pay_webhook_event_in: {
        findUnique: jest.fn(),
        create: jest.fn().mockResolvedValue({}),
      },
      novasis_pay_cobro: {
        updateMany: jest.fn().mockResolvedValue({ count: 1 }),
      },
      // El webhook también resuelve pedidos de la tienda online por intent_id.
      ecommerce_checkout_session: {
        findFirst: jest.fn().mockResolvedValue(null),
        updateMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
    };
    const ecommercePedidos = { confirmarPagoDesdeGateway: jest.fn().mockResolvedValue(undefined) };
    const service = new NovasisPayWebhooksService(prisma as any, ecommercePedidos as any);
    return { service, prisma, ecommercePedidos };
  };

  it('feliz path: firma válida + actualiza cobro a aprobado', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue(null);

    const body = buildPayload();
    const result = await service.processWebhook(body, {
      signature: sign(body, SECRET),
      eventId: 'evt_1',
      eventType: 'payment_intent.succeeded',
    });

    expect(prisma.novasis_pay_cobro.updateMany).toHaveBeenCalledWith({
      where: { empresa_id: EMPRESA_ID, intent_id: 'pi_123' },
      data: expect.objectContaining({ status: 'aprobado' }),
    });
    expect(result).toEqual({ received: true, result: 'aplicado' });
  });

  it('pago de la tienda online aprobado: confirma la sesión (crea pedido ERP + reserva stock)', async () => {
    const { service, prisma, ecommercePedidos } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue(null);
    // El checkout ecommerce no crea `novasis_pay_cobro`: el intent vive en la sesión.
    prisma.novasis_pay_cobro.updateMany.mockResolvedValue({ count: 0 });
    prisma.ecommerce_checkout_session.findFirst.mockResolvedValue({ id: 'sess-1' });

    const body = buildPayload();
    const result = await service.processWebhook(body, {
      signature: sign(body, SECRET),
      eventId: 'evt_ecom',
      eventType: 'payment_intent.succeeded',
    });

    expect(ecommercePedidos.confirmarPagoDesdeGateway).toHaveBeenCalledWith(EMPRESA_ID, 'sess-1');
    // Antes devolvía 'cobro_no_encontrado' y el pedido quedaba en pendiente_pago.
    expect(result).toEqual({ received: true, result: 'aplicado_ecommerce' });
  });

  it('intent sin cobro ni sesión ecommerce → cobro_no_encontrado', async () => {
    const { service, prisma, ecommercePedidos } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue(null);
    prisma.novasis_pay_cobro.updateMany.mockResolvedValue({ count: 0 });
    prisma.ecommerce_checkout_session.findFirst.mockResolvedValue(null);

    const body = buildPayload();
    const result = await service.processWebhook(body, {
      signature: sign(body, SECRET),
      eventId: 'evt_orphan',
      eventType: 'payment_intent.succeeded',
    });

    expect(ecommercePedidos.confirmarPagoDesdeGateway).not.toHaveBeenCalled();
    expect(result).toEqual({ received: true, result: 'cobro_no_encontrado' });
  });

  it('firma inválida → UnauthorizedException', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });

    const body = buildPayload();
    await expect(
      service.processWebhook(body, {
        signature: sign(body, 'otro_secret_distinto'),
        eventId: 'evt_2',
        eventType: 'payment_intent.succeeded',
      }),
    ).rejects.toThrow(UnauthorizedException);
    expect(prisma.novasis_pay_cobro.updateMany).not.toHaveBeenCalled();
  });

  it('firma con timestamp viejo (fuera de tolerancia) → rechaza', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });

    const body = buildPayload();
    const oldTs = Math.floor(Date.now() / 1000) - 600;
    await expect(
      service.processWebhook(body, {
        signature: sign(body, SECRET, oldTs),
        eventId: 'evt_3',
        eventType: 'payment_intent.succeeded',
      }),
    ).rejects.toThrow(UnauthorizedException);
  });

  it('dedup: event_id ya procesado devuelve duplicate sin reaplicar', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue({
      event_id: 'evt_1',
    });

    const body = buildPayload();
    const result = await service.processWebhook(body, {
      signature: sign(body, SECRET),
      eventId: 'evt_1',
      eventType: 'payment_intent.succeeded',
    });

    expect(result).toEqual({ received: true, ignored: 'duplicate' });
    expect(prisma.novasis_pay_cobro.updateMany).not.toHaveBeenCalled();
  });

  it('merchant_id desconocido → 200 con ignored merchant_not_found', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue(null);

    const body = buildPayload();
    const result = await service.processWebhook(body, {
      signature: 'irrelevante',
      eventId: 'evt_x',
      eventType: 'payment_intent.succeeded',
    });

    expect(result).toEqual({ received: true, ignored: 'merchant_not_found' });
    expect(prisma.novasis_pay_cobro.updateMany).not.toHaveBeenCalled();
  });

  it('payload sin merchant_id → BadRequest', async () => {
    const { service } = createService();
    const body = JSON.stringify({ id: 'evt_z', data: {} });
    await expect(
      service.processWebhook(body, {
        signature: 'x',
        eventId: 'evt_z',
        eventType: 'payment_intent.succeeded',
      }),
    ).rejects.toThrow(BadRequestException);
  });

  it('mapea payment_intent.failed → rechazado y .expired → expirado', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue(null);

    const bodyFail = buildPayload({ eventType: 'payment_intent.failed', eventId: 'evt_f' });
    await service.processWebhook(bodyFail, {
      signature: sign(bodyFail, SECRET),
      eventId: 'evt_f',
      eventType: 'payment_intent.failed',
    });
    expect(prisma.novasis_pay_cobro.updateMany).toHaveBeenLastCalledWith({
      where: { empresa_id: EMPRESA_ID, intent_id: 'pi_123' },
      data: expect.objectContaining({ status: 'rechazado' }),
    });

    const bodyExp = buildPayload({ eventType: 'payment_intent.expired', eventId: 'evt_e' });
    await service.processWebhook(bodyExp, {
      signature: sign(bodyExp, SECRET),
      eventId: 'evt_e',
      eventType: 'payment_intent.expired',
    });
    expect(prisma.novasis_pay_cobro.updateMany).toHaveBeenLastCalledWith({
      where: { empresa_id: EMPRESA_ID, intent_id: 'pi_123' },
      data: expect.objectContaining({ status: 'expirado' }),
    });
  });

  it('event_type ignorado (payment_intent.created) → no toca cobros, result=evento_ignorado', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_config.findFirst.mockResolvedValue({
      empresa_id: EMPRESA_ID,
      merchant_id: MERCHANT_ID,
      webhook_secret: SECRET,
    });
    prisma.novasis_pay_webhook_event_in.findUnique.mockResolvedValue(null);

    const body = buildPayload({ eventType: 'payment_intent.created', eventId: 'evt_c' });
    const result = await service.processWebhook(body, {
      signature: sign(body, SECRET),
      eventId: 'evt_c',
      eventType: 'payment_intent.created',
    });

    expect(prisma.novasis_pay_cobro.updateMany).not.toHaveBeenCalled();
    expect(result).toEqual({ received: true, result: 'evento_ignorado' });
  });
});
