/**
 * E2E contable — Importaciones, fase 2 (cuenta puente).
 *
 * Verifica los asientos del circuito que pidió DOBA, contra la base local y con
 * los servicios reales de contabilidad: mapeo, períodos y asientos. No hay mocks
 * de Prisma; lo que se mide es a qué cuenta va cada línea y por cuánto, que es
 * justo lo que no se puede comprobar leyendo el código.
 *
 * Cubre el hueco que dejaba `importacion-mercaderia-exterior.e2e.spec.ts`, que
 * ejercía el filtro de componentes del cierre pero no el asiento.
 *
 * Ver docs/plan-importaciones-fase2-contable.md.
 */
import { PrismaClient } from '@prisma/client';
import { AsientosService } from '../../contabilidad/services/asientos.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { MapeoCuentasService } from '../../contabilidad/services/mapeo-cuentas.service';
import { MapeoMediosPagoService } from '../../contabilidad/services/mapeo-medios-pago.service';
import { PeriodosService } from '../../contabilidad/services/periodos.service';
import { TipoCambioService } from '../../contabilidad/services/tipo-cambio.service';
import { ImportacionesCosteoService } from '../services/importaciones-costeo.service';
import { ImportacionesService } from '../services/importaciones.service';

const prisma = new PrismaClient();
const TAG = `CT${Date.now().toString(36).slice(-5)}`;
/** Dentro de un período contable abierto de la empresa anfitriona. */
const FECHA = '2026-03-15';

const auditStub = { log: () => undefined, registrar: () => undefined } as any;
const mediosPago = new MapeoMediosPagoService(prisma as any);
const mapeo = new MapeoCuentasService(prisma as any, mediosPago);
const periodos = new PeriodosService(prisma as any, auditStub);
const tipoCambio = new TipoCambioService(prisma as any);
const asientos = new AsientosService(prisma as any, periodos, tipoCambio, auditStub);
const integracion = new ContabilidadIntegracionService(
  prisma as any, asientos, mapeo, mediosPago, periodos, tipoCambio,
);
const costeo = new ImportacionesCosteoService();
const importaciones = new ImportacionesService(
  prisma as any, costeo,
  { convertir: () => Promise.resolve(undefined) } as any,
  integracion as any,
  { presentar: () => Promise.resolve(undefined) } as any,
);

type Fx = {
  empresaId: string;
  proveedorId: string;
  productoId: string;
  cuentaTesoreriaId: string;
  categoriaId: string;
  conceptoFobId: string;
  conceptoIreId: string;
  ctaPuente: string;
  ctaAnticipo: string;
  ctaInventario: string;
  ctaIva: string;
  ctaRenta: string;
};
let fx: Fx | null = null;
/** Mapeos y cuentas creados por el fixture, para devolver todo como estaba. */
const creadoPorFixture = { mapeos: [] as string[], cuentas: [] as string[] };

const hay = () => fx !== null;
const saltear = (n: string) => {
  // eslint-disable-next-line no-console
  console.warn(`[E2E contable] Salteado "${n}": la base local no tiene los prerrequisitos.`);
};

async function purgar() {
  const sentencias = [
    `CREATE TEMP TABLE IF NOT EXISTS _ct_emb AS SELECT id FROM imp_embarques WHERE numero LIKE 'CT%'`,
    `CREATE TEMP TABLE IF NOT EXISTS _ct_doc AS
       SELECT id FROM cont_documentos
        WHERE origen_id IN (SELECT id FROM _ct_emb)
           OR origen_id IN (SELECT id FROM imp_despachos WHERE embarque_id IN (SELECT id FROM _ct_emb))
           OR origen_id IN (SELECT id FROM tes_movimientos WHERE embarque_id IN (SELECT id FROM _ct_emb))`,
    `DELETE FROM cont_asientos_det WHERE asiento_id IN (SELECT id FROM cont_asientos WHERE documento_id IN (SELECT id FROM _ct_doc))`,
    `DELETE FROM cont_asientos WHERE documento_id IN (SELECT id FROM _ct_doc)`,
    `DELETE FROM cont_documentos WHERE id IN (SELECT id FROM _ct_doc)`,
    `DELETE FROM tes_movimiento_det WHERE movimiento_id IN (SELECT id FROM tes_movimientos WHERE embarque_id IN (SELECT id FROM _ct_emb))`,
    `DELETE FROM tes_movimientos WHERE embarque_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM imp_componentes_costo WHERE embarque_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM movimientos_inventario WHERE documento_origen = 'IMP' AND documento_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM imp_despachos WHERE embarque_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM imp_embarque_items WHERE embarque_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM imp_embarques_bitacora WHERE embarque_id IN (SELECT id FROM _ct_emb)`,
    `DELETE FROM imp_embarques WHERE id IN (SELECT id FROM _ct_emb)`,
    `DROP TABLE IF EXISTS _ct_doc`,
    `DROP TABLE IF EXISTS _ct_emb`,
  ];
  for (const sql of sentencias) await prisma.$executeRawUnsafe(sql);
}

