import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { inm_proyecto_tipo, inm_titular_tipo } from '@prisma/client';
import { codigoCentroCosto, ProyectosInmService } from './proyectos.service';

describe('ProyectosInmService', () => {
  const crear = (opts: { proyectoExistente?: unknown; ccExistente?: unknown; hitosEnEtapa?: number } = {}) => {
    const tx: any = {
      cont_centros_costo: {
        findFirst: jest.fn().mockResolvedValue(opts.ccExistente ?? null),
        create: jest.fn().mockResolvedValue({ id: 'cc-nuevo' }),
      },
      inm_proyectos: { create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'p-1', ...data })) },
    };
    const prisma: any = {
      inm_proyectos: {
        findFirst: jest
          .fn()
          .mockImplementation(({ where }) =>
            Promise.resolve(
              where.codigo
                ? (opts.proyectoExistente ?? null)
                : { id: 'p-1', empresa_id: 'emp-1', titular_tipo: 'PROPIO', nombre: 'X' },
            ),
          ),
        update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'p-1', ...data })),
        findMany: jest.fn().mockResolvedValue([]),
      },
      inm_etapas: {
        findFirst: jest.fn().mockResolvedValue({ id: 'et-1', empresa_id: 'emp-1', proyecto_id: 'p-1' }),
        update: jest.fn().mockImplementation(({ data }) => Promise.resolve(data)),
      },
      inm_hitos: {
        count: jest.fn().mockResolvedValue(opts.hitosEnEtapa ?? 0),
        findFirst: jest
          .fn()
          .mockResolvedValue({ id: 'h-1', empresa_id: 'emp-1', proyecto_id: 'p-1', fecha_cumplido: null }),
        update: jest.fn().mockImplementation(({ data }) => Promise.resolve(data)),
      },
      cont_centros_costo: { findFirst: jest.fn().mockResolvedValue({ id: 'cc-1' }) },
      tes_cuentas: { findFirst: jest.fn().mockResolvedValue({ id: 'cta-1' }) },
      moneda: {
        findFirst: jest.fn().mockResolvedValue({ id: 'pyg' }),
        findUnique: jest.fn().mockResolvedValue({ id: 'usd' }),
      },
      $transaction: jest.fn().mockImplementation((cb: any) => cb(tx)),
    };
    const audit = { log: jest.fn().mockResolvedValue(undefined) };
    const service = new ProyectosInmService(prisma, audit as any);
    jest.spyOn(service, 'detalle').mockImplementation((id) => Promise.resolve({ id } as any));
    return { service, prisma, tx, audit };
  };

  const base = { codigo: 'torre-sol', nombre: 'Torre Sol', tipo: inm_proyecto_tipo.EDIFICIO };

  it('sin centro de costo elegido, crea uno con el código del proyecto y guarda en guaraníes por defecto', async () => {
    const { service, tx } = crear();
    await service.crear(base, 'emp-1', 'u-1');

    expect(tx.cont_centros_costo.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ empresa_id: 'emp-1', codigo: 'INM-TORRE-SOL' }) }),
    );
    expect(tx.inm_proyectos.create.mock.calls[0][0].data).toEqual(
      expect.objectContaining({
        codigo: 'TORRE-SOL',
        centro_costo_id: 'cc-nuevo',
        moneda_id: 'pyg',
        empresa_id: 'emp-1',
      }),
    );
  });

  it('si el centro de costo del proyecto ya existía, lo reusa en vez de duplicarlo', async () => {
    const { service, tx } = crear({ ccExistente: { id: 'cc-viejo' } });
    await service.crear(base, 'emp-1');
    expect(tx.cont_centros_costo.create).not.toHaveBeenCalled();
    expect(tx.inm_proyectos.create.mock.calls[0][0].data.centro_costo_id).toBe('cc-viejo');
  });

  it('código repetido en la empresa: 409', async () => {
    const { service } = crear({ proyectoExistente: { id: 'otro' } });
    await expect(service.crear(base, 'emp-1')).rejects.toBeInstanceOf(ConflictException);
  });

  it('fideicomiso sin fiduciaria: 400', async () => {
    const { service } = crear();
    await expect(
      service.crear({ ...base, titular_tipo: inm_titular_tipo.FIDEICOMISO }, 'emp-1'),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('un centro de costo de otra empresa no se acepta', async () => {
    const { service, prisma } = crear();
    prisma.cont_centros_costo.findFirst.mockResolvedValue(null);
    await expect(service.crear({ ...base, centro_costo_id: 'cc-ajeno' }, 'emp-1')).rejects.toThrow(/centro de costo/);
    expect(prisma.cont_centros_costo.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'cc-ajeno', empresa_id: 'emp-1' } }),
    );
  });

  it('una etapa con hitos vivos no se da de baja', async () => {
    const { service } = crear({ hitosEnEtapa: 2 });
    await expect(service.darDeBajaEtapa('et-1', 'emp-1')).rejects.toThrow(/2 hito/);
  });

  it('cumplir un hito registra fecha y usuario; cumplirlo dos veces es 409', async () => {
    const { service, prisma } = crear();
    await service.cumplirHito('h-1', { fecha_cumplido: '2027-09-02' }, 'emp-1', 'u-9');
    const data = prisma.inm_hitos.update.mock.calls[0][0].data;
    expect(data.fecha_cumplido.toISOString().slice(0, 10)).toBe('2027-09-02');
    expect(data.cumplido_por).toBe('u-9');

    prisma.inm_hitos.findFirst.mockResolvedValue({ id: 'h-1', fecha_cumplido: new Date() });
    await expect(service.cumplirHito('h-1', {}, 'emp-1')).rejects.toBeInstanceOf(ConflictException);
  });

  it('todo filtra por empresa: un id de otra empresa da 404', async () => {
    const { service, prisma } = crear();
    prisma.inm_hitos.findFirst.mockResolvedValue(null);
    await expect(service.revertirHito('h-ajeno', 'emp-1')).rejects.toBeInstanceOf(NotFoundException);
    expect(prisma.inm_hitos.findFirst).toHaveBeenCalledWith({ where: { id: 'h-ajeno', empresa_id: 'emp-1' } });

    await service.listar('emp-1', { q: 'sol' });
    expect(prisma.inm_proyectos.findMany.mock.calls[0][0].where.empresa_id).toBe('emp-1');
  });

  it('el código del centro de costo entra en los 20 caracteres de la columna', () => {
    expect(codigoCentroCosto('abcdefghijklmnop')).toBe('INM-ABCDEFGHIJKLMNOP');
    expect(codigoCentroCosto('abcdefghijklmnop').length).toBeLessThanOrEqual(20);
  });
});
