import { BadRequestException, NotFoundException } from "@nestjs/common";
import { EstadoOT } from "../ots/entities/orden-trabajo.entity";
import { TipoEventoOT } from "../ots/entities/ot-evento.entity";
import { CotizacionesService } from "./cotizaciones.service";
import { Cotizacion, EstadoCotizacion } from "./entities/cotizacion.entity";
import { CotizacionLinea } from "./entities/cotizacion-linea.entity";
import { CotizacionLock } from "./entities/cotizacion-lock.entity";
import { CotizacionVersion } from "./entities/cotizacion-version.entity";

function createService(overrides: Record<string, any> = {}) {
  const baseCotizacion: Cotizacion = {
    id: 5,
    otId: 7,
    numeroCotizacion: "COT-20260429-0001",
    subtotal: 100,
    iva: 12,
    total: 112,
    estado: EstadoCotizacion.BORRADOR,
    fechaAprobacion: null as any,
    aprobadoPor: null as any,
    asesorId: null,
    asesorNombre: null,
    publicToken: null,
    publicExpiresAt: null,
    decisionClienteAt: null,
    decisionClienteNombre: null,
    decisionClienteFirma: null,
    decisionClienteMotivo: null,
    notas: null as any,
    localId: null as any,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ordenTrabajo: { id: 7, estado: EstadoOT.EN_PROCESO } as any,
    lineas: [
      {
        id: 11,
        cotizacionId: 5,
        plu: "REP-1",
        descripcion: "Filtro",
        precioUnitario: 50,
        cantidad: 2,
        subtotal: 100,
        iva: 12,
        total: 112,
        esServicio: false,
        orden: 0,
        createdAt: new Date(),
      } as any,
    ],
  };

  const cotizacionRepository = {
    findOne: jest.fn(async () => baseCotizacion),
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => ({ ...baseCotizacion, ...data })),
    update: jest.fn(async () => ({ affected: 1 })),
    createQueryBuilder: jest.fn(() => ({
      where: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      getOne: jest.fn(async () => null),
    })),
    ...overrides.cotizacionRepository,
  };

  const lineaRepository = {
    find: jest.fn(async () => baseCotizacion.lineas),
    findOne: jest.fn(async () => baseCotizacion.lineas[0]),
    create: jest.fn((data) => ({ id: 12, ...data })),
    save: jest.fn(async (data) => data),
    delete: jest.fn(async () => ({ affected: 1 })),
    ...overrides.lineaRepository,
  };

  const lockRepository = {
    findOne: jest.fn(async () => ({
      id: 1,
      cotizacionId: 5,
      token: "lock-token",
      usuarioId: 9,
      usuarioNombre: "tester",
      expiresAt: new Date(Date.now() + 60000),
    })),
    create: jest.fn((data) => ({ ...data })),
    save: jest.fn(async (data) => data),
    delete: jest.fn(async () => ({ affected: 1 })),
    ...overrides.lockRepository,
  };

  const productosService = {
    findByPlu: jest.fn(async () => ({
      plu: "REP-1",
      desclarga: "Filtro",
      desccorta: "Filtro",
      precio: 50,
      pagaiva: true,
      iva: 12,
      es_servicio: false,
    })),
    getCostoByPlu: jest.fn(async () => 0),
    ...overrides.productosService,
  };

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

  const otsService = {
    findById: jest.fn(async () => ({ id: 7, estado: EstadoOT.EN_PROCESO })),
    logEvento: jest.fn(async () => undefined),
    markCotizadaFromCotizacion: jest.fn(async () => undefined),
    markAprobadaFromCotizacion: jest.fn(async () => undefined),
    ...overrides.otsService,
  };

  const tenantAccessor = {
    repositoryFor: jest.fn((entity: any) => {
      if (entity === CotizacionLinea) return lineaRepository as any;
      if (entity === CotizacionLock) return lockRepository as any;
      if (entity === CotizacionVersion) return versionRepository as any;
      return cotizacionRepository as any;
    }),
    getCurrentTenantId: jest.fn(() => 1),
    runInTransaction: jest.fn(async (fn: any) =>
      fn({
        get: (entity: any) => {
          if (entity === CotizacionLinea) return lineaRepository as any;
          if (entity === CotizacionLock) return lockRepository as any;
          if (entity === CotizacionVersion) return versionRepository as any;
          return cotizacionRepository as any;
        },
      }),
    ),
  };

  const service = new CotizacionesService(
    tenantAccessor as any,
    productosService as any,
    otsService as any,
  );

  return {
    service,
    baseCotizacion,
    cotizacionRepository,
    lineaRepository,
    lockRepository,
    versionRepository,
    productosService,
    otsService,
  };
}

