import { BadRequestException, ForbiddenException } from "@nestjs/common";
import { OtsService } from "./ots.service";
import { EstadoOT, OrdenTrabajo } from "./entities/orden-trabajo.entity";
import { OtCosaTrae } from "./entities/ot-cosa-trae.entity";
import { OtDano } from "./entities/ot-dano.entity";
import { OtFirma } from "./entities/ot-firma.entity";
import { OtFoto } from "./entities/ot-foto.entity";
import { OtEvento } from "./entities/ot-evento.entity";
import { OtFacturacion } from "../facturacion-pos/entities/ot-facturacion.entity";
import { Usuario } from "../auth/entities/usuario.entity";
import { ClienteLocal } from "../clientes/entities/cliente-local.entity";
import { EstadoCotizacion } from "../cotizaciones/entities/cotizacion.entity";

function createQueryBuilderMock(lastOt: any = null) {
  return {
    leftJoinAndSelect: jest.fn().mockReturnThis(),
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    orderBy: jest.fn().mockReturnThis(),
    getOne: jest.fn().mockResolvedValue(lastOt),
  };
}

function createService(overrides: Record<string, any> = {}) {
  const savedOt = {
    id: 7,
    numeroOt: "OT-20260429-0001",
    clienteTarjeta: "CLI-1",
    vehiculoId: 3,
    fechaEntrada: new Date("2026-04-29T10:00:00.000Z"),
    estado: EstadoOT.BORRADOR,
    km: 1200,
    nivelGasolina: 50,
    requerimientoCliente: "Cambio de aceite",
    version: 1,
    cosasTrae: [],
    danos: [],
    fotos: [],
    cotizaciones: [],
  };

  const otRepository = {
    createQueryBuilder: jest.fn(() => createQueryBuilderMock(null)),
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({
      ...savedOt,
      ...data,
      id: data.id ?? savedOt.id,
    })),
    findOne: jest.fn(async () => savedOt),
    update: jest.fn(async () => ({ affected: 1 })),
    ...overrides.otRepository,
  };

  const cosaTraeRepository = {
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => data),
    delete: jest.fn(),
    ...overrides.cosaTraeRepository,
  };

  const danoRepository = {
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({ id: 11, ...data })),
    delete: jest.fn(async () => ({ affected: 1 })),
    ...overrides.danoRepository,
  };

  const fotoRepository = {
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({ id: 12, ...data })),
    delete: jest.fn(async () => ({ affected: 1 })),
    ...overrides.fotoRepository,
  };

  const firmaRepository = {
    findOne: jest.fn(async () => null),
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({ id: 13, ...data })),
    ...overrides.firmaRepository,
  };

  const eventoRepository = {
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({ id: 14, ...data })),
    find: jest.fn(async () => []),
    ...overrides.eventoRepository,
  };

  const usuarioRepository = {
    findOne: jest.fn(async () => null),
    find: jest.fn(async () => []),
    ...overrides.usuarioRepository,
  };

  const clienteRepository = {
    find: jest.fn(async () => []),
    findOne: jest.fn(async () => null),
    ...overrides.clienteRepository,
  };

  const otFacturacionRepository = {
    findOne: jest.fn(async () => null),
    update: jest.fn(async () => ({ affected: 1 })),
    ...overrides.otFacturacionRepository,
  };

  const facturacionPosService = {
    enviarOrdenTerminada: jest.fn(async () => ({
      exito: false,
      error: "tenant sin facturacion habilitada",
    })),
    // Por defecto la facturacion POS no aplica: asi los tests de flujo de OT no
    // dependen de ella. Los de facturacion lo sobreescriben.
    facturacionAplica: jest.fn(async () => false),
    ...overrides.facturacionPosService,
  };

  // Por defecto TODO encendido = el comportamiento previo a los parametros
  // operativos; cada test apaga solo lo que quiere probar.
  const flags: Record<string, boolean> = { ...overrides.flags };
  const tallerParametros = {
    getBooleanConFallback: jest.fn(async (clave: string) => flags[clave] ?? true),
    exigirHabilitado: jest.fn(async (clave: string) => {
      if (!(flags[clave] ?? true)) {
        throw new ForbiddenException(`${clave} apagado`);
      }
    }),
  };

  const repositoriesByEntity = new Map<any, any>([
    [OrdenTrabajo, otRepository],
    [OtCosaTrae, cosaTraeRepository],
    [OtDano, danoRepository],
    [OtFoto, fotoRepository],
    [OtFirma, firmaRepository],
    [OtEvento, eventoRepository],
    [Usuario, usuarioRepository],
    [ClienteLocal, clienteRepository],
    [OtFacturacion, otFacturacionRepository],
  ]);

  const tenantAccessor = {
    repositoryFor: jest.fn((entity: any) => repositoriesByEntity.get(entity)),
    runInTransaction: jest.fn(async (fn: any) =>
      fn({ get: (entity: any) => repositoriesByEntity.get(entity) }),
    ),
    getCurrentTenantId: jest.fn(() => 1),
  };

  const service = new OtsService(
    tenantAccessor as any,
    {
      ensureAssignable: jest.fn(async (id: number) => ({ id, activo: true })),
      ...overrides.mecanicosService,
    } as any,
    {
      findOrCreate: jest.fn(async () => ({ id: 3 })),
      ...overrides.vehiculosService,
    } as any,
    facturacionPosService as any,
    tallerParametros as any,
  );

  return {
    service,
    tenantAccessor,
    tallerParametros,
    flags,
    otRepository,
    cosaTraeRepository,
    danoRepository,
    fotoRepository,
    firmaRepository,
    eventoRepository,
    usuarioRepository,
    clienteRepository,
    facturacionPosService,
    otFacturacionRepository,
    savedOt,
  };
}