/** Deja el concepto mapeado a una cuenta usable, creándola si hace falta. */
async function asegurarMapeo(empresaId: string, concepto: string, codigo: string, descripcion: string) {
  const existente = await prisma.cont_mapeo_cuentas.findUnique({
    where: { empresa_id_concepto: { empresa_id: empresaId, concepto } },
    select: { cuenta_id: true },
  });
  if (existente) return existente.cuenta_id;

  let cuenta = await prisma.cont_plan_cuentas.findFirst({
    where: { empresa_id: empresaId, codigo },
    select: { id: true },
  });
  if (!cuenta) {
    cuenta = await prisma.cont_plan_cuentas.create({
      data: {
        empresa_id: empresaId, codigo, descripcion,
        tipo: 'ACTIVO', naturaleza: 'DEUDORA', nivel: 4, acepta_movimientos: true,
      },
      select: { id: true },
    });
    creadoPorFixture.cuentas.push(cuenta.id);
  }
  await prisma.cont_mapeo_cuentas.create({
    data: { empresa_id: empresaId, concepto, cuenta_id: cuenta.id },
  });
  creadoPorFixture.mapeos.push(concepto);
  return cuenta.id;
}

beforeAll(async () => {
  await purgar();

  // Empresa con contabilidad de verdad: módulo activo, período abierto en la
  // fecha de prueba, cuenta de tesorería con cuenta contable y los mapeos base.
  const cand = await prisma.$queryRaw<Array<{ empresa_id: string }>>`
    SELECT e.id AS empresa_id FROM empresas e
     WHERE EXISTS (SELECT 1 FROM suscripcion_modulos sm
                     JOIN modulos mo ON mo.id = sm.modulo_id
                     JOIN suscripciones su ON su.id = sm.suscripcion_id
                    WHERE su.empresa_id = e.id AND mo.codigo = 'CONTABILIDAD' AND sm.activo)
       AND EXISTS (SELECT 1 FROM cont_periodos p JOIN cont_ejercicios j ON j.id = p.ejercicio_id
                    WHERE j.empresa_id = e.id AND p.estado = 'ABIERTO'
                      AND ${FECHA}::date BETWEEN p.fecha_inicio AND p.fecha_fin)
       AND EXISTS (SELECT 1 FROM tes_cuentas t WHERE t.empresa_id = e.id AND t.cuenta_contable_id IS NOT NULL)
       AND EXISTS (SELECT 1 FROM proveedores pr WHERE pr.empresa_id = e.id AND pr.activo = true)
       AND EXISTS (SELECT 1 FROM productos p WHERE p.empresa_id = e.id AND p.deleted = false AND p.maneja_inventario = true)
     LIMIT 1`;
  if (!cand.length) return;
  const empresaId = cand[0].empresa_id;

  const [cuentaTes, categoria, proveedor, producto, fob, ire, inv, iva, renta] = await Promise.all([
    prisma.tes_cuentas.findFirst({ where: { empresa_id: empresaId, NOT: { cuenta_contable_id: null } }, select: { id: true } }),
    prisma.tes_categorias.findFirst({ where: { OR: [{ empresa_id: empresaId }, { empresa_id: null }] }, select: { id: true } }),
    prisma.proveedores.findFirst({ where: { empresa_id: empresaId, activo: true }, select: { id: true } }),
    prisma.productos.findFirst({ where: { empresa_id: empresaId, deleted: false, maneja_inventario: true }, select: { id: true } }),
    prisma.imp_conceptos_costo.findFirst({ where: { codigo: 'FOB', OR: [{ empresa_id: empresaId }, { empresa_id: null }] }, select: { id: true } }),
    prisma.imp_conceptos_costo.findFirst({ where: { codigo: 'IMP_ANTICIPO_IRE', OR: [{ empresa_id: empresaId }, { empresa_id: null }] }, select: { id: true } }),
    prisma.cont_mapeo_cuentas.findUnique({ where: { empresa_id_concepto: { empresa_id: empresaId, concepto: 'INVENTARIO' } }, select: { cuenta_id: true } }),
    prisma.cont_mapeo_cuentas.findUnique({ where: { empresa_id_concepto: { empresa_id: empresaId, concepto: 'IVA_CREDITO_10' } }, select: { cuenta_id: true } }),
    prisma.cont_mapeo_cuentas.findUnique({ where: { empresa_id_concepto: { empresa_id: empresaId, concepto: 'RETENCION_RENTA_FAVOR' } }, select: { cuenta_id: true } }),
  ]);
  if (!cuentaTes || !categoria || !proveedor || !producto || !fob || !ire || !inv || !iva || !renta) return;

  const ctaPuente = await asegurarMapeo(empresaId, 'IMPORTACIONES_EN_CURSO', '1.1.4.04', 'Importaciones en Curso');
  const ctaAnticipo = await asegurarMapeo(empresaId, 'ANTICIPO_DESPACHO', '1.1.2.13', 'Anticipos de Despacho');

  fx = {
    empresaId,
    proveedorId: proveedor.id,
    productoId: producto.id,
    cuentaTesoreriaId: cuentaTes.id,
    categoriaId: categoria.id,
    conceptoFobId: fob.id,
    conceptoIreId: ire.id,
    ctaPuente,
    ctaAnticipo,
    ctaInventario: inv.cuenta_id,
    ctaIva: iva.cuenta_id,
    ctaRenta: renta.cuenta_id,
  };
});