describe("CotizacionesService", () => {
  it("suma cantidad cuando el producto ya existe en la cotización", async () => {
    const { service, lineaRepository, cotizacionRepository, otsService } =
      createService();

    const result = await service.addLinea(
      5,
      { plu: "REP-1", cantidad: 3, lockToken: "lock-token" },
      9,
    );

    expect(result.duplicado).toBe(true);
    expect(result.mensaje).toContain("actualizó la cantidad");
    expect(lineaRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 11,
        cantidad: 5,
        subtotal: 250,
        iva: 30,
        total: 280,
        orden: 0,
      }),
    );
    expect(cotizacionRepository.update).toHaveBeenCalled();
    expect(otsService.logEvento).toHaveBeenCalledWith(
      7,
      TipoEventoOT.PRODUCTO_AGREGADO,
      expect.stringContaining("Producto actualizado"),
      9,
    );
  });

  it("rechaza agregar cantidades inválidas", async () => {
    const { service } = createService();

    await expect(
      service.addLinea(
        5,
        { plu: "REP-1", cantidad: 0, lockToken: "lock-token" },
        9,
      ),
    ).rejects.toThrow(
      new BadRequestException("La cantidad debe ser mayor a cero"),
    );
  });

  it("rechaza editar una cotización aprobada", async () => {
    const { service } = createService({
      cotizacionRepository: {
        findOne: jest.fn(async () => ({
          estado: EstadoCotizacion.APROBADA,
          lineas: [],
          ordenTrabajo: { estado: EstadoOT.EN_PROCESO },
        })),
      },
    });

    await expect(
      service.addLinea(
        5,
        { plu: "REP-1", cantidad: 1, lockToken: "lock-token" },
        9,
      ),
    ).rejects.toThrow("La cotización ya fue aprobada y no puede modificarse");
  });

  it("rechaza aprobar una cotización sin líneas", async () => {
    const { service } = createService({
      cotizacionRepository: {
        findOne: jest.fn(async () => ({
          id: 5,
          estado: EstadoCotizacion.BORRADOR,
          lineas: [],
        })),
      },
    });

    await expect(
      service.aprobar(5, "asesor", "lock-token", { id: 9 }),
    ).rejects.toThrow("No se puede aprobar una cotizacion sin lineas");
  });

  it("rechaza aprobar una cotización ya aprobada", async () => {
    const { service } = createService({
      cotizacionRepository: {
        findOne: jest.fn(async () => ({
          id: 5,
          estado: EstadoCotizacion.APROBADA,
          lineas: [{ id: 11 }],
        })),
      },
    });

    await expect(
      service.aprobar(5, "asesor", "lock-token", { id: 9 }),
    ).rejects.toThrow("La cotización ya fue aprobada");
  });

  it("mantiene el backend como fuente de verdad al crear línea nueva", async () => {
    const { service, lineaRepository } = createService({
      cotizacionRepository: {
        findOne: jest.fn(async () => ({
          id: 5,
          otId: 7,
          estado: EstadoCotizacion.BORRADOR,
          lineas: [],
          ordenTrabajo: { estado: EstadoOT.EN_PROCESO },
        })),
      },
      lineaRepository: {
        find: jest.fn(async () => [{ subtotal: 100, iva: 12, total: 112 }]),
      },
    });

    const result = await service.addLinea(
      5,
      { plu: "REP-1", cantidad: 2, lockToken: "lock-token" },
      9,
    );

    expect(result.duplicado).toBe(false);
    expect(lineaRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        precioUnitario: expect.closeTo(44.642857, 5),
        cantidad: 2,
        subtotal: expect.closeTo(89.285714, 5),
        iva: expect.closeTo(10.714286, 5),
        total: 100,
      }),
    );
  });

  it("lanza NotFound cuando no existe la cotización", async () => {
    const { service } = createService({
      cotizacionRepository: {
        findOne: jest.fn(async () => null),
      },
    });

    await expect(service.findById(999)).rejects.toThrow(NotFoundException);
  });
});