// El repositorio real devuelve la fila tal como quedo guardada: sin esto, el
// envio a POS (que corre DESPUES de guardar TERMINADA) leeria la OT en su estado
// viejo y concluiria que "todavia no esta terminada".
function otRepoConEstado(inicial: EstadoOT, extra: Record<string, any> = {}, base: any = {}) {
  let estado = inicial;
  return {
    findOne: jest.fn(async () => ({ ...base, ...extra, estado })),
    save: jest.fn(async (d: any) => {
      estado = d.estado ?? estado;
      return { ...d };
    }),
  };
}

function eventosGuardados(eventoRepository: any): string[] {
  return eventoRepository.save.mock.calls.map((c: any[]) => c[0].descripcion);
}

describe("OtsService", () => {
  it("rechaza crear una OT sin vehiculo", async () => {
    const { service } = createService();

    await expect(
      service.create({
        clienteTarjeta: "CLI-1",
        fechaEntrada: "2026-04-29T10:00:00.000Z",
        km: 1200,
        requerimientoCliente: "Cambio de aceite",
      }),
    ).rejects.toThrow(new BadRequestException("Se requiere un vehículo"));
  });

  it("crea una OT con vehiculo existente y normaliza el requerimiento", async () => {
    const { service, otRepository, cosaTraeRepository } = createService();

    const result = await service.create(
      {
        clienteTarjeta: "CLI-1",
        vehiculoId: 3,
        fechaEntrada: "2026-04-29T10:00:00.000Z",
        km: 1200,
        nivelGasolina: 50,
        requerimientoCliente: "  Cambio de aceite  ",
        observaciones: "  Revisar frenos  ",
        cosasTrae: [1, 2, 2],
      },
      9,
    );

    expect(otRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        clienteTarjeta: "CLI-1",
        vehiculoId: 3,
        requerimientoCliente: "Cambio de aceite",
        observaciones: "Revisar frenos",
        createdBy: 9,
        updatedBy: 9,
      }),
    );
    expect(cosaTraeRepository.save).toHaveBeenCalledWith([
      { otId: 7, cosaTraeId: 1 },
      { otId: 7, cosaTraeId: 2 },
    ]);
    expect(result.id).toBe(7);
  });

  it("rechaza crear otra OT si el vehiculo ya tiene una orden activa", async () => {
    const activeOt = {
      id: 10,
      numeroOt: "OT-20260429-0002",
      vehiculoId: 3,
      estado: EstadoOT.EN_PROCESO,
      vehiculo: { placa: "P123ABC" },
    };

    const { service } = createService({
      otRepository: {
        createQueryBuilder: jest
          .fn()
          .mockReturnValueOnce(createQueryBuilderMock(activeOt)),
      },
    });

    await expect(
      service.create({
        clienteTarjeta: "CLI-1",
        vehiculoId: 3,
        fechaEntrada: "2026-04-29T10:00:00.000Z",
        km: 1200,
        requerimientoCliente: "Cambio de aceite",
      }),
    ).rejects.toThrow(
      "Este vehiculo (P123ABC) ya tiene una orden activa: OT-20260429-0002",
    );
  });

  it("requiere fecha de salida al marcar una OT como entregada", async () => {
    const { service } = createService({
      otRepository: {
        findOne: jest.fn(async () => ({
          id: 7,
          estado: EstadoOT.TERMINADA,
          fechaSalida: null,
          version: 1,
        })),
      },
    });

    await expect(service.changeEstado(7, EstadoOT.ENTREGADA)).rejects.toThrow(
      "Se requiere la fecha de salida para marcar como entregada",
    );
  });

  it("requiere firmas antes de entregar una OT", async () => {
    const { service } = createService({
      otRepository: {
        findOne: jest.fn(async () => ({
          id: 7,
          estado: EstadoOT.TERMINADA,
          fechaSalida: null,
          version: 1,
          firma: null,
        })),
      },
    });

    await expect(
      service.changeEstado(7, EstadoOT.ENTREGADA, "2026-04-29T18:00:00.000Z"),
    ).rejects.toThrow(
      "Captura la firma del cliente antes de entregar el vehículo.",
    );
  });

  it("permite entregar una OT con fecha de salida y ambas firmas", async () => {
    const { service, otRepository } = createService({
      otRepository: {
        findOne: jest.fn(async () => ({
          id: 7,
          estado: EstadoOT.TERMINADA,
          fechaSalida: null,
          version: 1,
          firma: {
            firmaCliente: "https://example.com/firma-cliente.webp",
            firmaRecepcion: "https://example.com/firma-recepcion.webp",
          },
        })),
      },
    });

    const result = await service.changeEstado(
      7,
      EstadoOT.ENTREGADA,
      "2026-04-29T18:00:00.000Z",
      9,
    );

    expect(otRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({
        estado: EstadoOT.ENTREGADA,
        updatedBy: 9,
      }),
    );
    expect(result.id).toBe(7);
  });

  it("rechaza fotos con URL que no corresponde a imagen", async () => {
    const { service } = createService();

    await expect(
      service.addFoto(7, {
        tipo: "general" as any,
        url: "https://example.com/documento.pdf",
      }),
    ).rejects.toThrow("La evidencia debe ser una imagen valida.");
  });

  it("normaliza fotos validas antes de guardarlas", async () => {
    const { service, fotoRepository } = createService();

    const result = await service.addFoto(7, {
      tipo: "general" as any,
      url: "  https://example.com/foto.webp  ",
      descripcion: "  Frente del vehiculo  ",
    });

    expect(fotoRepository.create).toHaveBeenCalledWith({
      otId: 7,
      tipo: "general",
      url: "https://example.com/foto.webp",
      descripcion: "Frente del vehiculo",
    });
    expect(result.id).toBe(12);
  });

  it("rechaza danos sin posicion valida", async () => {
    const { service } = createService();

    await expect(
      service.addDano(7, {
        zona: "frontal",
        tipoDanoId: 1,
        posicionX: 120,
        posicionY: 50,
      }),
    ).rejects.toThrow("La posicion X debe estar entre 0 y 100.");
  });

  it("rechaza PDF de escaner sin extension PDF", async () => {
    const { service } = createService();

    await expect(
      service.savePdfEscaner(7, "https://example.com/scanner.png"),
    ).rejects.toThrow("El archivo del escaner debe ser PDF.");
  });

  it("rechaza guardar firmas sin firma dibujada", async () => {
    const { service } = createService();

    await expect(service.saveFirmas(7, {})).rejects.toThrow(
      "Dibuja al menos una firma antes de guardar.",
    );
  });

  it("requiere nombre cuando se guarda una firma nueva", async () => {
    const { service } = createService();

    await expect(
      service.saveFirmas(7, {
        firmaCliente: "https://example.com/firma.webp",
      }),
    ).rejects.toThrow("Escribe el nombre de quien firma como cliente.");
  });

  it("guarda y normaliza firmas validas", async () => {
    const { service, firmaRepository } = createService();

    const result = await service.saveFirmas(7, {
      firmaCliente: "  https://example.com/firma-cliente.webp  ",
      nombreFirmaCliente: "  Juan Perez  ",
    });

    expect(firmaRepository.create).toHaveBeenCalledWith({ otId: 7 });
    expect(firmaRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({
        firmaCliente: "https://example.com/firma-cliente.webp",
        nombreFirmaCliente: "Juan Perez",
      }),
    );
    expect(result.id).toBe(13);
  });

  describe("changeEstado — integracion POSTouch", () => {
    it("rechaza un PATCH manual a FACTURADA", async () => {
      const { service } = createService();

      await expect(
        service.changeEstado(7, EstadoOT.FACTURADA, undefined, 1),
      ).rejects.toThrow(/no se puede establecer manualmente/i);
    });

    const FACTURA = { tienda: 1, caja: 2, tipo: "FAC", serie: "A", transac: 555 };

    function lineaCotizada() {
      return {
        plu: "TLLP-001",
        descripcion: "Cambio de aceite",
        precioUnitario: "89.29",
        subtotal: "89.29",
        iva: "10.71",
        total: "100.00",
        cantidad: "1.00",
        esServicio: true,
      };
    }

    function otConEstado(estado: EstadoOT, extra: Record<string, any> = {}) {
      return {
        id: 7,
        numeroOt: "OT-20260429-0001",
        clienteTarjeta: "CLI-1",
        tiendaId: 1,
        estado,
        fechaSalida: null,
        version: 1,
        firma: { firmaCliente: "x", firmaRecepcion: "y" },
        cotizaciones: [{ estado: EstadoCotizacion.APROBADA, lineas: [lineaCotizada()] }],
        ...extra,
      };
    }

    it("al pasar a TERMINADA con facturacion aplicable y envio exitoso, registra la factura y queda en FACTURADA", async () => {
      const { service, otRepository, otFacturacionRepository, eventoRepository, facturacionPosService } =
        createService({
          otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO)),
          otFacturacionRepository: { save: jest.fn(async (d: any) => d) },
          facturacionPosService: {
            enviarOrdenTerminada: jest.fn(async () => ({ exito: true, factura: FACTURA })),
          },
        });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      expect(facturacionPosService.enviarOrdenTerminada).toHaveBeenCalledTimes(1);
      expect(otFacturacionRepository.save).toHaveBeenCalledWith({ otId: 7, ...FACTURA });
      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.FACTURADA }),
      );
      expect(eventosGuardados(eventoRepository).join("|")).toContain("A-555");
    });

    it("manda a POS el total CON IVA de cada linea y su porcentaje de IVA (no solo la base)", async () => {
      const { service, facturacionPosService } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO)),
        otFacturacionRepository: { save: jest.fn(async (d: any) => d) },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: true, factura: FACTURA })),
        },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      const enviada = (facturacionPosService.enviarOrdenTerminada.mock.calls as any[][])[0][0];
      expect(enviada.cotizacion.lineas[0]).toEqual(
        expect.objectContaining({ precioUnitario: 89.29, total: 100, ivaPorcentaje: 12 }),
      );
    });

    it("trata las lineas MANUAL- igual que las TLLP-: necesitan espejo en maeplu", async () => {
      const manual = { ...lineaCotizada(), plu: "MANUAL-ABC123" };
      const { service, facturacionPosService } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, {
          ...otConEstado(EstadoOT.EN_PROCESO),
          cotizaciones: [{ estado: EstadoCotizacion.APROBADA, lineas: [manual] }],
        }),
        otFacturacionRepository: { save: jest.fn(async (d: any) => d) },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: true, factura: FACTURA })),
        },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      const enviada = (facturacionPosService.enviarOrdenTerminada.mock.calls as any[][])[0][0];
      expect(enviada.cotizacion.lineas[0].origenLocal).toBe(true);
    });

    it("si el envio falla queda en TERMINADA con un evento de error", async () => {
      const { service, otRepository, eventoRepository } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO)),
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: false, error: "POSTouch no responde" })),
        },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.TERMINADA }),
      );
      expect(eventosGuardados(eventoRepository).join("|")).toContain("POSTouch no responde");
    });

    it("sin facturacion POS aplicable no escribe ningun evento de error al terminar la OT", async () => {
      const { service, eventoRepository } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO)),
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({
            exito: false,
            noAplica: true,
            error: "no aplica",
          })),
        },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      expect(eventosGuardados(eventoRepository).join("|")).not.toMatch(/No se pudo enviar/);
    });

    it("sin cotizacion aprobada no falla la transicion y deja un evento de error", async () => {
      const { service, otRepository, eventoRepository, facturacionPosService } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO, { cotizaciones: [] })),
      });

      await expect(service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1)).resolves.toBeDefined();

      expect(facturacionPosService.enviarOrdenTerminada).not.toHaveBeenCalled();
      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.TERMINADA }),
      );
      expect(eventosGuardados(eventoRepository).join("|")).toContain(
        "No se pudo enviar a POSTouch automáticamente",
      );
    });

    it("un PATCH a terminada sobre una OT que YA esta terminada NO reenvia a POS", async () => {
      const { service, facturacionPosService } = createService({
        otRepository: { findOne: jest.fn(async () => otConEstado(EstadoOT.TERMINADA)) },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      expect(facturacionPosService.enviarOrdenTerminada).not.toHaveBeenCalled();
    });

    it("si la factura ya existe y la OT quedo en TERMINADA, la repara a FACTURADA sin llamar a POS", async () => {
      const { service, otRepository, facturacionPosService } = createService({
        otRepository: { findOne: jest.fn(async () => otConEstado(EstadoOT.TERMINADA)) },
        otFacturacionRepository: { findOne: jest.fn(async () => ({ otId: 7, ...FACTURA })) },
      });

      await service.reintentarFacturacionPos(7, 1);

      expect(facturacionPosService.enviarOrdenTerminada).not.toHaveBeenCalled();
      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.FACTURADA }),
      );
    });

    it("si POS creo la factura pero el guardado local falla, el error dice que la factura YA existe y no invita a reintentar", async () => {
      const { service, eventoRepository } = createService({
        otRepository: otRepoConEstado(EstadoOT.EN_PROCESO, otConEstado(EstadoOT.EN_PROCESO)),
        otFacturacionRepository: {
          save: jest.fn(async () => {
            throw new Error("conexion perdida");
          }),
        },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: true, factura: FACTURA })),
        },
      });

      await service.changeEstado(7, EstadoOT.TERMINADA, undefined, 1);

      const eventos = eventosGuardados(eventoRepository).join("|");
      expect(eventos).toContain("YA se creó en POSTouch");
      expect(eventos).toContain("A-555");
      expect(eventos).toContain("NO reintentes");
    });

    it("dos reintentos concurrentes envian UNA sola orden a POS", async () => {
      // La transaccion real bloquea la fila de la OT; aqui se simula con una
      // cola, que es lo que ese bloqueo garantiza.
      const facturas: any[] = [];
      let estado = EstadoOT.TERMINADA;
      let cola: Promise<unknown> = Promise.resolve();
      const enviarOrdenTerminada = jest.fn(async () => {
        await new Promise((r) => setTimeout(r, 10));
        return { exito: true, factura: FACTURA };
      });
      const { service, tenantAccessor } = createService({
        otRepository: {
          findOne: jest.fn(async () => otConEstado(estado)),
          save: jest.fn(async (d: any) => {
            estado = d.estado;
            return d;
          }),
        },
        otFacturacionRepository: {
          findOne: jest.fn(async () => facturas[facturas.length - 1] ?? null),
          save: jest.fn(async (d: any) => {
            facturas.push(d);
            return d;
          }),
        },
        facturacionPosService: { enviarOrdenTerminada },
      });
      const original = tenantAccessor.runInTransaction;
      tenantAccessor.runInTransaction = jest.fn((fn: any) => {
        const turno = cola.then(() => original(fn));
        cola = turno.catch(() => undefined);
        return turno;
      }) as any;

      await Promise.all([service.reintentarFacturacionPos(7, 1), service.reintentarFacturacionPos(7, 1)]);

      expect(enviarOrdenTerminada).toHaveBeenCalledTimes(1);
      expect(facturas).toHaveLength(1);
    });

    it("el reintento propaga el error real de POSTouch en vez de responder 200", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => otConEstado(EstadoOT.TERMINADA)) },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: false, error: "PLU inexistente" })),
        },
      });

      await expect(service.reintentarFacturacionPos(7, 1)).rejects.toThrow("PLU inexistente");
    });

    it("rechaza reintentar si la OT no esta en TERMINADA", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => ({ id: 7, estado: EstadoOT.FACTURADA })) },
      });

      await expect(service.reintentarFacturacionPos(7, 1)).rejects.toThrow(/solo se puede reintentar/i);
    });
  });

  describe("getFacturacionPos", () => {
    it("siempre devuelve un objeto: aplica=false y sin datos de factura cuando el taller no usa POS", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => ({ id: 7, tiendaId: 1, pagoConfirmadoAt: null })) },
      });

      expect(await service.getFacturacionPos(7)).toEqual({
        aplica: false,
        exigePago: false,
        pagoConfirmado: false,
        pagoConfirmadoAt: null,
        pagoDetectadoEnPos: false,
        pagoPos: null,
        pagoPosDetalle: null,
      });
    });

    it("con POS aplicable incluye la factura y el estado del pago", async () => {
      const confirmado = new Date("2026-10-05T10:00:00Z");
      const { service, otFacturacionRepository } = createService({
        otRepository: { findOne: jest.fn(async () => ({ id: 7, tiendaId: 1, pagoConfirmadoAt: confirmado })) },
        otFacturacionRepository: {
          findOne: jest.fn(async () => ({ otId: 7, tienda: 1, caja: 2, tipo: "FAC", serie: "A", transac: 555 })),
        },
        facturacionPosService: { facturacionAplica: jest.fn(async () => true) },
      });

      const result = await service.getFacturacionPos(7);

      expect(otFacturacionRepository.findOne).toHaveBeenCalledWith({
        where: { otId: 7 },
        order: { id: "DESC" },
      });
      expect(result).toEqual(
        expect.objectContaining({
          aplica: true,
          exigePago: true,
          pagoConfirmado: true,
          pagoConfirmadoAt: confirmado,
          serie: "A",
          transac: 555,
        }),
      );
    });
  });

  describe("entrega con facturacion POS", () => {
    const base = (estado: EstadoOT, extra: Record<string, any> = {}) => ({
      id: 7,
      numeroOt: "OT-1",
      clienteTarjeta: "CLI-1",
      tiendaId: 1,
      estado,
      fechaSalida: new Date(),
      version: 1,
      firma: { firmaCliente: "x", firmaRecepcion: "y" },
      cotizaciones: [],
      ...extra,
    });
    const conPos = { facturacionAplica: jest.fn(async () => true) };

    it("con POS aplicable no se puede entregar desde TERMINADA", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => base(EstadoOT.TERMINADA)) },
        facturacionPosService: conPos,
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(
        /facturarse en POSTouch/,
      );
    });

    it("desde FACTURADA exige el pago confirmado", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => base(EstadoOT.FACTURADA)) },
        facturacionPosService: conPos,
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(
        /confirmar el pago/,
      );
    });

    it("con el pago confirmado se entrega", async () => {
      const { service, otRepository } = createService({
        otRepository: {
          findOne: jest.fn(async () => base(EstadoOT.FACTURADA, { pagoConfirmadoAt: new Date() })),
        },
        facturacionPosService: conPos,
      });

      await service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1);

      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.ENTREGADA }),
      );
    });

    it("si el taller apago exige_pago_para_entrega, FACTURADA basta", async () => {
      const { service } = createService({
        flags: { "facturacion.exige_pago_para_entrega": false },
        otRepository: { findOne: jest.fn(async () => base(EstadoOT.FACTURADA)) },
        facturacionPosService: conPos,
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).resolves.toBeDefined();
    });

    it("sin POS aplicable la entrega sale directo de TERMINADA, como antes", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => base(EstadoOT.TERMINADA)) },
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).resolves.toBeDefined();
    });
  });

  describe("confirmarPago", () => {
    const factura = (extra: Record<string, any> = {}) => ({
      id: 7,
      tiendaId: 1,
      estado: EstadoOT.FACTURADA,
      version: 1,
      pagoConfirmadoAt: null,
      cotizaciones: [],
      ...extra,
    });

    it("guarda quien y cuando confirmo, y lo deja en la bitacora", async () => {
      const { service, otRepository, eventoRepository } = createService({
        otRepository: { findOne: jest.fn(async () => factura()) },
        facturacionPosService: { facturacionAplica: jest.fn(async () => true) },
      });

      await service.confirmarPago(7, 4, { id: 4, rol: "admin" });

      expect(otRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ pagoConfirmadoAt: expect.any(Date), pagoConfirmadoPor: 4 }),
      );
      expect(eventosGuardados(eventoRepository).join("|")).toContain("Pago confirmado");
    });

    it("es idempotente: reconfirmar no cambia la fecha original", async () => {
      const original = new Date("2026-10-01T00:00:00Z");
      const { service, otRepository } = createService({
        otRepository: { findOne: jest.fn(async () => factura({ pagoConfirmadoAt: original })) },
        facturacionPosService: { facturacionAplica: jest.fn(async () => true) },
      });

      await service.confirmarPago(7, 4, { id: 4, rol: "admin" });

      expect(otRepository.save).not.toHaveBeenCalled();
    });

    it("solo aplica a OTs facturadas", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => factura({ estado: EstadoOT.TERMINADA })) },
        facturacionPosService: { facturacionAplica: jest.fn(async () => true) },
      });

      await expect(service.confirmarPago(7, 4, { id: 4, rol: "admin" })).rejects.toThrow(/facturada/);
    });

    it("rechaza si la facturacion POS no aplica a la OT", async () => {
      const { service } = createService({
        otRepository: { findOne: jest.fn(async () => factura()) },
      });

      await expect(service.confirmarPago(7, 4, { id: 4, rol: "admin" })).rejects.toThrow(/no se factura/);
    });
  });

  describe("funciones apagables por taller", () => {
    const ot = (extra: Record<string, any> = {}) => ({
      id: 7,
      numeroOt: "OT-1",
      clienteTarjeta: "CLI-1",
      tiendaId: 1,
      estado: EstadoOT.EN_PROCESO,
      version: 1,
      firma: null,
      cotizaciones: [],
      ...extra,
    });
    const dtoBase = {
      clienteTarjeta: "CLI-1",
      vehiculoId: 3,
      fechaEntrada: "2026-04-29T10:00:00.000Z",
      km: 1200,
      requerimientoCliente: "Revisión",
    };

    it.each([
      ["ot.usa_fotos", (s: any) => s.addFoto(7, { tipo: "general", url: "https://x/y.jpg" } as any, 1, { id: 1, rol: "admin" })],
      ["ot.usa_fotos", (s: any) => s.removeFoto(7, 3, { id: 1, rol: "admin" })],
      ["ot.usa_danos", (s: any) => s.addDano(7, { zona: "puerta", tipoDanoId: 1 } as any, 1, { id: 1, rol: "admin" })],
      ["ot.usa_danos", (s: any) => s.removeDano(7, 3, { id: 1, rol: "admin" })],
      ["ot.usa_pdf_escaner", (s: any) => s.savePdfEscaner(7, "https://x/a.pdf", 1, { id: 1, rol: "admin" })],
    ])("con %s apagado el backend rechaza la operacion", async (clave, operacion) => {
      const { service } = createService({
        flags: { [clave]: false },
        otRepository: { findOne: jest.fn(async () => ot()) },
      });

      await expect(operacion(service)).rejects.toThrow(clave);
    });

    it("con fotos encendido siguen funcionando (control)", async () => {
      const { service, fotoRepository } = createService({
        otRepository: { findOne: jest.fn(async () => ot()) },
      });

      await service.addFoto(7, { tipo: "general", url: "https://example.com/a.jpg" } as any, 1, { id: 1, rol: "admin" });

      expect(fotoRepository.save).toHaveBeenCalled();
    });

    it("con mecanicos apagado no se puede asignar mecanico al crear la OT", async () => {
      const { service } = createService({ flags: { "ot.usa_mecanicos": false } });

      await expect(service.create({ ...dtoBase, mecanicoId: 5 } as any, 1, { id: 1, rol: "admin" })).rejects.toThrow(
        "ot.usa_mecanicos",
      );
    });

    it("con mecanicos apagado, editar la OT sin mecanico NO desasigna al historico", async () => {
      const { service, otRepository } = createService({
        flags: { "ot.usa_mecanicos": false },
        otRepository: { findOne: jest.fn(async () => ot({ mecanicoId: 9 })) },
      });

      await service.update(7, { observaciones: "x", mecanicoId: null } as any, 1, { id: 1, rol: "admin" });

      expect(otRepository.save).toHaveBeenCalledWith(expect.objectContaining({ mecanicoId: 9 }));
    });

    it("con 'cosas que trae' apagado rechaza guardar elementos pero [] no borra los existentes", async () => {
      const { service, cosaTraeRepository } = createService({
        flags: { "ot.usa_cosas_trae": false },
        otRepository: { findOne: jest.fn(async () => ot()) },
      });

      await expect(service.update(7, { cosasTrae: [1] } as any, 1, { id: 1, rol: "admin" })).rejects.toThrow(
        "ot.usa_cosas_trae",
      );
      await service.update(7, { cosasTrae: [] } as any, 1, { id: 1, rol: "admin" });
      expect(cosaTraeRepository.delete).not.toHaveBeenCalled();
    });

    it("con firma del cliente apagada solo se exige la de recepcion para entregar", async () => {
      const { service } = createService({
        flags: { "ot.usa_firma_cliente": false },
        otRepository: {
          findOne: jest.fn(async () =>
            ot({ estado: EstadoOT.TERMINADA, fechaSalida: new Date(), firma: { firmaRecepcion: "y" } }),
          ),
        },
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).resolves.toBeDefined();
    });

    it("con ambas firmas apagadas se entrega sin firmas; con la de recepcion encendida y sin ella, no", async () => {
      const sinFirmas = ot({ estado: EstadoOT.TERMINADA, fechaSalida: new Date(), firma: null });
      const apagadas = createService({
        flags: { "ot.usa_firma_cliente": false, "ot.usa_firma_recepcion": false },
        otRepository: { findOne: jest.fn(async () => sinFirmas) },
      });
      await expect(apagadas.service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).resolves.toBeDefined();

      const encendida = createService({
        flags: { "ot.usa_firma_cliente": false },
        otRepository: { findOne: jest.fn(async () => ({ ...sinFirmas })) },
      });
      await expect(encendida.service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(
        /firma de recepción/,
      );
    });

    it("saveFirmas rechaza la firma cuyo flag esta apagado", async () => {
      const { service } = createService({
        flags: { "ot.usa_firma_cliente": false },
        otRepository: { findOne: jest.fn(async () => ot()) },
      });

      await expect(
        service.saveFirmas(7, { firmaCliente: "https://x/f.png", nombreFirmaCliente: "Ana" } as any, 1, { id: 1, rol: "admin" }),
      ).rejects.toThrow("ot.usa_firma_cliente");
    });

    it("sin fecha de salida obligatoria, entregar asigna la fecha actual en vez de bloquear", async () => {
      const { service, otRepository } = createService({
        flags: { "ot.exige_fecha_salida": false },
        otRepository: {
          findOne: jest.fn(async () =>
            ot({ estado: EstadoOT.TERMINADA, fechaSalida: null, firma: { firmaCliente: "x", firmaRecepcion: "y" } }),
          ),
        },
      });

      await service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1);

      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.ENTREGADA, fechaSalida: expect.any(Date) }),
      );
    });

    describe("datos de la entrega", () => {
      const SALIDA = "2026-04-29T18:00:00.000Z";
      const otTerminada = () =>
        ot({ estado: EstadoOT.TERMINADA, fechaSalida: null, firma: { firmaCliente: "x", firmaRecepcion: "y" } });

      it("guarda km, gasolina y recomendaciones de salida en la OT", async () => {
        const { service, otRepository } = createService({
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1, undefined, {
          kmSalida: 38900,
          nivelGasolinaSalida: 75,
          recomendacionesEntrega: "  Revisar frenos en 5,000 km  ",
        });

        expect(otRepository.save).toHaveBeenLastCalledWith(
          expect.objectContaining({
            estado: EstadoOT.ENTREGADA,
            kmSalida: 38900,
            nivelGasolinaSalida: 75,
            recomendacionesEntrega: "Revisar frenos en 5,000 km",
          }),
        );
      });

      it("guarda nombre y firma de quien entrega sin tocar la firma de recepcion", async () => {
        const firmaExistente = { otId: 7, firmaRecepcion: "recepcion.webp", nombreFirmaRecepcion: "Carolina" };
        const { service, firmaRepository } = createService({
          firmaRepository: { findOne: jest.fn(async () => ({ ...firmaExistente })) },
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1, undefined, {
          entregadoPor: " Melvin ",
          firmaEntrega: "/uploads/firmas/entrega.webp",
        });

        expect(firmaRepository.save).toHaveBeenCalledWith(
          expect.objectContaining({
            firmaRecepcion: "recepcion.webp",
            nombreFirmaRecepcion: "Carolina",
            firmaEntrega: "/uploads/firmas/entrega.webp",
            nombreFirmaEntrega: "Melvin",
            fechaFirmaEntrega: expect.any(Date),
          }),
        );
      });

      it("sin datos de entrega entrega igual (sync offline / clientes viejos)", async () => {
        const { service, otRepository, firmaRepository } = createService({
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1);

        expect(otRepository.save).toHaveBeenCalledWith(expect.objectContaining({ estado: EstadoOT.ENTREGADA }));
        expect(firmaRepository.save).not.toHaveBeenCalled();
      });

      it("rechaza la firma de entrega sin nombre", async () => {
        const { service } = createService({
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await expect(
          service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1, undefined, {
            firmaEntrega: "/uploads/firmas/entrega.webp",
          }),
        ).rejects.toThrow(/nombre de quien entrega/);
      });

      it("rechaza la firma de entrega si no es una imagen", async () => {
        const { service } = createService({
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await expect(
          service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1, undefined, {
            entregadoPor: "Melvin",
            firmaEntrega: "/uploads/firmas/entrega.exe",
          }),
        ).rejects.toThrow(/imagen valida/);
      });

      it("rechaza la firma de entrega si las firmas del taller estan apagadas", async () => {
        const { service } = createService({
          flags: { "ot.usa_firma_recepcion": false },
          otRepository: { findOne: jest.fn(async () => otTerminada()) },
        });

        await expect(
          service.changeEstado(7, EstadoOT.ENTREGADA, SALIDA, 1, undefined, {
            entregadoPor: "Melvin",
            firmaEntrega: "/uploads/firmas/entrega.webp",
          }),
        ).rejects.toThrow("ot.usa_firma_recepcion");
      });
    });

    it("con la fecha de salida obligatoria sigue bloqueando (control)", async () => {
      const { service } = createService({
        otRepository: {
          findOne: jest.fn(async () =>
            ot({ estado: EstadoOT.TERMINADA, fechaSalida: null, firma: { firmaCliente: "x", firmaRecepcion: "y" } }),
          ),
        },
      });

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(/fecha de salida/);
    });

    it("con 'una OT por vehiculo' apagado se puede crear aunque el vehiculo tenga otra OT abierta", async () => {
      const abierta = { numeroOt: "OT-0", vehiculo: { placa: "P123" } };
      const { service } = createService({
        flags: { "ot.una_ot_por_vehiculo": false },
        otRepository: { createQueryBuilder: jest.fn(() => createQueryBuilderMock(abierta)) },
      });

      await expect(service.create(dtoBase as any, 1, { id: 1, rol: "admin" })).resolves.toBeDefined();
    });

    it("con 'una OT por vehiculo' encendido bloquea (control)", async () => {
      const abierta = { numeroOt: "OT-0", vehiculo: { placa: "P123" } };
      const { service } = createService({
        otRepository: { createQueryBuilder: jest.fn(() => createQueryBuilderMock(abierta)) },
      });

      await expect(service.create(dtoBase as any, 1, { id: 1, rol: "admin" })).rejects.toThrow(/ya tiene una orden activa/);
    });
  });

  describe("pago detectado desde hdetmed de POSTouch", () => {
    const FACTURA = { id: 90, otId: 7, tienda: 1, caja: 2, tipo: "1", serie: "A", transac: 555 };
    const otFacturada = (extra: Record<string, any> = {}) => ({
      id: 7,
      numeroOt: "OT-1",
      clienteTarjeta: "CLI-1",
      tiendaId: 1,
      estado: EstadoOT.FACTURADA,
      fechaSalida: new Date(),
      version: 1,
      pagoConfirmadoAt: null,
      pagoConfirmadoPor: null,
      firma: { firmaCliente: "x", firmaRecepcion: "y" },
      cotizaciones: [],
      ...extra,
    });
    const armar = (consulta: any, extra: Record<string, any> = {}) =>
      createService({
        otRepository: { findOne: jest.fn(async () => otFacturada(extra.ot)) , ...extra.otRepository },
        otFacturacionRepository: { findOne: jest.fn(async () => FACTURA), ...extra.otFacturacionRepository },
        facturacionPosService: {
          facturacionAplica: jest.fn(async () => true),
          consultarPago: typeof consulta === "function" ? consulta : jest.fn(async () => consulta),
        },
      });
    const pagada = { estado: "pagada", total: 112, pagado: 112 };

    it("al refrescar una OT facturada cuyo pago ya se liquido en POSTouch, lo registra solo (sin confirmacion manual)", async () => {
      const { service, otRepository, otFacturacionRepository, eventoRepository } = armar(pagada);

      const resultado: any = await service.getFacturacionPos(7);

      expect(resultado).toEqual(
        expect.objectContaining({
          pagoConfirmado: true,
          pagoDetectadoEnPos: true,
          pagoPos: "pagada",
          pagoConfirmadoAt: expect.any(Date),
        }),
      );
      expect(otRepository.update).toHaveBeenCalledWith(
        { id: 7, pagoConfirmadoAt: expect.anything() },
        { pagoConfirmadoAt: expect.any(Date), pagoConfirmadoPor: null },
      );
      expect(otFacturacionRepository.update).toHaveBeenCalledWith({ id: 90 }, { fechaLiquidacion: expect.any(Date) });
      expect(eventosGuardados(eventoRepository).join("|")).toContain("Pago detectado en POSTouch");
    });

    it("la actualizacion es condicional: dos refrescos simultaneos no duplican el evento de bitacora", async () => {
      const { service, eventoRepository } = armar(pagada, {
        otRepository: { update: jest.fn(async () => ({ affected: 0 })) },
      });

      await service.getFacturacionPos(7);

      expect(eventosGuardados(eventoRepository).join("|")).not.toContain("Pago detectado");
    });

    it("si POSTouch aun lo tiene por liquidar, NO confirma nada", async () => {
      const { service, otRepository } = armar({ estado: "pendiente", total: 112, pagado: 0 });

      const resultado: any = await service.getFacturacionPos(7);

      expect(resultado.pagoConfirmado).toBe(false);
      expect(resultado.pagoPos).toBe("pendiente");
      expect(otRepository.update).not.toHaveBeenCalled();
    });

    it("si POSTouch no se puede leer, la pantalla NO se rompe: devuelve 'desconocido' y deja el respaldo manual", async () => {
      const { service, otRepository } = armar(
        jest.fn(async () => {
          throw new Error("Table 'hdetmed' doesn't exist");
        }),
      );

      const resultado: any = await service.getFacturacionPos(7);

      expect(resultado.pagoPos).toBe("desconocido");
      expect(resultado.pagoPosDetalle).toMatch(/No se pudo consultar el pago en POSTouch/);
      expect(otRepository.update).not.toHaveBeenCalled();
    });

    it("no consulta POSTouch si la OT no esta facturada o si el pago ya esta confirmado (el refresco de la pantalla no debe golpear la base de POSTouch en vano)", async () => {
      const terminada = armar(pagada, { ot: { estado: EstadoOT.TERMINADA } });
      await terminada.service.getFacturacionPos(7);
      expect(terminada.facturacionPosService.consultarPago).not.toHaveBeenCalled();

      const yaPagada = armar(pagada, { ot: { pagoConfirmadoAt: new Date() } });
      await yaPagada.service.getFacturacionPos(7);
      expect(yaPagada.facturacionPosService.consultarPago).not.toHaveBeenCalled();
    });

    it("al entregar, si el cliente ya pago en caja se entrega SIN pedir confirmacion manual", async () => {
      const { service, otRepository } = armar(pagada);

      await service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1);

      expect(otRepository.save).toHaveBeenLastCalledWith(
        expect.objectContaining({ estado: EstadoOT.ENTREGADA, pagoConfirmadoAt: expect.any(Date) }),
      );
    });

    it.each([
      [{ estado: "pendiente", total: 1, pagado: 0 }, /sigue pendiente de liquidar en POSTouch/],
      [{ estado: "anulada", total: 1, pagado: 1 }, /anulada en POSTouch/],
      [{ estado: "no_encontrada", total: 0, pagado: 0 }, /No se pudo verificar el pago.*confirmarlo manualmente/],
    ])("al entregar con POSTouch diciendo %p se bloquea con un mensaje claro", async (consulta, mensaje) => {
      const { service } = armar(consulta);

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(mensaje);
    });

    it("al entregar con POSTouch inaccesible se bloquea y ofrece la confirmacion manual", async () => {
      const { service } = armar(
        jest.fn(async () => {
          throw new Error("ECONNREFUSED");
        }),
      );

      await expect(service.changeEstado(7, EstadoOT.ENTREGADA, undefined, 1)).rejects.toThrow(
        /No se pudo verificar el pago en POSTouch.*administrador puede confirmarlo manualmente/,
      );
    });

    it("confirmar manualmente: si POSTouch ya dice pagada, se reconoce desde hdetmed (no queda como manual)", async () => {
      const { service, otRepository } = armar(pagada);

      await service.confirmarPago(7, 4, { id: 4, rol: "admin" });

      expect(otRepository.save).not.toHaveBeenCalled();
      expect(otRepository.update).toHaveBeenCalledWith(
        expect.anything(),
        { pagoConfirmadoAt: expect.any(Date), pagoConfirmadoPor: null },
      );
    });

    it("confirmar manualmente una factura ANULADA en POSTouch se rechaza", async () => {
      const { service } = armar({ estado: "anulada", total: 1, pagado: 1 });

      await expect(service.confirmarPago(7, 4, { id: 4, rol: "admin" })).rejects.toThrow(/anulada en POSTouch/);
    });

    it("el respaldo manual con POSTouch aun 'por liquidar' se permite, pero deja constancia de lo que decia POSTouch", async () => {
      const { service, otRepository, eventoRepository } = armar({ estado: "pendiente", total: 1, pagado: 0 });

      await service.confirmarPago(7, 4, { id: 4, rol: "admin" });

      expect(otRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ pagoConfirmadoAt: expect.any(Date), pagoConfirmadoPor: 4 }),
      );
      const eventos = eventosGuardados(eventoRepository).join("|");
      expect(eventos).toContain("manualmente");
      expect(eventos).toContain("Pendiente de liquidar en POSTouch");
    });
  });
});
