import { BadRequestException } from '@nestjs/common';
import { GastosService } from './gastos.service';
import { GastosTesoreriaService } from './services/gastos-tesoreria.service';

/**
 * Gastos en otra moneda (docs/plan-gastos-multimoneda.md): el pago sale de la
 * cuenta en la moneda de la cuenta y la cotización nunca queda en 1 por omisión.
 */

const CUENTAS: Record<string, { id: string; nombre: string; moneda: string }> = {
  usd: { id: 'usd', nombre: 'Itaú USD', moneda: 'USD' },
  pyg: { id: 'pyg', nombre: 'Banco Continental', moneda: 'PYG' },
};

function tesoreria(gasto: Record<string, unknown>, config: { noMueveCaja?: boolean } = {}) {
  const creados = { movimientos: [] as any[], cheques: [] as any[], saldos: [] as any[] };
  const tx: any = {
    gasto_cab: { findFirst: jest.fn().mockResolvedValue({ id: 'g1', empresa_id: 'emp', estado: 'pagada', forma_pago: 'CONTADO', fecha_emision: new Date('2026-10-16'), numero_factura: '001-001-0000123', medio_pago_id: 'mp', medio_pago: { codigo: 1, descripcion: 'Transferencia' }, ...gasto }) },
    tes_movimientos: {
      findFirst: jest.fn().mockResolvedValue(null),
      create: jest.fn().mockImplementation(({ data }) => (creados.movimientos.push(data), { id: 'm1' })),
    },
    tes_cheques: {
      findFirst: jest.fn().mockResolvedValue(null),
      create: jest.fn().mockImplementation(({ data }) => (creados.cheques.push(data), {})),
    },
    tes_cuentas: {
      findFirst: jest.fn().mockImplementation(({ where }) => CUENTAS[where.id] ?? null),
      update: jest.fn().mockImplementation(({ data }) => (creados.saldos.push(data.saldo_actual), {})),
    },
    tes_reglas: { findFirst: jest.fn().mockResolvedValue({ activo: true, cuenta_id: 'pyg' }) },
    tes_categorias: { findFirst: jest.fn().mockResolvedValue({ id: 'cat' }) },
    tes_movimiento_det: { create: jest.fn().mockResolvedValue({}) },
    empresa_medio_pago: { findFirst: jest.fn().mockResolvedValue(config.noMueveCaja ? { no_mueve_caja: true } : null) },
  };
  const prisma: any = { ...tx, $transaction: (fn: any) => fn(tx) };
  return { service: new GastosTesoreriaService(prisma), creados };
}

describe('GastosTesoreriaService.integrarGasto — moneda de la cuenta', () => {
  it('USD desde la cuenta en USD: descuenta los dólares', async () => {
    const { service, creados } = tesoreria({ total: 100, cotizacion: 7300, moneda: { codigo: 'USD' }, cuenta_tesoreria_id: 'usd' });
    expect(await service.integrarGasto('g1')).toEqual({});
    expect(creados.movimientos[0]).toMatchObject({ cuenta_id: 'usd', monto: 100, tipo_cambio: null });
    expect(creados.saldos).toEqual([{ decrement: 100 }]);
  });

  it('USD desde una cuenta en guaraníes: descuenta total × cotización y guarda la cotización', async () => {
    const { service, creados } = tesoreria({ total: 100, cotizacion: 7300, moneda: { codigo: 'USD' }, cuenta_tesoreria_id: 'pyg' });
    await service.integrarGasto('g1');
    expect(creados.movimientos[0]).toMatchObject({ cuenta_id: 'pyg', monto: 730000, tipo_cambio: 7300 });
    expect(creados.saldos).toEqual([{ decrement: 730000 }]);
  });

  it('sin cuenta elegida usa la regla y convierte igual', async () => {
    const { service, creados } = tesoreria({ total: 50, cotizacion: 7000, moneda: { codigo: 'USD' }, cuenta_tesoreria_id: null });
    await service.integrarGasto('g1');
    expect(creados.movimientos[0]).toMatchObject({ cuenta_id: 'pyg', monto: 350000 });
  });

  it('guaraníes desde la cuenta en USD: no mueve nada y avisa', async () => {
    const { service, creados } = tesoreria({ total: 500000, cotizacion: 1, moneda: null, cuenta_tesoreria_id: 'usd' });
    const r = await service.integrarGasto('g1');
    expect(r.warning).toContain('está en USD');
    expect(creados.movimientos).toHaveLength(0);
    expect(creados.saldos).toHaveLength(0);
  });

  it('pagado con "Pago por anticipo" no saca plata del banco', async () => {
    const { service, creados } = tesoreria({
      total: 10000, cotizacion: 1, moneda: null, cuenta_tesoreria_id: 'pyg',
      medio_pago: { codigo: 11, descripcion: 'Pago por anticipo' },
    });
    expect(await service.integrarGasto('g1')).toEqual({});
    expect(creados.movimientos).toHaveLength(0);
    expect(creados.cheques).toHaveLength(0);
    expect(creados.saldos).toHaveLength(0);
  });

  it('pagado con "Otro" que la empresa marcó como que no mueve dinero: tampoco saca plata (DOBA)', async () => {
    const otro = { total: 300000, cotizacion: 1, moneda: null, cuenta_tesoreria_id: 'pyg', medio_pago: { codigo: 22, descripcion: 'Otro' } };
    const marcado = tesoreria(otro, { noMueveCaja: true });
    expect(await marcado.service.integrarGasto('g1')).toEqual({});
    expect(marcado.creados.movimientos).toHaveLength(0);

    // Sin marcar, "Otro" es un pago común y sale del banco.
    const sinMarcar = tesoreria(otro);
    await sinMarcar.service.integrarGasto('g1');
    expect(sinMarcar.creados.movimientos).toHaveLength(1);
  });

  it('el cheque se emite en la moneda de la cuenta', async () => {
    const { service, creados } = tesoreria({
      total: 100,
      cotizacion: 7300,
      moneda: { codigo: 'USD' },
      cuenta_tesoreria_id: 'usd',
      medio_pago: { codigo: 2, descripcion: 'Cheque' },
    });
    await service.integrarGasto('g1');
    expect(creados.cheques[0]).toMatchObject({ monto: 100, moneda: 'USD' });
  });
});