afterAll(async () => {
  await purgar();
  if (fx) {
    // Se devuelven los mapeos y cuentas que creó el fixture. Sin esto la empresa
    // quedaría con el modelo de cuenta puente activo para siempre.
    for (const concepto of creadoPorFixture.mapeos) {
      await prisma.cont_mapeo_cuentas.deleteMany({ where: { empresa_id: fx.empresaId, concepto } });
    }
    for (const id of creadoPorFixture.cuentas) {
      await prisma.cont_plan_cuentas.deleteMany({ where: { id } });
    }
  }
  await prisma.$disconnect();
});

async function nuevoEmbarque() {
  const f = fx as Fx;
  const emb = await prisma.imp_embarques.create({
    data: {
      empresa_id: f.empresaId,
      numero: `${TAG}-${Math.random().toString(36).slice(2, 6)}`,
      proveedor_id: f.proveedorId,
      perfil: 'CONSUMO_MASIVO',
      estado: 'DESPACHADO',
    },
  });
  await prisma.imp_embarque_items.create({
    data: {
      empresa_id: f.empresaId, embarque_id: emb.id, perfil: 'CONSUMO_MASIVO',
      producto_id: f.productoId, cantidad: 10, fob_unit_usd: 100, fob_total_usd: 1000,
    },
  });
  return emb.id;
}

async function pagoImputado(embarqueId: string, tipo: 'PROVEEDOR' | 'DESPACHO', monto: number) {
  const f = fx as Fx;
  const mov = await prisma.tes_movimientos.create({
    data: {
      empresa_id: f.empresaId, cuenta_id: f.cuentaTesoreriaId, tipo: 'EGRESO',
      estado: 'CONFIRMADO', fecha: new Date(FECHA), monto,
      descripcion: `${TAG} ${tipo}`, embarque_id: embarqueId, tipo_pago_importacion: tipo,
    },
  });
  await prisma.tes_movimiento_det.create({
    data: { movimiento_id: mov.id, categoria_id: f.categoriaId, monto },
  });
  await integracion.integrarMovimientoTesoreria(mov.id);
  return mov.id;
}

