import { BadRequestException } from "@nestjs/common";
import { consolidarLineasParaPos, FacturacionPosService } from "./facturacion-pos.service";
import { PostouchRespuestaIncierta } from "./postouch-client.service";

function createTallerParametrosMock(overrides: Record<string, any> = {}) {
  const valores: Record<string, any> = {
    "facturacion.usa_facturacion": true,
    "facturacion_pos.habilitada": true,
    "facturacion_pos.url": "http://postouch/create_ordenes_edngt",
    "facturacion_pos.username": "22",
    "facturacion_pos.password": "5551",
    "facturacion_pos.computadora": "5.5",
    "facturacion_pos.precio_decimales": 2,
    "facturacion_pos.precio_incluye_iva": true,
    "facturacion_pos.plu_productos_taller": "TALLER-PROD",
    "facturacion_pos.plu_servicios_taller": "TALLER-SERV",
    ...overrides.valores,
  };

  return {
    getBooleanConFallback: jest.fn(async (clave: string) => Boolean(valores[clave])),
    getParaTienda: jest.fn(async (_tiendaId: number, clave: string) => valores[clave]),
    ...overrides.tallerParametros,
  };
}

function createService(overrides: Record<string, any> = {}) {
  const otFacturacionRepository = {
    save: jest.fn(async (data: any) => ({ id: 1, ...data })),
    ...overrides.otFacturacionRepository,
  };

  const tenantAccessor = {
    getCurrentTenantId: jest.fn(() => 42),
    repositoryFor: jest.fn(() => otFacturacionRepository),
    ...overrides.tenantAccessor,
  };

  const postouchClientService = {
    enviarOrden: jest.fn(async () => ({
      tienda: 1,
      caja: 2,
      tipo: "FAC",
      serie: "A",
      transac: 555,
    })),
    ...overrides.postouchClientService,
  };

  const tallerParametros = createTallerParametrosMock(overrides);

  const service = new FacturacionPosService(
    tenantAccessor as any,
    postouchClientService as any,
    tallerParametros as any,
  );

  return {
    service,
    tenantAccessor,
    tallerParametros,
    postouchClientService,
    otFacturacionRepository,
  };
}

const otBase = {
  id: 7,
  numeroOt: "OT-1234",
  tiendaId: 3,
  clienteTarjeta: "2031",
  cliente: { nombre: "Juan Perez", nit: "CF", telefono: "5085-4657" },
  cotizacion: {
    lineas: [
      // 100.00 final por unidad con IVA 12 %: la base guardada es 89.29.
      { plu: "2205", descripcion: "Aceite", precioUnitario: 89.29, total: 200, ivaPorcentaje: 12, cantidad: 2, esServicio: false },
      { plu: "TLLP-00000001", descripcion: "Mano de obra", precioUnitario: 223.21, total: 250, ivaPorcentaje: 12, cantidad: 1, esServicio: true, origenLocal: true },
    ],
  },
};

describe("FacturacionPosService.facturacionAplica", () => {
  it("aplica solo si el taller usa facturacion Y la tienda la tiene habilitada", async () => {
    expect(await createService().service.facturacionAplica(3)).toBe(true);
    expect(
      await createService({ valores: { "facturacion.usa_facturacion": false } }).service.facturacionAplica(3),
    ).toBe(false);
    expect(
      await createService({ valores: { "facturacion_pos.habilitada": false } }).service.facturacionAplica(3),
    ).toBe(false);
  });

  it("una OT sin tienda nunca aplica", async () => {
    const { service, tallerParametros } = createService();

    expect(await service.facturacionAplica(null)).toBe(false);
    expect(tallerParametros.getParaTienda).not.toHaveBeenCalled();
  });

  it("si no se puede leer la config de la tienda, no aplica (no bloquea la OT por un fallo de lectura)", async () => {
    const { service } = createService({
      tallerParametros: {
        getParaTienda: jest.fn(() => {
          throw new Error("sin contexto");
        }),
      },
    });

    expect(await service.facturacionAplica(3)).toBe(false);
  });
});