describe('GastosService — moneda y cotización del gasto', () => {
  const MONEDAS: Record<string, string> = { 'mon-usd': 'USD', 'mon-pyg': 'PYG' };
  const crear = (vigente: { tasa: number; warning?: string } = { tasa: 7350 }) => {
    const monedas = { getCotizacionVigente: jest.fn().mockResolvedValue(vigente) };
    const gastosTesoreria = {
      resolverCuentaPago: jest.fn().mockImplementation(async (_tx, _e, { cuentaTesoreriaId }) => ({
        cuenta: CUENTAS[cuentaTesoreriaId ?? 'pyg'],
      })),
    };
    const service = new GastosService({} as any, {} as any, gastosTesoreria as any, {} as any, monedas as any);
    const tx: any = { moneda: { findUnique: jest.fn().mockImplementation(({ where }) => ({ codigo: MONEDAS[where.id] })) } };
    return { service: service as any, tx, monedas };
  };
  const fecha = new Date('2026-10-16');

  it('en guaraníes la cotización es 1 aunque venga otra', async () => {
    const { service, tx } = crear();
    expect(await service.resolverMonedaGasto(tx, 'emp', null, 7300, fecha)).toEqual({ codigo: 'PYG', cotizacion: 1 });
    expect(await service.resolverMonedaGasto(tx, 'emp', 'mon-pyg', 7300, fecha)).toEqual({ codigo: 'PYG', cotizacion: 1 });
  });

  it('en USD respeta la cotización cargada', async () => {
    const { service, tx, monedas } = crear();
    expect(await service.resolverMonedaGasto(tx, 'emp', 'mon-usd', 7280, fecha)).toEqual({ codigo: 'USD', cotizacion: 7280 });
    expect(monedas.getCotizacionVigente).not.toHaveBeenCalled();
  });

  it('en USD sin cotización toma la vigente a la fecha del gasto', async () => {
    const { service, tx, monedas } = crear();
    expect(await service.resolverMonedaGasto(tx, 'emp', 'mon-usd', undefined, fecha)).toEqual({ codigo: 'USD', cotizacion: 7350 });
    expect(monedas.getCotizacionVigente).toHaveBeenCalledWith('emp', 'USD', fecha);
  });

  it('en USD sin cotización cargada en ningún lado no deja grabar', async () => {
    const { service, tx } = crear({ tasa: 1, warning: 'Cargá una en Contabilidad → Tipo de Cambio.' });
    await expect(service.resolverMonedaGasto(tx, 'emp', 'mon-usd', 1, fecha)).rejects.toThrow(/Falta la cotización del USD/);
  });

  it('bloquea un gasto en guaraníes pagado desde la cuenta en USD', async () => {
    const { service, tx } = crear();
    await expect(
      service.assertCuentaAceptaMoneda(tx, 'emp', { cuentaTesoreriaId: 'usd', moneda: { codigo: 'PYG', cotizacion: 1 }, total: 500000 }),
    ).rejects.toThrow(BadRequestException);
  });

  it('acepta USD desde guaraníes y USD desde USD', async () => {
    const { service, tx } = crear();
    const usd = { codigo: 'USD', cotizacion: 7300 };
    await expect(service.assertCuentaAceptaMoneda(tx, 'emp', { cuentaTesoreriaId: 'pyg', moneda: usd, total: 100 })).resolves.toBeUndefined();
    await expect(service.assertCuentaAceptaMoneda(tx, 'emp', { cuentaTesoreriaId: 'usd', moneda: usd, total: 100 })).resolves.toBeUndefined();
  });

  it('si la cuenta sale de la regla, el mensaje lo dice', async () => {
    const { service, tx } = crear();
    service.gastosTesoreria.resolverCuentaPago.mockResolvedValue({ cuenta: CUENTAS.usd });
    await expect(
      service.assertCuentaAceptaMoneda(tx, 'emp', { cuentaTesoreriaId: null, moneda: { codigo: 'PYG', cotizacion: 1 }, total: 1 }),
    ).rejects.toThrow(/Pago de gasto/);
  });
});