/** Las líneas del asiento de un documento, como [cuenta, debe, haber]. */
async function lineas(origenTipo: string, origenId: string) {
  const doc = await prisma.cont_documentos.findFirst({
    where: { origen_tipo: origenTipo, origen_id: origenId },
    select: { id: true },
  });
  if (!doc) return null;
  const det = await prisma.cont_asientos_det.findMany({
    where: { asiento: { documento_id: doc.id } },
    select: { cuenta_id: true, debe_moneda: true, haber_moneda: true },
  });
  return det.map((d) => ({
    cuenta: d.cuenta_id,
    debe: Number(d.debe_moneda),
    haber: Number(d.haber_moneda),
  }));
}

const debeDe = (ls: any[], cuenta: string) =>
  ls.filter((l) => l.cuenta === cuenta).reduce((a, l) => a + l.debe, 0);
const haberDe = (ls: any[], cuenta: string) =>
  ls.filter((l) => l.cuenta === cuenta).reduce((a, l) => a + l.haber, 0);

describe('E2E contable — los pagos imputados a la importación', () => {
  it('el anticipo al proveedor del exterior va DIRECTO a importaciones en curso', async () => {
    if (!hay()) return saltear('anticipo proveedor');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    const movId = await pagoImputado(emb, 'PROVEEDOR', 5_000_000);

    const ls = await lineas('tes_movimientos', movId);
    expect(ls).not.toBeNull();
    // Lo que pidió el cliente: sin pasar por una cuenta de anticipos.
    expect(debeDe(ls!, f.ctaPuente)).toBe(5_000_000);
    expect(debeDe(ls!, f.ctaAnticipo)).toBe(0);
  });

  it('el anticipo de despacho va a la cuenta de anticipos', async () => {
    if (!hay()) return saltear('anticipo despacho');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    const movId = await pagoImputado(emb, 'DESPACHO', 3_000_000);

    const ls = await lineas('tes_movimientos', movId);
    expect(debeDe(ls!, f.ctaAnticipo)).toBe(3_000_000);
    expect(debeDe(ls!, f.ctaPuente)).toBe(0);
  });
});

describe('E2E contable — el asiento del despacho', () => {
  /** Despacho con arancel (costo), IVA importación y anticipo de IRE. */
  async function despachoConTributos(embarqueId: string) {
    const f = fx as Fx;
    const desp = await prisma.imp_despachos.create({
      data: {
        embarque_id: embarqueId, empresa_id: f.empresaId,
        numero_despacho: `${TAG}-D`, fecha_despacho: new Date(FECHA),
        arancel_gs: 1_000_000, iva_importacion_gs: 600_000, anticipo_ire_gs: 200_000,
        total_tributos_gs: 1_800_000, estado: 'CONFIRMADO',
      },
    });
    const base = { empresa_id: f.empresaId, embarque_id: embarqueId, cotizacion: 1 };
    await prisma.imp_componentes_costo.createMany({
      data: [
        { ...base, concepto_id: f.conceptoFobId, descripcion: 'Arancel', importe: 1_000_000, importe_gs: 1_000_000 },
        { ...base, concepto_id: f.conceptoFobId, descripcion: 'IVA importación', importe: 600_000, importe_gs: 600_000 },
        { ...base, concepto_id: f.conceptoIreId, descripcion: 'Anticipo IRE', importe: 200_000, importe_gs: 200_000 },
      ],
    });
    return desp.id;
  }

  it('acumula en la cuenta puente, separa el IVA y saca el IRE del costo', async () => {
    if (!hay()) return saltear('asiento despacho');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    await pagoImputado(emb, 'DESPACHO', 3_000_000);
    const despId = await despachoConTributos(emb);

    await integracion.integrarCierreDespacho(despId);
    const ls = await lineas('imp_despacho', despId);
    expect(ls).not.toBeNull();

    // Total componentes 1.800.000 = arancel 1.000.000 + IVA 600.000 + IRE 200.000.
    // Al costo va solo el arancel: el IVA es crédito fiscal y el IRE, impuesto
    // a la renta. Ese es el corazón de lo que pidió el cliente.
    expect(debeDe(ls!, f.ctaPuente)).toBe(1_000_000);
    expect(debeDe(ls!, f.ctaIva)).toBe(600_000);
    expect(debeDe(ls!, f.ctaRenta)).toBe(200_000);
    // Y nada de esto toca inventario todavía.
    expect(debeDe(ls!, f.ctaInventario)).toBe(0);
    // La contrapartida es el anticipo de despacho, no deuda con el proveedor.
    expect(haberDe(ls!, f.ctaAnticipo)).toBe(1_800_000);

    const totalDebe = ls!.reduce((a, l) => a + l.debe, 0);
    const totalHaber = ls!.reduce((a, l) => a + l.haber, 0);
    expect(totalDebe).toBe(totalHaber);
  });

  it('CONTROL NEGATIVO: sin mapear la cuenta puente, el asiento es el directo de siempre', async () => {
    if (!hay()) return saltear('control negativo');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    const despId = await despachoConTributos(emb);

    // Se quita el mapeo para simular una empresa que no usa el modelo nuevo.
    const guardado = await prisma.cont_mapeo_cuentas.findUnique({
      where: { empresa_id_concepto: { empresa_id: f.empresaId, concepto: 'IMPORTACIONES_EN_CURSO' } },
      select: { cuenta_id: true },
    });
    await prisma.cont_mapeo_cuentas.deleteMany({
      where: { empresa_id: f.empresaId, concepto: 'IMPORTACIONES_EN_CURSO' },
    });
    try {
      await integracion.integrarCierreDespacho(despId);
      const ls = await lineas('imp_despacho', despId);
      // Sin puente el costo vuelve a inventario, como antes de la fase 2.
      expect(debeDe(ls!, f.ctaInventario)).toBe(1_000_000);
      expect(debeDe(ls!, f.ctaPuente)).toBe(0);
    } finally {
      if (guardado) {
        await prisma.cont_mapeo_cuentas.create({
          data: { empresa_id: f.empresaId, concepto: 'IMPORTACIONES_EN_CURSO', cuenta_id: guardado.cuenta_id },
        });
      }
    }
  });
});

