import { ContabilidadIntegracionService } from './integracion.service';
import { MapeoMediosPagoService } from './mapeo-medios-pago.service';

/**
 * Antes, toda venta contado y toda cobranza debitaban el concepto CAJA_GENERAL y
 * todo pago a proveedor lo acreditaba, sin mirar con qué se cobró ni de dónde
 * salió la plata. El contador de DOBA pidió que el efectivo y la transferencia
 * entren a "Recaudaciones efectivo" (1.01.01.02) y el cheque a "Recaudaciones
 * cheque" (1.01.01.03), y que los pagos salgan por banco.
 *
 * El criterio NO se programa: cada empresa lo configura en
 * `cont_mapeo_medio_pago`. Lo que no configura tiene que seguir funcionando
 * exactamente como antes.
 */
describe('Cuenta contable por medio de pago', () => {
  const EFECTIVO = 'mp-efectivo';
  const TRANSFER = 'mp-transfer';
  const CHEQUE = 'mp-cheque';

  const crearIntegracion = () => {
    const service = Object.create(ContabilidadIntegracionService.prototype) as any;
    service.logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };
    return service;
  };

  describe('reparto de un cobro entre cuentas', () => {
    it('sin nada configurado, todo va a la cuenta de siempre en una sola línea', () => {
      const service = crearIntegracion();

      const porCuenta = service.repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 600_000 },
          { medioPagoId: TRANSFER, monto: 500_000 },
        ],
        new Map(),
        'cta-caja',
      );

      expect([...porCuenta]).toEqual([['cta-caja', 1_100_000]]);
    });

    it('separa el efectivo del cheque cuando la empresa los mapeó distinto', () => {
      const service = crearIntegracion();

      const porCuenta = service.repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 600_000 },
          { medioPagoId: CHEQUE, monto: 500_000 },
        ],
        new Map([
          [EFECTIVO, 'cta-recaud-efectivo'],
          [CHEQUE, 'cta-recaud-cheque'],
        ]),
        'cta-caja',
      );

      expect(porCuenta.get('cta-recaud-efectivo')).toBe(600_000);
      expect(porCuenta.get('cta-recaud-cheque')).toBe(500_000);
      expect(porCuenta.has('cta-caja')).toBe(false);
    });

    it('dos medios a la misma cuenta se suman en una línea, como lo escribiría a mano', () => {
      // Es lo que pidió el contador: efectivo y transferencia a Recaudaciones efectivo.
      const service = crearIntegracion();

      const porCuenta = service.repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 600_000 },
          { medioPagoId: TRANSFER, monto: 500_000 },
        ],
        new Map([
          [EFECTIVO, 'cta-recaudaciones'],
          [TRANSFER, 'cta-recaudaciones'],
        ]),
        'cta-caja',
      );

      expect([...porCuenta]).toEqual([['cta-recaudaciones', 1_100_000]]);
    });

    it('un medio sin mapear conserva la cuenta de siempre y no arrastra a los demás', () => {
      const service = crearIntegracion();

      const porCuenta = service.repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 600_000 },
          { medioPagoId: TRANSFER, monto: 500_000 },
        ],
        new Map([[EFECTIVO, 'cta-recaudaciones']]),
        'cta-caja',
      );

      expect(porCuenta.get('cta-recaudaciones')).toBe(600_000);
      expect(porCuenta.get('cta-caja')).toBe(500_000);
    });

    it('ignora las partes sin monto y las partes sin medio', () => {
      const service = crearIntegracion();

      const porCuenta = service.repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 0 },
          { medioPagoId: null, monto: 300_000 },
        ],
        new Map([[EFECTIVO, 'cta-recaudaciones']]),
        'cta-caja',
      );

      expect([...porCuenta]).toEqual([['cta-caja', 300_000]]);
    });
  });

  describe('el asiento tiene que cuadrar', () => {
    it('carga el redondeo de la conversión a la cuenta más grande', () => {
      // Los medios se guardan en la moneda del documento; al pasarlos a guaraníes
      // la suma puede quedar a un guaraní del total, y el asiento se rechaza.
      const service = crearIntegracion();

      const porCuenta = service.ajustarAlTotal(
        new Map([
          ['cta-recaudaciones', 733_333],
          ['cta-caja', 366_666],
        ]),
        1_100_000,
      );

      expect([...porCuenta.values()].reduce((a, b) => a + b, 0)).toBe(1_100_000);
      expect(porCuenta.get('cta-recaudaciones')).toBe(733_334);
      expect(porCuenta.get('cta-caja')).toBe(366_666);
    });

    it('si ya cuadra no toca nada', () => {
      const service = crearIntegracion();

      const porCuenta = service.ajustarAlTotal(new Map([['cta-caja', 1_100_000]]), 1_100_000);

      expect([...porCuenta]).toEqual([['cta-caja', 1_100_000]]);
    });

    it('un reparto vacío no inventa líneas', () => {
      const service = crearIntegracion();

      expect([...service.ajustarAlTotal(new Map(), 1_100_000)]).toEqual([]);
    });
  });

  describe('MapeoMediosPagoService.resolverPorMedio', () => {
    const crear = (mapeos: any[]) => {
      const service = Object.create(MapeoMediosPagoService.prototype) as any;
      service.logger = { warn: jest.fn(), log: jest.fn() };
      service.prisma = {
        cont_mapeo_medio_pago: { findMany: jest.fn().mockResolvedValue(mapeos) },
        medio_pago: { findMany: jest.fn().mockResolvedValue([{ id: EFECTIVO, codigo: 1 }]) },
      };
      return service;
    };

    it('devuelve los medios configurados', async () => {
      const service = crear([
        {
          medio_pago_id: EFECTIVO,
          cuenta_contable_id: 'cta-recaudaciones',
          cuenta: { codigo: '1.01.01.02', active: true, acepta_movimientos: true },
        },
      ]);

      const mapa = await service.resolverPorMedio('emp-1', 'COBRO');

      expect(mapa.get(EFECTIVO)).toBe('cta-recaudaciones');
    });

    it('un mapeo a una cuenta desactivada se avisa y se ignora, no frena la facturación', async () => {
      const service = crear([
        {
          medio_pago_id: EFECTIVO,
          cuenta_contable_id: 'cta-muerta',
          cuenta: { codigo: '1.01.01.02', active: false, acepta_movimientos: true },
        },
      ]);

      const mapa = await service.resolverPorMedio('emp-1', 'COBRO');

      expect(mapa.size).toBe(0);
      expect(service.logger.warn).toHaveBeenCalledWith(expect.stringContaining('1.01.01.02'));
    });

    it('tampoco se usa una cuenta de agrupación', async () => {
      const service = crear([
        {
          medio_pago_id: EFECTIVO,
          cuenta_contable_id: 'cta-grupo',
          cuenta: { codigo: '1.01.01', active: true, acepta_movimientos: false },
        },
      ]);

      expect((await service.resolverPorMedio('emp-1', 'COBRO')).size).toBe(0);
    });

    it('resolverPorCodigoMedio indexa por el código SIFEN, para los recibos que guardan el medio como texto', async () => {
      const service = crear([
        {
          medio_pago_id: EFECTIVO,
          cuenta_contable_id: 'cta-recaudaciones',
          cuenta: { codigo: '1.01.01.02', active: true, acepta_movimientos: true },
        },
      ]);

      const mapa = await service.resolverPorCodigoMedio('emp-1', 'COBRO');

      expect(mapa.get(1)).toBe('cta-recaudaciones');
    });

    it('sin mapeos no consulta los medios de pago', async () => {
      const service = crear([]);

      const mapa = await service.resolverPorCodigoMedio('emp-1', 'COBRO');

      expect(mapa.size).toBe(0);
      expect(service.prisma.medio_pago.findMany).not.toHaveBeenCalled();
    });
  });

  describe('el cheque no se configura por medio de pago', () => {
    /**
     * Tenía cuenta en los dos lados —acá y en los conceptos CHEQUES_EN_CARTERA /
     * CHEQUES_EN_TRANSITO— y no se veía cuál mandaba. Se queda en los conceptos,
     * que además distinguen el cheque al día del diferido.
     */
    const crearListar = () => {
      const prisma: any = {
        empresa_medio_pago: { findMany: jest.fn().mockResolvedValue([]) },
        medio_pago: { findMany: jest.fn().mockResolvedValue([{ id: EFECTIVO, codigo: 1, descripcion: 'Efectivo' }]) },
        cont_mapeo_medio_pago: { findMany: jest.fn().mockResolvedValue([]) },
      };
      const service = Object.create(MapeoMediosPagoService.prototype) as any;
      service.prisma = prisma;
      service.logger = { warn: jest.fn() };
      return { service, prisma };
    };

    it('la pantalla no lista el cheque', async () => {
      const { service, prisma } = crearListar();

      await service.listar('emp-1');

      expect(prisma.medio_pago.findMany.mock.calls[0][0].where.codigo).toEqual({ notIn: [2] });
    });

    it('al resolver se excluye por consulta, no quedan filas viejas mandando', async () => {
      const service = Object.create(MapeoMediosPagoService.prototype) as any;
      service.logger = { warn: jest.fn() };
      service.prisma = { cont_mapeo_medio_pago: { findMany: jest.fn().mockResolvedValue([]) } };

      await service.resolverPorMedio('emp-1', 'COBRO');

      const where = service.prisma.cont_mapeo_medio_pago.findMany.mock.calls[0][0].where;
      expect(where.medio_pago).toEqual({ codigo: { notIn: [2] } });
    });

    it('guardar una cuenta para el cheque se rechaza con un mensaje que dice dónde va', async () => {
      const service = Object.create(MapeoMediosPagoService.prototype) as any;
      service.prisma = {
        cont_plan_cuentas: { findFirst: jest.fn().mockResolvedValue({ id: 'cta-1' }) },
        medio_pago: { findUnique: jest.fn().mockResolvedValue({ id: CHEQUE, codigo: 2, descripcion: 'Cheque' }) },
        cont_mapeo_medio_pago: { upsert: jest.fn(), deleteMany: jest.fn() },
      };

      await expect(
        service.actualizarBulk('emp-1', [{ medio_pago_id: CHEQUE, flujo: 'COBRO', cuenta_id: 'cta-1' }]),
      ).rejects.toThrow(/Cheques en cartera/);
      expect(service.prisma.cont_mapeo_medio_pago.upsert).not.toHaveBeenCalled();
    });
  });

  describe('a dónde va el cheque en venta contado y cobranza simple', () => {
    /**
     * Esos dos caminos nunca miraban los conceptos de cheques: un cheque cobrado
     * en el acto iba a "Caja general". Ahora entra por CHEQUES_EN_CARTERA, porque
     * ni la factura ni el detalle del recibo simple guardan el vencimiento.
     */
    const crear = (cuentaCartera: string | null) => {
      const service = Object.create(ContabilidadIntegracionService.prototype) as any;
      service.logger = { warn: jest.fn() };
      service.mediosPago = { idMedioPorCodigo: jest.fn().mockResolvedValue(CHEQUE) };
      service.mapeo = {
        getCuentaPorConcepto: jest.fn().mockImplementation((_e: string, c: string) =>
          c === 'CHEQUES_EN_CARTERA' && cuentaCartera
            ? Promise.resolve(cuentaCartera)
            : Promise.reject(new Error('sin mapear')),
        ),
      };
      return service;
    };

    it('el cheque se resuelve con la cuenta de cheques en cartera', async () => {
      const service = crear('cta-cheques-cartera');
      const mapa = new Map([[EFECTIVO, 'cta-recaudaciones']]);

      await service.agregarChequeAlMapa('emp-1', mapa);

      expect(mapa.get(CHEQUE)).toBe('cta-cheques-cartera');
      expect(service.mediosPago.idMedioPorCodigo).toHaveBeenCalledWith(2);
      // No pisa lo que la empresa configuró para los demás medios.
      expect(mapa.get(EFECTIVO)).toBe('cta-recaudaciones');
    });

    it('sin el concepto mapeado no inventa cuenta: cae al comportamiento de siempre', async () => {
      const service = crear(null);
      const mapa = new Map<string, string>();

      await service.agregarChequeAlMapa('emp-1', mapa);

      expect(mapa.size).toBe(0);
    });

    it('un cheque así repartido abre su propia línea de debe', async () => {
      const service = crear('cta-cheques-cartera');
      const mapa = new Map([[EFECTIVO, 'cta-recaudaciones']]);
      await service.agregarChequeAlMapa('emp-1', mapa);

      const reparto = crearIntegracion().repartirPorMedio(
        [
          { medioPagoId: EFECTIVO, monto: 565_000 },
          { medioPagoId: CHEQUE, monto: 200_000 },
        ],
        mapa,
        'cta-caja',
      );

      expect(reparto.get('cta-recaudaciones')).toBe(565_000);
      expect(reparto.get('cta-cheques-cartera')).toBe(200_000);
    });
  });

  describe('actualizarBulk', () => {
    const crear = () => {
      const prisma: any = {
        cont_plan_cuentas: { findFirst: jest.fn().mockResolvedValue({ id: 'cta-1' }) },
        medio_pago: { findUnique: jest.fn().mockResolvedValue({ id: EFECTIVO }) },
        cont_mapeo_medio_pago: {
          upsert: jest.fn().mockResolvedValue({}),
          deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      };
      const service = Object.create(MapeoMediosPagoService.prototype) as any;
      service.prisma = prisma;
      service.logger = { warn: jest.fn() };
      return { service, prisma };
    };

    it('guardar un medio con cuenta hace upsert por empresa + medio + flujo', async () => {
      const { service, prisma } = crear();

      await service.actualizarBulk('emp-1', [
        { medio_pago_id: EFECTIVO, flujo: 'COBRO', cuenta_id: 'cta-1' },
      ]);

      expect(prisma.cont_mapeo_medio_pago.upsert).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            empresa_id_medio_pago_id_flujo: {
              empresa_id: 'emp-1',
              medio_pago_id: EFECTIVO,
              flujo: 'COBRO',
            },
          },
        }),
      );
    });

    it('dejar un medio en blanco borra el mapeo y lo devuelve al comportamiento anterior', async () => {
      const { service, prisma } = crear();

      await service.actualizarBulk('emp-1', [
        { medio_pago_id: EFECTIVO, flujo: 'PAGO', cuenta_id: null },
      ]);

      expect(prisma.cont_mapeo_medio_pago.deleteMany).toHaveBeenCalledWith({
        where: { empresa_id: 'emp-1', medio_pago_id: EFECTIVO, flujo: 'PAGO' },
      });
      expect(prisma.cont_mapeo_medio_pago.upsert).not.toHaveBeenCalled();
    });

    it('rechaza un flujo que no sea COBRO o PAGO', async () => {
      const { service } = crear();

      await expect(
        service.actualizarBulk('emp-1', [{ medio_pago_id: EFECTIVO, flujo: 'OTRO', cuenta_id: 'cta-1' }]),
      ).rejects.toThrow(/COBRO o PAGO/);
    });

    it('rechaza una cuenta que no es del plan de la empresa', async () => {
      const { service, prisma } = crear();
      prisma.cont_plan_cuentas.findFirst.mockResolvedValue(null);

      await expect(
        service.actualizarBulk('emp-1', [{ medio_pago_id: EFECTIVO, flujo: 'COBRO', cuenta_id: 'cta-ajena' }]),
      ).rejects.toThrow(/no encontrada/);
    });
  });
});
