import { NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { NovasisPayCobrosService } from './novasis-pay-cobros.service';

const EMPRESA_ID = '00000000-0000-0000-0000-000000000001';
const USUARIO_ID = '00000000-0000-0000-0000-000000000002';

describe('NovasisPayCobrosService', () => {
  const createService = () => {
    const prisma = {
      novasis_pay_cobro: {
        findUnique: jest.fn(),
        findFirst: jest.fn(),
        findMany: jest.fn(),
        create: jest.fn(),
      },
      novasis_pay_customer_ref: {
        findFirst: jest.fn().mockResolvedValue(null),
        create: jest.fn().mockResolvedValue({}),
      },
    };
    const client = {
      createPaymentIntent: jest.fn(),
      createCheckoutSession: jest.fn(),
      createCustomer: jest.fn(),
      findCustomerByEmailOrDoc: jest.fn().mockResolvedValue(null),
    };
    const config = {
      requireActiveConfig: jest.fn().mockResolvedValue({
        apiBaseUrl: 'https://gw/v1',
        checkoutBaseUrl: 'https://co',
        secretKey: 'sk_test_xxx',
        defaultProvider: 'dlocal',
        defaultCountry: 'PY',
        mode: 'test',
      }),
    };
    const service = new NovasisPayCobrosService(prisma as any, client as any, config as any);
    return { service, prisma, client, config };
  };

  it('crea cobro feliz path: pasarela ok + persistencia', async () => {
    const { service, prisma, client } = createService();

    client.createPaymentIntent.mockResolvedValue({
      id: 'pi_123',
      merchantId: 'mer_x',
      status: 'created',
      amount: '50000',
      currency: 'PYG',
      country: 'PY',
    });
    client.createCheckoutSession.mockResolvedValue({
      id: 'cs_123',
      intentId: 'pi_123',
      publicToken: 'tok_xxx',
      url: 'https://co/c/tok_xxx',
      status: 'open',
      expiresAt: new Date().toISOString(),
    });
    prisma.novasis_pay_cobro.create.mockResolvedValue({
      id: 'cob_1',
      empresa_id: EMPRESA_ID,
      monto: new Prisma.Decimal(50000),
      currency: 'PYG',
      country: 'PY',
      descripcion: 'Test',
      comprador_nombre: null,
      comprador_email: null,
      comprador_documento: null,
      comprador_telefono: null,
      external_reference: null,
      intent_id: 'pi_123',
      session_id: 'cs_123',
      hosted_url: 'https://co/c/tok_xxx',
      public_token: 'tok_xxx',
      status: 'pendiente',
      return_url: null,
      cancel_url: null,
      creado_por: USUARIO_ID,
      created_at: new Date(),
      updated_at: new Date(),
    });

    const result = await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 50000,
      descripcion: 'Test',
    });

    expect(client.createPaymentIntent).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ amount: 50000, currency: 'PYG', country: 'PY' }),
    );
    expect(client.createCheckoutSession).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ intentId: 'pi_123' }),
    );
    expect(result.id).toBe('cob_1');
    expect(result.hosted_url).toBe('https://co/c/tok_xxx');
    expect(result.status).toBe('pendiente');
  });

  it('si no hay config activa, falla antes de tocar la pasarela', async () => {
    const { service, client, config } = createService();
    config.requireActiveConfig.mockRejectedValue(new NotFoundException('inactiva'));

    await expect(service.create(EMPRESA_ID, USUARIO_ID, { monto: 1000 })).rejects.toThrow(
      NotFoundException,
    );
    expect(client.createPaymentIntent).not.toHaveBeenCalled();
  });

  it('devuelve cobro existente si external_reference ya fue usado (idempotencia)', async () => {
    const { service, prisma, client } = createService();
    prisma.novasis_pay_cobro.findUnique.mockResolvedValue({
      id: 'cob_existing',
      empresa_id: EMPRESA_ID,
      monto: new Prisma.Decimal(1000),
      currency: 'PYG',
      country: 'PY',
      descripcion: null,
      comprador_nombre: null,
      comprador_email: null,
      comprador_documento: null,
      comprador_telefono: null,
      external_reference: 'FAC-001',
      intent_id: 'pi_old',
      session_id: 'cs_old',
      hosted_url: 'https://co/c/old',
      public_token: 'old',
      status: 'pendiente',
      return_url: null,
      cancel_url: null,
      creado_por: USUARIO_ID,
      created_at: new Date(),
      updated_at: new Date(),
    });

    const result = await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 1000,
      external_reference: 'FAC-001',
    });

    expect(result.id).toBe('cob_existing');
    expect(client.createPaymentIntent).not.toHaveBeenCalled();
  });

  it('list aplica paginación con cursor y nextCursor', async () => {
    const { service, prisma } = createService();
    const rows = Array.from({ length: 3 }, (_, i) => ({
      id: `cob_${i}`,
      empresa_id: EMPRESA_ID,
      monto: new Prisma.Decimal(1000 * (i + 1)),
      currency: 'PYG',
      country: 'PY',
      descripcion: null,
      comprador_nombre: null,
      comprador_email: null,
      comprador_documento: null,
      comprador_telefono: null,
      external_reference: null,
      intent_id: `pi_${i}`,
      session_id: `cs_${i}`,
      hosted_url: 'https://co',
      public_token: `tok_${i}`,
      status: 'pendiente' as const,
      return_url: null,
      cancel_url: null,
      creado_por: null,
      created_at: new Date(),
      updated_at: new Date(),
    }));
    // take = 2 + 1 = 3 → con 3 filas devueltas, hasMore = true y devuelve 2.
    prisma.novasis_pay_cobro.findMany.mockResolvedValue(rows);

    const result = await service.list(EMPRESA_ID, { limit: 2 });

    expect(result.data).toHaveLength(2);
    expect(result.nextCursor).toBe('cob_1');
  });

  it('si viene comprador con email/doc nuevo, crea customer en gateway y persiste mapping', async () => {
    const { service, prisma, client } = createService();
    client.createPaymentIntent.mockResolvedValue({
      id: 'pi_1', merchantId: 'mer_x', status: 'created', amount: '5000', currency: 'PYG', country: 'PY',
    });
    client.createCheckoutSession.mockResolvedValue({
      id: 'cs_1', intentId: 'pi_1', publicToken: 'tok', url: 'https://co/c/tok', status: 'open', expiresAt: new Date().toISOString(),
    });
    client.createCustomer.mockResolvedValue({
      id: 'cus_abc', email: 'a@b.com', name: 'Juan', docType: 'ruc', docNumber: '80012345-6', phone: null, country: 'PY',
    });
    prisma.novasis_pay_cobro.create.mockResolvedValue({
      id: 'cob_1', empresa_id: EMPRESA_ID, monto: new Prisma.Decimal(5000), currency: 'PYG', country: 'PY',
      descripcion: null, comprador_nombre: 'Juan', comprador_email: 'a@b.com', comprador_documento: '80012345-6', comprador_telefono: null,
      external_reference: null, intent_id: 'pi_1', session_id: 'cs_1', hosted_url: 'https://co/c/tok', public_token: 'tok',
      status: 'pendiente', return_url: null, cancel_url: null, creado_por: USUARIO_ID, created_at: new Date(), updated_at: new Date(),
    });

    await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 5000,
      comprador: { nombre: 'Juan', email: 'a@b.com', documento: '80012345-6' },
    });

    expect(client.createCustomer).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ email: 'a@b.com', docType: 'ruc', docNumber: '80012345-6', country: 'PY' }),
    );
    expect(prisma.novasis_pay_customer_ref.create).toHaveBeenCalledWith({
      data: expect.objectContaining({ gateway_customer_id: 'cus_abc', email: 'a@b.com' }),
    });
    expect(client.createPaymentIntent).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ customerId: 'cus_abc' }),
    );
  });

  it('si hay mapping local del comprador, reusa customerId sin pegarle al gateway', async () => {
    const { service, prisma, client } = createService();
    prisma.novasis_pay_customer_ref.findFirst.mockResolvedValue({
      gateway_customer_id: 'cus_existing',
    });
    client.createPaymentIntent.mockResolvedValue({
      id: 'pi_2', merchantId: 'mer_x', status: 'created', amount: '5000', currency: 'PYG', country: 'PY',
    });
    client.createCheckoutSession.mockResolvedValue({
      id: 'cs_2', intentId: 'pi_2', publicToken: 't', url: 'u', status: 'open', expiresAt: new Date().toISOString(),
    });
    prisma.novasis_pay_cobro.create.mockResolvedValue({
      id: 'cob_2', empresa_id: EMPRESA_ID, monto: new Prisma.Decimal(5000), currency: 'PYG', country: 'PY',
      descripcion: null, comprador_nombre: null, comprador_email: 'a@b.com', comprador_documento: null, comprador_telefono: null,
      external_reference: null, intent_id: 'pi_2', session_id: 'cs_2', hosted_url: 'u', public_token: 't',
      status: 'pendiente', return_url: null, cancel_url: null, creado_por: USUARIO_ID, created_at: new Date(), updated_at: new Date(),
    });

    await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 5000,
      comprador: { email: 'a@b.com' },
    });

    expect(client.createCustomer).not.toHaveBeenCalled();
    expect(client.createPaymentIntent).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ customerId: 'cus_existing' }),
    );
  });

  it('comprador sin email ni documento → no se intenta crear customer', async () => {
    const { service, prisma, client } = createService();
    client.createPaymentIntent.mockResolvedValue({
      id: 'pi_3', merchantId: 'mer_x', status: 'created', amount: '5000', currency: 'PYG', country: 'PY',
    });
    client.createCheckoutSession.mockResolvedValue({
      id: 'cs_3', intentId: 'pi_3', publicToken: 't', url: 'u', status: 'open', expiresAt: new Date().toISOString(),
    });
    prisma.novasis_pay_cobro.create.mockResolvedValue({
      id: 'cob_3', empresa_id: EMPRESA_ID, monto: new Prisma.Decimal(5000), currency: 'PYG', country: 'PY',
      descripcion: null, comprador_nombre: 'Solo nombre', comprador_email: null, comprador_documento: null, comprador_telefono: null,
      external_reference: null, intent_id: 'pi_3', session_id: 'cs_3', hosted_url: 'u', public_token: 't',
      status: 'pendiente', return_url: null, cancel_url: null, creado_por: USUARIO_ID, created_at: new Date(), updated_at: new Date(),
    });

    await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 5000,
      comprador: { nombre: 'Solo nombre' },
    });

    expect(client.createCustomer).not.toHaveBeenCalled();
    expect(client.createPaymentIntent).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.not.objectContaining({ customerId: expect.anything() }),
    );
  });

  it('si createCustomer devuelve 409, recupera por lookup y persiste mapping', async () => {
    const { service, prisma, client } = createService();
    client.createCustomer.mockRejectedValue(
      new Error('Novasis Pay respondió 409: {"message":"Customer with same email or document already exists","statusCode":409}'),
    );
    client.findCustomerByEmailOrDoc.mockResolvedValue({
      id: 'cus_existing_remote',
      email: 'a@b.com',
      name: 'Juan',
      docType: 'ruc',
      docNumber: '80012345-6',
      phone: null,
      country: 'PY',
    });
    client.createPaymentIntent.mockResolvedValue({
      id: 'pi_409', merchantId: 'mer_x', status: 'created', amount: '5000', currency: 'PYG', country: 'PY',
    });
    client.createCheckoutSession.mockResolvedValue({
      id: 'cs_409', intentId: 'pi_409', publicToken: 't', url: 'u', status: 'open', expiresAt: new Date().toISOString(),
    });
    prisma.novasis_pay_cobro.create.mockResolvedValue({
      id: 'cob_409', empresa_id: EMPRESA_ID, monto: new Prisma.Decimal(5000), currency: 'PYG', country: 'PY',
      descripcion: null, comprador_nombre: 'Juan', comprador_email: 'a@b.com', comprador_documento: '80012345-6', comprador_telefono: null,
      external_reference: null, intent_id: 'pi_409', session_id: 'cs_409', hosted_url: 'u', public_token: 't',
      status: 'pendiente', return_url: null, cancel_url: null, creado_por: USUARIO_ID, created_at: new Date(), updated_at: new Date(),
    });

    await service.create(EMPRESA_ID, USUARIO_ID, {
      monto: 5000,
      comprador: { nombre: 'Juan', email: 'a@b.com', documento: '80012345-6' },
    });

    expect(client.findCustomerByEmailOrDoc).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ email: 'a@b.com', docNumber: '80012345-6' }),
    );
    expect(prisma.novasis_pay_customer_ref.create).toHaveBeenCalledWith({
      data: expect.objectContaining({ gateway_customer_id: 'cus_existing_remote', email: 'a@b.com' }),
    });
    expect(client.createPaymentIntent).toHaveBeenCalledWith(
      EMPRESA_ID,
      expect.objectContaining({ customerId: 'cus_existing_remote' }),
    );
  });

  it('findOne 404 cuando no existe en esa empresa', async () => {
    const { service, prisma } = createService();
    prisma.novasis_pay_cobro.findFirst.mockResolvedValue(null);
    await expect(service.findOne(EMPRESA_ID, 'cob_x')).rejects.toThrow(NotFoundException);
  });
});