describe('E2E contable — el cierre de la importación', () => {
  it('descarga la cuenta puente contra mercaderías por el costo del embarque', async () => {
    if (!hay()) return saltear('cierre');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId, embarque_id: emb, concepto_id: f.conceptoFobId,
        descripcion: 'Mercadería', importe: 4_000_000, cotizacion: 1, importe_gs: 4_000_000,
      },
    });
    await costeo.recalcular(prisma as any, f.empresaId, emb);

    // El IRE no debe haber entrado al costeo del ítem.
    await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId, embarque_id: emb, concepto_id: f.conceptoIreId,
        descripcion: 'Anticipo IRE', importe: 500_000, cotizacion: 1, importe_gs: 500_000,
      },
    });
    await costeo.recalcular(prisma as any, f.empresaId, emb);

    const embDb = await prisma.imp_embarques.findUnique({
      where: { id: emb }, select: { costo_total_gs: true },
    });
    // 4.000.000 de mercadería; el IRE queda afuera.
    expect(Number(embDb?.costo_total_gs)).toBe(4_000_000);

    await prisma.imp_embarques.update({ where: { id: emb }, data: { estado: 'CERRADO' } });
    await integracion.integrarCierreImportacion(emb);

    const ls = await lineas('imp_cierre', emb);
    expect(ls).not.toBeNull();
    expect(debeDe(ls!, f.ctaInventario)).toBe(4_000_000);
    expect(haberDe(ls!, f.ctaPuente)).toBe(4_000_000);
  });

  it('no duplica el asiento si se vuelve a disparar', async () => {
    if (!hay()) return saltear('idempotencia del cierre');
    const f = fx as Fx;
    const emb = await nuevoEmbarque();
    await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId, embarque_id: emb, concepto_id: f.conceptoFobId,
        descripcion: 'Mercadería', importe: 1_000_000, cotizacion: 1, importe_gs: 1_000_000,
      },
    });
    await costeo.recalcular(prisma as any, f.empresaId, emb);
    await prisma.imp_embarques.update({ where: { id: emb }, data: { estado: 'CERRADO' } });

    await integracion.integrarCierreImportacion(emb);
    await integracion.integrarCierreImportacion(emb);

    const docs = await prisma.cont_documentos.count({
      where: { origen_tipo: 'imp_cierre', origen_id: emb },
    });
    expect(docs).toBe(1);
  });
});