describe("FacturacionPosService.enviarOrdenTerminada", () => {
  it("no llama a POSTouch y marca noAplica si facturacion.usa_facturacion esta apagado", async () => {
    const { service, postouchClientService } = createService({
      valores: { "facturacion.usa_facturacion": false },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, noAplica: true }));
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("no llama a POSTouch y marca noAplica si la OT no tiene tienda asignada", async () => {
    const { service, postouchClientService } = createService();

    const result = await service.enviarOrdenTerminada({ ...otBase, tiendaId: null });

    expect(result).toEqual(expect.objectContaining({ exito: false, noAplica: true }));
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("no llama a POSTouch y marca noAplica si la tienda no tiene facturacion habilitada", async () => {
    const { service, postouchClientService } = createService({
      valores: { "facturacion_pos.habilitada": false },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, noAplica: true }));
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("devuelve la factura creada y NO guarda nada local (lo registra quien llama, dentro de su transaccion)", async () => {
    const { service, otFacturacionRepository } = createService();

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual({
      exito: true,
      factura: { tienda: 1, caja: 2, tipo: "FAC", serie: "A", transac: 555 },
    });
    expect(otFacturacionRepository.save).not.toHaveBeenCalled();
  });

  it("con precio_incluye_iva manda el precio FINAL (total/cantidad) y el total esperado, no la base", async () => {
    const { service, postouchClientService } = createService();

    await service.enviarOrdenTerminada(otBase);

    expect(postouchClientService.enviarOrden).toHaveBeenCalledWith(
      expect.objectContaining({ url: "http://postouch/create_ordenes_edngt", precioIncluyeIva: true }),
      expect.objectContaining({
        id: 7,
        cliente: { tarjeta: "2031", nombre: "Juan Perez", nit: "CF", telefono: "5085-4657" },
        lineas: [
          { plu: "2205", cantidad: 2, precio: 100, totalEsperado: 200 },
          { plu: "TALLER-SERV", cantidad: 1, precio: 250, totalEsperado: 250 },
        ],
      }),
    );
  });

  it("sin precio_incluye_iva manda la base sin IVA y no compara contra el total cotizado", async () => {
    const { service, postouchClientService } = createService({
      valores: { "facturacion_pos.precio_incluye_iva": false },
    });

    await service.enviarOrdenTerminada(otBase);

    expect(postouchClientService.enviarOrden).toHaveBeenCalledWith(
      expect.objectContaining({ precioIncluyeIva: false }),
      expect.objectContaining({
        lineas: [
          { plu: "2205", cantidad: 2, precio: 89.29, totalEsperado: undefined },
          { plu: "TALLER-SERV", cantidad: 1, precio: 223.21, totalEsperado: undefined },
        ],
      }),
    );
  });

  it("lo creado en el taller se factura bajo los PLU genericos y no se escribe nada en el ERP", async () => {
    const { service, postouchClientService, tenantAccessor } = createService();

    await service.enviarOrdenTerminada({
      ...otBase,
      cotizacion: {
        lineas: [
          { plu: "2205", descripcion: "Aceite", precioUnitario: 89.29, total: 200, ivaPorcentaje: 12, cantidad: 2, esServicio: false },
          { plu: "TLLP-00000001", descripcion: "Filtro", precioUnitario: 8.93, total: 10, ivaPorcentaje: 12, cantidad: 1, esServicio: false, origenLocal: true },
          { plu: "TLLP-00000002", descripcion: "Bujia", precioUnitario: 17.86, total: 20, ivaPorcentaje: 12, cantidad: 1, esServicio: false, origenLocal: true },
          { plu: "MANUAL-1", descripcion: "Engrase", precioUnitario: 111.61, total: 125, ivaPorcentaje: 12, cantidad: 1, esServicio: true, origenLocal: true },
          { plu: "MANUAL-2", descripcion: "Revision", precioUnitario: 133.93, total: 150, ivaPorcentaje: 12, cantidad: 1, esServicio: true, origenLocal: true },
        ],
      },
    });

    const lineas = (postouchClientService.enviarOrden.mock.calls as any[][])[0][1].lineas;
    expect(lineas).toEqual([
      { plu: "2205", cantidad: 2, precio: 100, totalEsperado: 200 },
      { plu: "TALLER-PROD", cantidad: 1, precio: 30, totalEsperado: 30 },
      { plu: "TALLER-SERV", cantidad: 1, precio: 275, totalEsperado: 275 },
    ]);
    // El taller ya no usa ninguna ruta de escritura hacia el ERP.
    for (const [entidad] of (tenantAccessor.repositoryFor.mock.calls as any[][])) {
      expect(entidad?.name).not.toBe("MaepluMirror");
    }
  });

  it("si la OT no tiene nada creado en el taller no exige los PLU genericos", async () => {
    const { service, postouchClientService } = createService({
      valores: {
        "facturacion_pos.plu_productos_taller": undefined,
        "facturacion_pos.plu_servicios_taller": undefined,
      },
    });

    const result = await service.enviarOrdenTerminada({
      ...otBase,
      cotizacion: {
        lineas: [{ plu: "2205", descripcion: "Aceite", precioUnitario: 89.29, total: 200, ivaPorcentaje: 12, cantidad: 2, esServicio: false }],
      },
    });

    expect(result).toEqual(expect.objectContaining({ exito: true }));
    expect(postouchClientService.enviarOrden).toHaveBeenCalledTimes(1);
  });

  it("con lineas del taller y sin PLU generico configurado no llama a POSTouch y dice que configurar", async () => {
    const { service, postouchClientService } = createService({
      valores: { "facturacion_pos.plu_servicios_taller": undefined },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(
      expect.objectContaining({
        exito: false,
        error: expect.stringContaining("PLU genérico de servicios del taller"),
      }),
    );
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("retorna exito:false con el mensaje si falla la resolucion de contexto de tenant", async () => {
    const { service, postouchClientService } = createService({
      tallerParametros: {
        getBooleanConFallback: jest.fn(async () => true),
        getParaTienda: jest.fn(() => {
          throw new Error("No hay contexto de tenant resuelto");
        }),
      },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    // facturacionAplica no puede leer la tienda: no aplica, y no se llama a POS.
    expect(result).toEqual(expect.objectContaining({ exito: false }));
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("retorna exito:false con el mensaje si la config de la tienda esta incompleta", async () => {
    const { service, postouchClientService } = createService({
      valores: { "facturacion_pos.url": null },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(
      expect.objectContaining({
        exito: false,
        error: "La tienda 3 tiene facturacionPosHabilitada pero no tiene facturacionPosUrl configurada",
      }),
    );
    expect(postouchClientService.enviarOrden).not.toHaveBeenCalled();
  });

  it("retorna exito:false con el mensaje si POSTouch rechaza la orden", async () => {
    const { service } = createService({
      postouchClientService: {
        enviarOrden: jest.fn(async () => {
          throw new Error("POSTouch rechazo la orden: No existe el PLU[2205].");
        }),
      },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual({
      exito: false,
      error: "POSTouch rechazo la orden: No existe el PLU[2205].",
      incierto: false,
    });
  });

  it("propaga la marca 'incierto' cuando POSTouch pudo haber creado la factura", async () => {
    const incierto = Object.assign(new Error("POSTouch no respondió en 30 s"), { incierto: true });
    const { service } = createService({
      postouchClientService: { enviarOrden: jest.fn(async () => Promise.reject(incierto)) },
    });

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, incierto: true }));
  });
});

// Caso real del demo (12-oct-2026): con una caja de tipo domicilio, POSTouch
// primero crea un PEDIDO y contesta algo que el taller no puede leer (no trae
// tienda/caja/tipo); al recibir la MISMA orden otra vez la confirma a factura.
// POSTouch deja el rastro en pedidos_bitacora (webifica_order_id = id de la OT).
describe("FacturacionPosService.enviarOrdenTerminada -- reconciliacion con pedidos_bitacora", () => {
  const incierta = () => new PostouchRespuestaIncierta("POSTouch respondió pero no se pudo leer la factura creada.");
  const filaFactura = { tienda: 1, caja: 1, tipo: 4, serie: "CI0101", transac: 3156 };
  const filaPedido = { tienda: 1, caja: 1, tipo: 0, serie: "", transac: 0 };

  function conBitacora(respuestas: any[], enviar: jest.Mock) {
    const query = jest.fn();
    for (const r of respuestas) {
      if (r instanceof Error) query.mockRejectedValueOnce(r);
      else query.mockResolvedValueOnce(r);
    }
    const { service, postouchClientService } = createService({
      tenantAccessor: { repositoryFor: jest.fn(() => ({ query })) },
      postouchClientService: { enviarOrden: enviar },
    });
    return { service, query, postouchClientService };
  }

  it("respuesta ilegible + factura ya confirmada en la bitacora: se toma esa factura, sin reenviar", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service, query } = conBitacora([[filaFactura]], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual({
      exito: true,
      factura: { tienda: 1, caja: 1, tipo: "4", serie: "CI0101", transac: 3156 },
    });
    expect(enviar).toHaveBeenCalledTimes(1);
    expect(query).toHaveBeenCalledTimes(1);
  });

  it("pedido pendiente en la bitacora: reenvia la MISMA orden una vez (POSTouch la confirma a factura) y la lee", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service, query } = conBitacora([[filaPedido], [filaFactura]], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual({
      exito: true,
      factura: { tienda: 1, caja: 1, tipo: "4", serie: "CI0101", transac: 3156 },
    });
    expect(enviar).toHaveBeenCalledTimes(2);
    expect(enviar.mock.calls[1]).toEqual(enviar.mock.calls[0]);
    expect(query).toHaveBeenCalledTimes(2);
  });

  it("si despues de reenviar sigue pendiente, NO insiste: queda incierto tras exactamente dos envios", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service } = conBitacora([[filaPedido], [filaPedido]], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, incierto: true }));
    expect(enviar).toHaveBeenCalledTimes(2);
  });

  it("sin fila en la bitacora (caja normal, POSTouch revento): sigue incierto y NO reenvia", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service } = conBitacora([[]], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, incierto: true }));
    expect(enviar).toHaveBeenCalledTimes(1);
  });

  it("si la bitacora no se puede leer (otra base, sin la tabla), conserva el error incierto original", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service } = conBitacora([new Error("Table 'pedidos_bitacora' doesn't exist")], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(
      expect.objectContaining({ exito: false, incierto: true, error: expect.stringContaining("no se pudo leer la factura creada") }),
    );
    expect(enviar).toHaveBeenCalledTimes(1);
  });

  it("un rechazo explicito (no incierto) no consulta la bitacora ni reenvia", async () => {
    const enviar = jest.fn().mockRejectedValue(new BadRequestException("POSTouch rechazo la orden: No existe el PLU[2205]."));
    const { service, query } = conBitacora([], enviar);

    const result = await service.enviarOrdenTerminada(otBase);

    expect(result).toEqual(expect.objectContaining({ exito: false, error: expect.stringContaining("No existe el PLU") }));
    expect((result as any).incierto).toBe(false);
    expect(enviar).toHaveBeenCalledTimes(1);
    expect(query).not.toHaveBeenCalled();
  });

  it("si el envio sale bien no toca la bitacora", async () => {
    const enviar = jest.fn(async () => ({ tienda: 1, caja: 2, tipo: "FAC", serie: "A", transac: 555 }));
    const { service, query } = conBitacora([], enviar as any);

    await service.enviarOrdenTerminada(otBase);

    expect(query).not.toHaveBeenCalled();
  });

  it("la lectura es un SELECT parametrizado por el id de la OT, sobre pedidos_bitacora", async () => {
    const enviar = jest.fn().mockRejectedValue(incierta());
    const { service, query } = conBitacora([[filaFactura]], enviar);

    await service.enviarOrdenTerminada(otBase);

    const [sql, params] = (query.mock.calls as any[][])[0];
    expect(sql.trim().toUpperCase().startsWith("SELECT")).toBe(true);
    expect(sql).toMatch(/pedidos_bitacora/);
    expect(sql).not.toMatch(/\b(UPDATE|INSERT|DELETE|REPLACE)\b/i);
    expect(params).toEqual([otBase.id]);
  });
});

describe("FacturacionPosService.consultarPago", () => {
  const factura = { tienda: 1, caja: 2, tipo: "1", serie: "A", transac: 555 };

  function conFilas(filas: any[]) {
    const query = jest.fn(async () => filas);
    const { service } = createService({
      tenantAccessor: { repositoryFor: jest.fn(() => ({ query })) },
    });
    return { service, query };
  }

  it("lee SOLO con un SELECT, parametrizado por la clave del documento", async () => {
    const { service, query } = conFilas([{ total: 100, anulacion: 0, pendientes: 0, pagado: 100 }]);

    await service.consultarPago(factura);

    const [sql, params] = (query.mock.calls as any[][])[0];
    expect(sql.trim().toUpperCase().startsWith("SELECT")).toBe(true);
    expect(sql).not.toMatch(/\b(UPDATE|INSERT|DELETE|REPLACE)\b/i);
    expect(params).toEqual([1, 2, "1", "A", 555]);
  });

  it("usa el medio 'por liquidar' configurado en formas_pago, no el numero 99 del payload", async () => {
    const { service, query } = conFilas([{ total: 100, anulacion: 0, pendientes: 0, pagado: 100 }]);

    await service.consultarPago(factura);

    const [sql] = (query.mock.calls as any[][])[0];
    expect(sql).toMatch(/formas_pago WHERE porliquidar = 1/);
    expect(sql).not.toMatch(/\b99\b/);
  });

  it("pagada: sin fila por liquidar y con pagos reales que cubren el total", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 0, pendientes: 0, pagado: 112 }]);

    expect(await service.consultarPago(factura)).toEqual({ estado: "pagada", total: 112, pagado: 112 });
  });

  it("pagada con vuelto: el efectivo recibido puede superar el total", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 0, pendientes: 0, pagado: 150 }]);

    expect((await service.consultarPago(factura)).estado).toBe("pagada");
  });

  it("tolera un centavo de diferencia, igual que POSTouch", async () => {
    const { service } = conFilas([{ total: "112.00", anulacion: null, pendientes: "0", pagado: "111.99" }]);

    expect((await service.consultarPago(factura)).estado).toBe("pagada");
  });

  it("pendiente mientras exista la fila del medio 'por liquidar', aunque haya otros pagos", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 0, pendientes: 1, pagado: 112 }]);

    expect((await service.consultarPago(factura)).estado).toBe("pendiente");
  });

  it("pendiente si los pagos no cubren el total", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 0, pendientes: 0, pagado: 50 }]);

    expect((await service.consultarPago(factura)).estado).toBe("pendiente");
  });

  it("una venta sin ninguna forma de pago real NO se da por cobrada", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 0, pendientes: 0, pagado: 0 }]);

    expect((await service.consultarPago(factura)).estado).toBe("pendiente");
  });

  it("anulada (anulacion distinta de cero) nunca es pagada, aunque este liquidada", async () => {
    const { service } = conFilas([{ total: 112, anulacion: 7, pendientes: 0, pagado: 112 }]);

    expect((await service.consultarPago(factura)).estado).toBe("anulada");
  });

  it("no_encontrada si la factura no aparece en hmaetra", async () => {
    const { service } = conFilas([]);

    expect((await service.consultarPago(factura)).estado).toBe("no_encontrada");
  });

  it("si la lectura falla (tabla inexistente, sin permiso) el error se propaga: quien llama decide degradar", async () => {
    const { service } = createService({
      tenantAccessor: {
        repositoryFor: jest.fn(() => ({
          query: jest.fn(async () => {
            throw new Error("Table 'hdetmed' doesn't exist");
          }),
        })),
      },
    });

    await expect(service.consultarPago(factura)).rejects.toThrow("doesn't exist");
  });
});

