import { BadRequestException } 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),
    ...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),
    ...overrides.otFacturacionRepository,
  };

  const facturacionPosService = {
    enviarOrdenTerminada: jest.fn(async () => ({
      exito: false,
      error: "tenant sin facturacion habilitada",
    })),
    ...overrides.facturacionPosService,
  };

  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,
  );

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

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 vehiculo"));
  });

  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 vehiculo.",
    );
  });

  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", () => {
    function otEnProcesoConCotizacionAprobada() {
      return {
        id: 7,
        numeroOt: "OT-20260429-0001",
        clienteTarjeta: "CLI-1",
        estado: EstadoOT.EN_PROCESO,
        fechaSalida: null,
        version: 1,
        cotizaciones: [
          {
            estado: EstadoCotizacion.APROBADA,
            lineas: [
              {
                plu: "TLLP-001",
                descripcion: "Cambio de aceite",
                precioUnitario: "100.00",
                cantidad: "1.00",
                esServicio: true,
              },
            ],
          },
        ],
      };
    }

    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);
    });

    it("al pasar a TERMINADA con facturacion habilitada y envio exitoso, queda en FACTURADA", async () => {
      const { service, otRepository, facturacionPosService } = createService({
        otRepository: {
          findOne: jest.fn(async () => otEnProcesoConCotizacionAprobada()),
        },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: true })),
        },
      });

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

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

    it("al pasar a TERMINADA y el envio falla, queda en TERMINADA con un evento de error", async () => {
      const { service, otRepository, eventoRepository, facturacionPosService } =
        createService({
          otRepository: {
            findOne: jest.fn(async () => otEnProcesoConCotizacionAprobada()),
          },
          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(eventoRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          descripcion: expect.stringContaining("POSTouch no responde"),
        }),
      );
    });

    it("al pasar a TERMINADA sin cotizacion aprobada, no falla la transicion y queda en TERMINADA con un evento de error", async () => {
      const { service, otRepository, eventoRepository, facturacionPosService } =
        createService({
          otRepository: {
            findOne: jest.fn(async () => ({
              id: 7,
              numeroOt: "OT-20260429-0001",
              clienteTarjeta: "CLI-1",
              estado: EstadoOT.EN_PROCESO,
              fechaSalida: null,
              version: 1,
              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(eventoRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          descripcion: expect.stringContaining(
            "No se pudo enviar a POSTouch automaticamente",
          ),
        }),
      );
    });
  });

  describe("getFacturacionPos", () => {
    it("devuelve el registro de OtFacturacion de la OT", async () => {
      const registro = { otId: 7, tienda: 1, caja: 2, tipo: "FAC", serie: "A", transac: 555 };
      const { service, otFacturacionRepository } = createService({
        otFacturacionRepository: { findOne: jest.fn(async () => registro) },
      });

      const result = await service.getFacturacionPos(7);

      expect(otFacturacionRepository.findOne).toHaveBeenCalledWith({ where: { otId: 7 } });
      expect(result).toEqual(registro);
    });

    it("devuelve null si la OT nunca se facturo", async () => {
      const { service } = createService();

      expect(await service.getFacturacionPos(7)).toBeNull();
    });
  });

  describe("reintentarFacturacionPos", () => {
    function otTerminadaConCotizacionAprobada() {
      return {
        id: 7,
        numeroOt: "OT-20260429-0001",
        clienteTarjeta: "CLI-1",
        estado: EstadoOT.TERMINADA,
        fechaSalida: null,
        version: 1,
        cotizaciones: [
          {
            estado: EstadoCotizacion.APROBADA,
            lineas: [
              {
                plu: "TLLP-001",
                descripcion: "Cambio de aceite",
                precioUnitario: "100.00",
                cantidad: "1.00",
                esServicio: true,
              },
            ],
          },
        ],
      };
    }

    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,
      );
    });

    it("reintenta el envio y pasa la OT a FACTURADA si esta vez tiene exito", async () => {
      const { service, otRepository, facturacionPosService } = createService({
        otRepository: { findOne: jest.fn(async () => otTerminadaConCotizacionAprobada()) },
        facturacionPosService: {
          enviarOrdenTerminada: jest.fn(async () => ({ exito: true })),
        },
      });

      await service.reintentarFacturacionPos(7, 1);

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