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

import {
  assertBulkTonnageRemains,
  assertTruckCountWithinContainers,
  assertTruckLoad,
  remainingBulkTons,
} from './truck-load.util';

/**
 * One physical rule, shared by customer self-haul and EDR last-mile. It used to
 * be written out three times (addTruck, updateTruck, departTruck) plus a fourth
 * in LastMileService.
 */
describe('assertTruckLoad', () => {
  const booking = ['ABCD1234567', 'ABCD7654321', 'WXYZ1111111'];

  it('accepts two 20ft containers on one truck', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ABCD1234567', 'ABCD7654321'],
        bookingContainers: booking,
        sizes: ['20ft', '20ft'],
      }),
    ).not.toThrow();
  });

  it('accepts a single 40ft container', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ABCD1234567'],
        bookingContainers: booking,
        sizes: ['40ft'],
      }),
    ).not.toThrow();
  });

  it('rejects a 40ft sharing the truck — it fills the bed', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ABCD1234567', 'ABCD7654321'],
        bookingContainers: booking,
        sizes: ['40ft', '20ft'],
      }),
    ).toThrow(BadRequestException);
  });

  it('rejects more than two containers', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ABCD1234567', 'ABCD7654321', 'WXYZ1111111'],
        bookingContainers: booking,
        sizes: ['20ft', '20ft', '20ft'],
      }),
    ).toThrow(BadRequestException);
  });

  it('rejects a container that is not on the booking', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ZZZZ9999999'],
        bookingContainers: booking,
        sizes: ['20ft'],
      }),
    ).toThrow(BadRequestException);
  });

  it('rejects a container already riding another truck', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['ABCD1234567'],
        bookingContainers: booking,
        sizes: ['20ft'],
        assignedElsewhere: ['ABCD1234567'],
      }),
    ).toThrow(ConflictException);
  });

  it('skips membership checks when the booking has no containers (bulk)', () => {
    expect(() =>
      assertTruckLoad({ containers: [], bookingContainers: [], sizes: [] }),
    ).not.toThrow();
  });

  it('still caps the count when the booking has no containers', () => {
    expect(() =>
      assertTruckLoad({
        containers: ['A', 'B', 'C'],
        bookingContainers: [],
        sizes: [],
      }),
    ).toThrow(BadRequestException);
  });
});

describe('assertBulkTonnageRemains', () => {
  it('allows another truck while tonnage is left', () => {
    expect(() => assertBulkTonnageRemains(100, 40)).not.toThrow();
  });

  it('rejects a truck once the booking is fully hauled', () => {
    expect(() => assertBulkTonnageRemains(100, 0)).toThrow(BadRequestException);
  });

  it('does not cap a booking with no declared weight', () => {
    // Nothing to draw down against — capping here would block every truck.
    expect(() => assertBulkTonnageRemains(0, 0)).not.toThrow();
  });
});

describe('remainingBulkTons', () => {
  const dataSourceReturning = (totalTons: string, hauledTons: string) =>
    ({ query: jest.fn().mockResolvedValue([{ totalTons, hauledTons }]) }) as never;

  it('counts trucks from both haulage paths against the declared weight', async () => {
    const result = await remainingBulkTons(dataSourceReturning('100', '60'), 'b-1');

    expect(result).toEqual({
      totalTons: 100,
      hauledTons: 60,
      remainingTons: 40,
      complete: false,
    });
  });

  it('is complete once everything is hauled', async () => {
    const result = await remainingBulkTons(dataSourceReturning('100', '100'), 'b-1');

    expect(result.remainingTons).toBe(0);
    expect(result.complete).toBe(true);
  });

  it('never reports negative tonnage when trucks overshoot', async () => {
    const result = await remainingBulkTons(dataSourceReturning('100', '104'), 'b-1');

    expect(result.remainingTons).toBe(0);
    expect(result.complete).toBe(true);
  });

  it('is not complete for a booking with no declared weight', async () => {
    const result = await remainingBulkTons(dataSourceReturning('0', '0'), 'b-1');

    expect(result.complete).toBe(false);
  });
});

describe('assertTruckCountWithinContainers', () => {
  it('allows one truck per container', () => {
    expect(() => assertTruckCountWithinContainers(3, 3)).not.toThrow();
  });

  it('rejects more trucks than containers', () => {
    expect(() => assertTruckCountWithinContainers(4, 3)).toThrow(BadRequestException);
  });

  it('does not cap a bulk booking, which has no container count', () => {
    expect(() => assertTruckCountWithinContainers(9, 0)).not.toThrow();
  });
});