describe("consolidarLineasParaPos", () => {
  const config = { precioIncluyeIva: true, pluProductosTaller: "TALLER-PROD", pluServiciosTaller: "TALLER-SERV" };
  const linea = (extra: Record<string, any>) => ({
    plu: "X",
    descripcion: "x",
    precioUnitario: 0,
    total: 0,
    ivaPorcentaje: 12,
    cantidad: 1,
    esServicio: false,
    ...extra,
  });

  it("una linea del ERP viaja intacta, con su PLU y su cantidad", () => {
    expect(
      consolidarLineasParaPos([linea({ plu: "2205", cantidad: 3, total: 300, precioUnitario: 89.29 })], config),
    ).toEqual([{ plu: "2205", cantidad: 3, precio: 100, totalEsperado: 300 }]);
  });

  it("sin IVA incluido suma base por cantidad y no compara contra el total", () => {
    const sinIva = { ...config, precioIncluyeIva: false };
    expect(
      consolidarLineasParaPos(
        [
          linea({ origenLocal: true, esServicio: true, cantidad: 2, precioUnitario: 50, total: 112 }),
          linea({ origenLocal: true, esServicio: true, cantidad: 1, precioUnitario: 10, total: 11.2 }),
        ],
        sinIva,
      ),
    ).toEqual([{ plu: "TALLER-SERV", cantidad: 1, precio: 110, totalEsperado: undefined }]);
  });

  it("la suma se redondea a centavos para no arrastrar error de punto flotante", () => {
    const [consolidada] = consolidarLineasParaPos(
      [
        linea({ origenLocal: true, total: 0.1 }),
        linea({ origenLocal: true, total: 0.2 }),
      ],
      config,
    );
    expect(consolidada).toEqual({ plu: "TALLER-PROD", cantidad: 1, precio: 0.3, totalEsperado: 0.3 });
  });

  it("un servicio TLLP- (producto local marcado como servicio) va con los servicios", () => {
    const [consolidada] = consolidarLineasParaPos(
      [linea({ plu: "TLLP-00000009", origenLocal: true, esServicio: true, total: 80 })],
      config,
    );
    expect(consolidada.plu).toBe("TALLER-SERV");
  });

  it("falla con un mensaje claro si falta el PLU generico del grupo que se necesita", () => {
    expect(() =>
      consolidarLineasParaPos([linea({ origenLocal: true, total: 10 })], { ...config, pluProductosTaller: undefined }),
    ).toThrow(/PLU genérico de productos del taller/);
  });
});
