import { BadRequestException } from '@nestjs/common';

import { ContractBookingService } from './contract-booking.service';
import { Contract } from './entities/contract.entity';

/**
 * Contract auto-completion by quantity cap. Once a GENERAL contract's capped
 * scope is fully consumed (e.g. a split remainder rebooked), the contract moves
 * to CONTRACT_CLOSED even inside its validity window, and further bookings are
 * blocked — including while a booking window is open. Released capacity
 * (cancelled/expired booking) reopens the contract on the next attempt.
 */
describe('ContractBookingService — quantity-cap completion', () => {
  function makeService() {
    const contractsRepository = {
      findByIdWithRelations: jest.fn(),
      update: jest.fn().mockResolvedValue(undefined),
    };
    const service = new ContractBookingService(
      contractsRepository as never,
      {} as never, // bookingsRepository
      {} as never, // bookingPricingService
      {} as never, // consolidationService
      {} as never, // containerTypesService
      {} as never, // ruleEngineService
      {} as never, // milestoneService
      {} as never, // workflowService
      {} as never, // invoiceService
      { createdToStaff: jest.fn() } as never, // bookingNotifier
      {} as never, // dataSource
      {} as never, // trainSchedulingService
      {} as never, // bookingBatchService
      {} as never, // bookingTransitionService
    );
    return { service, contractsRepository };
  }

  type WithPrivate = {
    maybeCompleteContract: (c: Contract) => Promise<void>;
  };

  const generalContract = (status: string): Contract =>
    ({
      id: 'c-1',
      reference: 'CTR-1',
      contractKind: 'GENERAL',
      status,
    }) as Contract;

  it('closes a GENERAL contract when every capped line is exhausted', async () => {
    const { service, contractsRepository } = makeService();
    jest.spyOn(service, 'computeCapacity').mockResolvedValue([
      { containerSize: '20FT', cap: 10, booked: 10, remaining: 0 },
      { containerSize: '40FT', cap: 4, booked: 4, remaining: 0 },
    ]);

    await (service as never as WithPrivate).maybeCompleteContract(
      generalContract('CONTRACT_ACTIVE'),
    );

    expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
      status: 'CONTRACT_CLOSED',
    });
  });

  it('absorbs bulk-ton float dust when judging exhaustion', async () => {
    const { service, contractsRepository } = makeService();
    jest
      .spyOn(service, 'computeCapacity')
      .mockResolvedValue([{ cap: 100, booked: 99.9995, remaining: 0.0005 }]);

    await (service as never as WithPrivate).maybeCompleteContract(
      generalContract('FULLY_EXECUTED'),
    );

    expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
      status: 'CONTRACT_CLOSED',
    });
  });

  it('keeps the contract open while any capped line has capacity left', async () => {
    const { service, contractsRepository } = makeService();
    jest.spyOn(service, 'computeCapacity').mockResolvedValue([
      { containerSize: '20FT', cap: 10, booked: 10, remaining: 0 },
      { containerSize: '40FT', cap: 4, booked: 3, remaining: 1 },
    ]);

    await (service as never as WithPrivate).maybeCompleteContract(
      generalContract('CONTRACT_ACTIVE'),
    );

    expect(contractsRepository.update).not.toHaveBeenCalled();
  });

  it('never closes an uncapped contract', async () => {
    const { service, contractsRepository } = makeService();
    jest.spyOn(service, 'computeCapacity').mockResolvedValue([]);

    await (service as never as WithPrivate).maybeCompleteContract(
      generalContract('CONTRACT_ACTIVE'),
    );

    expect(contractsRepository.update).not.toHaveBeenCalled();
  });

  it('never closes a ONE_TIME contract (single-slot rule governs it)', async () => {
    const { service, contractsRepository } = makeService();
    const spy = jest.spyOn(service, 'computeCapacity');

    await (service as never as WithPrivate).maybeCompleteContract({
      id: 'c-1',
      contractKind: 'ONE_TIME',
      status: 'FULLY_EXECUTED',
    } as Contract);

    expect(spy).not.toHaveBeenCalled();
    expect(contractsRepository.update).not.toHaveBeenCalled();
  });

  it('rejects a new booking on a completed contract even inside an open window', async () => {
    const { service, contractsRepository } = makeService();
    contractsRepository.findByIdWithRelations.mockResolvedValue(
      generalContract('CONTRACT_CLOSED'),
    );
    jest
      .spyOn(service, 'computeCapacity')
      .mockResolvedValue([{ cap: 10, booked: 10, remaining: 0 }]);

    await expect(
      service.createUnderContract('c-1', {} as never, null, null),
    ).rejects.toThrow(BadRequestException);
    expect(contractsRepository.update).not.toHaveBeenCalled();
  });

  it('reopens a completed contract when capacity was released', async () => {
    const { service, contractsRepository } = makeService();
    contractsRepository.findByIdWithRelations.mockResolvedValue(
      generalContract('CONTRACT_CLOSED'),
    );
    jest
      .spyOn(service, 'computeCapacity')
      .mockResolvedValue([{ cap: 10, booked: 8, remaining: 2 }]);

    // The create path continues past the gate and dies later on the bare mocks —
    // only the reopen transition is under test here.
    await service.createUnderContract('c-1', {} as never, null, null).catch(() => undefined);

    expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
      status: 'CONTRACT_ACTIVE',
    });
  });
});
