import { BookingPricingService } from './booking-pricing.service';
import type { Booking } from './entities/booking.entity';
import type { Rate } from '../rule-engine/entities/rate.entity';

const MOCK_CBE_RATE = 130;

// Base freight is configured per leg, so every rate and every booking names the
// route it runs. MOJO → DIRE is the corridor these rates are priced for.
const MOJO = 'yard-mojo';
const DIRE = 'yard-dire-dawa';
const LEBU = 'yard-lebu';

describe('BookingPricingService — domestic corridor', () => {
  const intercityBulkUsd: Rate = {
    id: 'rate-intercity-bulk-usd',
    rateType: 'INTERCITY_BULK',
    currency: 'USD',
    rateValue: 35,
    rateUnit: 'PER_TON',
    status: 'LIVE',
    containerTypeId: null,
    originYardId: MOJO,
    destinationYardId: DIRE,
  } as Rate;

  const intercityContainerUsd: Rate = {
    id: 'rate-intercity-container-usd',
    rateType: 'INTERCITY_CONTAINER',
    currency: 'USD',
    rateValue: 400,
    rateUnit: 'PER_CONTAINER',
    status: 'LIVE',
    containerTypeId: null,
    originYardId: MOJO,
    destinationYardId: DIRE,
  } as Rate;

  let service: BookingPricingService;
  let bookingsRepository: { calculateWagonCount: jest.Mock };
  let ratesService: { findLiveRates: jest.Mock };
  let exchangeService: { getRate: jest.Mock };

  beforeEach(() => {
    bookingsRepository = { calculateWagonCount: jest.fn().mockResolvedValue(2) };
    ratesService = {
      findLiveRates: jest.fn().mockResolvedValue([intercityBulkUsd, intercityContainerUsd]),
    };
    exchangeService = {
      getRate: jest.fn().mockResolvedValue(MOCK_CBE_RATE),
    };

    service = new BookingPricingService(
      bookingsRepository as never,
      {} as never,
      {} as never,
      ratesService as never,
      exchangeService as never,
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      {} as never,
    );
  });

  it('prices domestic bulk in ETB using INTERCITY_BULK USD rate × CBE exchange rate', async () => {
    const booking = {
      id: 'b-1',
      freightType: 'BULK',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'ETB',
      cargoTotalWeightVgm: 120,
      originYardId: MOJO,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: { containers: [] },
        ) => Promise<{ lineItems: Array<{ amount: number; code: string; currency: string }> }>;
      }
    ).computeBaseRailLinesWithRates(booking, { containers: [] });

    expect(result.lineItems).toHaveLength(1);
    expect(result.lineItems[0].code).toBe('INTERCITY_BULK');
    expect(result.lineItems[0].currency).toBe('ETB');
    expect(result.lineItems[0].amount).toBe(Math.round(35 * 120 * MOCK_CBE_RATE));
  });

  it('prices domestic bulk in USD using INTERCITY_BULK USD rate directly', async () => {
    const booking = {
      id: 'b-1-usd',
      freightType: 'BULK',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'USD',
      cargoTotalWeightVgm: 120,
      originYardId: MOJO,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: { containers: [] },
        ) => Promise<{ lineItems: Array<{ amount: number; code: string; currency: string }> }>;
      }
    ).computeBaseRailLinesWithRates(booking, { containers: [] });

    expect(result.lineItems).toHaveLength(1);
    expect(result.lineItems[0].code).toBe('INTERCITY_BULK');
    expect(result.lineItems[0].currency).toBe('USD');
    expect(result.lineItems[0].amount).toBe(35 * 120);
  });

  it('prices domestic container in ETB using INTERCITY_CONTAINER USD fallback × CBE rate', async () => {
    const booking = {
      id: 'b-2',
      freightType: 'CONTAINER',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'ETB',
      cargoTotalWeightVgm: 50,
      originYardId: MOJO,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: {
            containers: Array<{ containerTypeId: string; quantity: number }>;
          },
        ) => Promise<{ lineItems: Array<{ amount: number; code: string; currency: string }> }>;
      }
    ).computeBaseRailLinesWithRates(booking, {
      containers: [{ containerTypeId: 'ct-20', quantity: 3 }],
    });

    expect(result.lineItems.some((l) => l.code === 'INTERCITY_CONTAINER')).toBe(true);
    const line = result.lineItems.find((l) => l.code === 'INTERCITY_CONTAINER')!;
    expect(line.currency).toBe('ETB');
  });

  // Rates are quoted per leg, so one configured for MOJO → DIRE must not price a
  // shipment that runs LEBU → DIRE. Charging the wrong corridor's price because
  // nobody configured this one yet is worse than billing no base freight.
  it('does not price bulk off a rate configured for a different leg', async () => {
    const booking = {
      id: 'b-3',
      freightType: 'BULK',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'USD',
      cargoTotalWeightVgm: 120,
      originYardId: LEBU,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: { containers: [] },
        ) => Promise<{ lineItems: Array<{ amount: number }>; blocked: string[] }>;
      }
    ).computeBaseRailLinesWithRates(booking, { containers: [] });

    expect(result.lineItems).toHaveLength(0);
    expect(result.blocked).toHaveLength(1);
  });

  it('does not price containers off a rate configured for a different leg', async () => {
    const booking = {
      id: 'b-4',
      freightType: 'CONTAINER',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'USD',
      cargoTotalWeightVgm: 50,
      originYardId: LEBU,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: {
            containers: Array<{ containerTypeId: string; quantity: number }>;
          },
        ) => Promise<{ lineItems: Array<{ amount: number }> }>;
      }
    ).computeBaseRailLinesWithRates(booking, {
      containers: [{ containerTypeId: 'ct-20', quantity: 3 }],
    });

    expect(result.lineItems).toHaveLength(0);
  });

  // A mixed booking where only one container size has a configured rate must
  // hard-block, not silently carry the unconfigured size for free.
  it('blocks the unconfigured container size and prices the configured one', async () => {
    const fortyOnly: Rate = {
      ...intercityContainerUsd,
      id: 'rate-ct-40-only',
      containerTypeId: 'ct-40',
    } as Rate;
    ratesService.findLiveRates.mockResolvedValue([fortyOnly]);

    const booking = {
      id: 'b-5',
      freightType: 'CONTAINER',
      tradeDirection: 'DOMESTIC',
      paymentCurrency: 'USD',
      originYardId: MOJO,
      destinationYardId: DIRE,
      bookingContainers: [],
    } as unknown as Booking;

    const result = await (
      service as unknown as {
        computeBaseRailLinesWithRates: (
          b: Booking,
          input: {
            containers: Array<{ containerTypeId: string; quantity: number }>;
          },
        ) => Promise<{ lineItems: Array<{ code: string }>; blocked: string[] }>;
      }
    ).computeBaseRailLinesWithRates(booking, {
      containers: [
        { containerTypeId: 'ct-40', quantity: 2 },
        { containerTypeId: 'ct-20', quantity: 3 },
      ],
    });

    expect(result.lineItems).toHaveLength(1);
    expect(result.blocked).toHaveLength(1);
    expect(result.blocked[0]).toContain('rate is configured');
  });
});

describe('BookingPricingService — customs clearance fee billed on the booking price', () => {
  const DJ = 'yard-dj';

  const containerFee20: Rate = {
    id: 'rate-cc-20',
    rateType: 'CUSTOMS_CLEARANCE',
    trigger: 'CUSTOMS_CLEARANCE',
    currency: 'USD',
    rateValue: 100,
    rateUnit: 'PER_CONTAINER',
    status: 'LIVE',
    containerTypeId: 'ct-20',
    tradeDirection: 'IMPORT',
    originYardId: DJ,
    destinationYardId: DIRE,
  } as Rate;

  const bulkFeePerTon: Rate = {
    ...containerFee20,
    id: 'rate-cc-bulk',
    rateValue: 5,
    rateUnit: 'PER_TON',
    containerTypeId: null,
  } as Rate;

  const emptyEval = {
    priorityScore: 0,
    appliedModifiers: [],
    containerWeightResults: [],
    warnings: [],
    hardBlocked: [],
    requiresDirectorApproval: false,
  };

  const makeService = (opts: {
    snapshots?: unknown[];
    liveRates?: Rate[];
    wagonCapacity?: number;
  }) =>
    new BookingPricingService(
      {
        calculateWagonCount: jest.fn().mockResolvedValue(0),
        findContractRateSnapshots: jest.fn().mockResolvedValue(opts.snapshots ?? []),
      } as never,
      { evaluate: jest.fn().mockResolvedValue(emptyEval) } as never,
      {
        findById: jest.fn(async (id: string) => ({
          id,
          sizeFt: id === 'ct-40' ? 40 : 20,
          isReefer: false,
          code: id === 'ct-40' ? 'C40' : 'C20',
        })),
      } as never,
      { findLiveRates: jest.fn().mockResolvedValue(opts.liveRates ?? []) } as never,
      { getRate: jest.fn().mockResolvedValue(MOCK_CBE_RATE) } as never,
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      {
        findById: jest.fn().mockResolvedValue({
          wagonTypes:
            opts.wagonCapacity !== undefined
              ? [{ capacityTons: opts.wagonCapacity }]
              : [],
        }),
      } as never,
    );

  const containerBooking = (overrides: Record<string, unknown> = {}) =>
    ({
      id: 'b-cc',
      freightType: 'CONTAINER',
      tradeDirection: 'IMPORT',
      paymentCurrency: 'USD',
      customsClearingEnabled: true,
      originYardId: DJ,
      destinationYardId: DIRE,
      bookingContainers: [
        { containerTypeId: 'ct-20', quantity: 4, vgmPerUnitTons: 10, wagonsRequired: 2 },
      ],
      ...overrides,
    }) as unknown as Booking;

  const bulkBooking = (overrides: Record<string, unknown> = {}) =>
    ({
      id: 'b-cc-bulk',
      freightType: 'BULK',
      tradeDirection: 'IMPORT',
      paymentCurrency: 'USD',
      customsClearingEnabled: true,
      cargoTypeId: 'cargo-1',
      cargoTotalWeightVgm: 120,
      originYardId: DJ,
      destinationYardId: DIRE,
      bookingContainers: [],
      ...overrides,
    }) as unknown as Booking;

  it('bills a container booking per box at its own container type fee', async () => {
    const service = makeService({ liveRates: [containerFee20] });
    const result = await service.computePriceForBooking(containerBooking());

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE_20FT');
    expect(line).toBeDefined();
    expect(line!.unit).toBe('PER_CONTAINER');
    expect(line!.quantity).toBe(4);
    expect(line!.amount).toBe(400);
  });

  it('bills a PER_WAGON container fee on the wagons the boxes occupy (two 20ft share one)', async () => {
    const service = makeService({
      liveRates: [{ ...containerFee20, rateUnit: 'PER_WAGON' } as Rate],
    });
    const result = await service.computePriceForBooking(containerBooking());

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE_20FT');
    expect(line!.unit).toBe('PER_WAGON');
    expect(line!.quantity).toBe(2);
    expect(line!.amount).toBe(200);
  });

  it('hard-blocks a container type with no fee configured (never free clearance)', async () => {
    const service = makeService({ liveRates: [bulkFeePerTon] });
    const result = await service.computePriceForBooking(containerBooking());

    expect(result.lineItems.some((l) => l.code.startsWith('CUSTOMS_CLEARANCE'))).toBe(false);
    expect(result.hardBlocked.some((m) => m.includes('customs clearance'))).toBe(true);
  });

  it('bills a bulk booking per ton at the route bulk fee', async () => {
    const service = makeService({ liveRates: [bulkFeePerTon] });
    const result = await service.computePriceForBooking(bulkBooking());

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE');
    expect(line!.unit).toBe('PER_TON');
    expect(line!.quantity).toBe(120);
    expect(line!.amount).toBe(600);
  });

  it('the fee scoped to the booking commodity wins over the catch-all', async () => {
    const service = makeService({
      liveRates: [
        { ...bulkFeePerTon, id: 'rate-cc-catchall', rateValue: 5 } as Rate,
        {
          ...bulkFeePerTon,
          id: 'rate-cc-sugar',
          rateValue: 9,
          cargoTypeId: 'cargo-1',
        } as Rate,
      ],
    });
    const result = await service.computePriceForBooking(bulkBooking());

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE');
    expect(line!.unitAmount).toBe(9); // commodity rate, not the 5 USD catch-all
    expect(line!.amount).toBe(1080);
  });

  it('bills a PER_WAGON bulk fee on ceil(tons ÷ wagon capacity)', async () => {
    const service = makeService({
      liveRates: [{ ...bulkFeePerTon, rateUnit: 'PER_WAGON', rateValue: 50 } as Rate],
      wagonCapacity: 60,
    });
    const result = await service.computePriceForBooking(bulkBooking());

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE');
    expect(line!.unit).toBe('PER_WAGON');
    expect(line!.quantity).toBe(2); // 120 t ÷ 60 t per wagon
    expect(line!.amount).toBe(100);
  });

  it('blocks a PER_WAGON bulk fee when no wagon capacity is configured', async () => {
    const service = makeService({
      liveRates: [{ ...bulkFeePerTon, rateUnit: 'PER_WAGON' } as Rate],
    });
    const result = await service.computePriceForBooking(bulkBooking());

    expect(result.hardBlocked.some((m) => m.includes('wagon'))).toBe(true);
  });

  it('prefers the contract frozen per-size snapshot over the live rate', async () => {
    const service = makeService({
      liveRates: [containerFee20],
      snapshots: [
        {
          rateCode: 'CUSTOMS_CLEARANCE_20FT',
          unitPrice: 80,
          currency: 'USD',
          unitOfMeasure: 'per_container',
          isClearance: true,
        },
      ],
    });
    const result = await service.computePriceForBooking(
      containerBooking({ contractId: 'c-1' }),
    );

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE_20FT');
    expect(line!.amount).toBe(320); // 4 × frozen 80, not live 100
  });

  it('honours a legacy FLAT snapshot once for the whole container booking', async () => {
    const service = makeService({
      liveRates: [],
      snapshots: [
        {
          rateCode: 'CUSTOMS_CLEARANCE',
          unitPrice: 500,
          currency: 'USD',
          unitOfMeasure: 'flat',
          isClearance: true,
        },
      ],
    });
    const result = await service.computePriceForBooking(
      containerBooking({ contractId: 'c-legacy' }),
    );

    const line = result.lineItems.find((l) => l.code === 'CUSTOMS_CLEARANCE');
    expect(line!.unit).toBe('FLAT');
    expect(line!.amount).toBe(500);
    expect(result.hardBlocked.some((m) => m.includes('customs clearance'))).toBe(false);
  });

  it('adds no fee line when customs clearing is disabled', async () => {
    const service = makeService({ liveRates: [containerFee20] });
    const result = await service.computePriceForBooking(
      containerBooking({ customsClearingEnabled: false }),
    );

    expect(result.lineItems.some((l) => l.code.startsWith('CUSTOMS_CLEARANCE'))).toBe(false);
  });
});

/**
 * Bulk freight bills in the commodity's own unit: tonnage for a weighed
 * commodity (PER_TON), item count for a counted one (PER_ITEM). Both read the
 * booking's cargo amount; PER_WAGON bills the wagons the cargo occupies.
 */
describe('BookingPricingService — bulk base freight units', () => {
  const DJ = 'yard-dj-bulk';
  const DIRE_B = 'yard-dire-bulk';

  const bulkRate = (overrides: Partial<Rate> = {}): Rate =>
    ({
      id: 'rate-bulk',
      rateType: 'BULK_IMPORT',
      appliesTo: 'BULK',
      trigger: 'ALWAYS',
      currency: 'USD',
      rateValue: 200,
      rateUnit: 'PER_ITEM',
      status: 'LIVE',
      containerTypeId: null,
      cargoTypeId: null,
      tradeDirection: 'IMPORT',
      originYardId: DJ,
      destinationYardId: DIRE_B,
      ...overrides,
    }) as Rate;

  const makeService = (liveRates: Rate[], wagonCapacity?: number) =>
    new BookingPricingService(
      {
        calculateWagonCount: jest.fn().mockResolvedValue(0),
        findContractRateSnapshots: jest.fn().mockResolvedValue([]),
      } as never,
      {
        evaluate: jest.fn().mockResolvedValue({
          priorityScore: 0,
          appliedModifiers: [],
          containerWeightResults: [],
          warnings: [],
          hardBlocked: [],
          requiresDirectorApproval: false,
        }),
      } as never,
      { findById: jest.fn() } as never,
      { findLiveRates: jest.fn().mockResolvedValue(liveRates) } as never,
      { getRate: jest.fn().mockResolvedValue(MOCK_CBE_RATE) } as never,
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      {
        findById: jest.fn().mockResolvedValue({
          wagonTypes: wagonCapacity !== undefined ? [{ capacityTons: wagonCapacity }] : [],
        }),
      } as never,
    );

  // 12 machines, not 12 tonnes — a PER_ITEM commodity records its count here.
  const booking = (overrides: Record<string, unknown> = {}) =>
    ({
      id: 'b-bulk',
      freightType: 'BULK',
      tradeDirection: 'IMPORT',
      paymentCurrency: 'USD',
      cargoTypeId: 'cargo-machinery',
      cargoTotalWeightVgm: 12,
      originYardId: DJ,
      destinationYardId: DIRE_B,
      bookingContainers: [],
      ...overrides,
    }) as unknown as Booking;

  it('bills a PER_ITEM rate on the item count', async () => {
    const result = await makeService([bulkRate()]).computePriceForBooking(booking());

    const line = result.lineItems.find((l) => l.code === 'BULK_IMPORT');
    expect(line!.unit).toBe('PER_ITEM');
    expect(line!.quantity).toBe(12);
    expect(line!.amount).toBe(2400);
  });

  it('bills a PER_TON rate on the tonnage', async () => {
    const result = await makeService([
      bulkRate({ rateUnit: 'PER_TON', rateValue: 35 }),
    ]).computePriceForBooking(booking({ cargoTotalWeightVgm: 120 }));

    const line = result.lineItems.find((l) => l.code === 'BULK_IMPORT');
    expect(line!.unit).toBe('PER_TON');
    expect(line!.amount).toBe(35 * 120);
  });

  it('bills a PER_WAGON rate on the wagons the cargo occupies, not zero', async () => {
    const result = await makeService(
      [bulkRate({ rateUnit: 'PER_WAGON', rateValue: 500 })],
      60,
    ).computePriceForBooking(booking({ cargoTotalWeightVgm: 120 }));

    const line = result.lineItems.find((l) => l.code === 'BULK_IMPORT');
    expect(line!.unit).toBe('PER_WAGON');
    expect(line!.quantity).toBe(2); // 120 t ÷ 60 t per wagon
    expect(line!.amount).toBe(1000);
  });

  it('prices off the rate scoped to the booking commodity, not another one', async () => {
    const result = await makeService([
      bulkRate({ id: 'rate-wheat', cargoTypeId: 'cargo-wheat', rateUnit: 'PER_TON', rateValue: 35 }),
      bulkRate({ id: 'rate-machinery', cargoTypeId: 'cargo-machinery', rateValue: 200 }),
    ]).computePriceForBooking(booking());

    const line = result.lineItems.find((l) => l.code === 'BULK_IMPORT');
    expect(line!.unit).toBe('PER_ITEM');
    expect(line!.amount).toBe(2400);
  });

  it('hard-blocks when the leg only carries another commodity’s rate', async () => {
    const result = await makeService([
      bulkRate({ id: 'rate-wheat', cargoTypeId: 'cargo-wheat' }),
    ]).computePriceForBooking(booking());

    expect(result.lineItems.some((l) => l.code === 'BULK_IMPORT')).toBe(false);
    expect(result.hardBlocked.some((m) => m.includes('rate is configured'))).toBe(true);
  });
});

/**
 * A PER_WAGON container rate bills the wagons the LINE occupies — two 20ft share
 * one wagon, a 40ft takes a whole one. Regression cases taken from real
 * bookings on Doraleh → Gelan, where the 20ft line was being charged for the
 * 40ft line's wagons as well.
 */
describe('BookingPricingService — PER_WAGON container freight', () => {
  const DJ = 'yard-dj-w';
  const ET = 'yard-et-w';

  const perWagon20: Rate = {
    id: 'rate-20-wagon',
    rateType: 'CONTAINER_IMPORT',
    currency: 'USD',
    rateValue: 1690,
    rateUnit: 'PER_WAGON',
    status: 'LIVE',
    containerTypeId: 'ct-20',
    originYardId: DJ,
    destinationYardId: ET,
  } as Rate;

  const perContainer40: Rate = {
    ...perWagon20,
    id: 'rate-40-container',
    rateValue: 1676,
    rateUnit: 'PER_CONTAINER',
    containerTypeId: 'ct-40',
  } as Rate;

  const makeService = () =>
    new BookingPricingService(
      {
        // Booking-wide aggregate — deliberately larger than any single line, so
        // a regression that reads it instead of the line's own wagons shows up.
        calculateWagonCount: jest.fn().mockResolvedValue(5),
        findContractRateSnapshots: jest.fn().mockResolvedValue([]),
      } as never,
      {
        evaluate: jest.fn().mockResolvedValue({
          priorityScore: 0,
          appliedModifiers: [],
          containerWeightResults: [],
          warnings: [],
          hardBlocked: [],
          requiresDirectorApproval: false,
        }),
      } as never,
      {
        findById: jest.fn(async (id: string) => ({
          id,
          sizeFt: id === 'ct-40' ? 40 : 20,
          isReefer: false,
          code: id === 'ct-40' ? 'C40' : 'C20',
          label: id === 'ct-40' ? 'C40' : 'C20',
        })),
      } as never,
      { findLiveRates: jest.fn().mockResolvedValue([perWagon20, perContainer40]) } as never,
      { getRate: jest.fn().mockResolvedValue(MOCK_CBE_RATE) } as never,
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      { findById: jest.fn() } as never,
    );

  const booking = (
    lines: Array<{ containerTypeId: string; quantity: number }>,
  ) =>
    ({
      id: 'b-wagon',
      freightType: 'CONTAINER',
      tradeDirection: 'IMPORT',
      paymentCurrency: 'USD',
      originYardId: DJ,
      destinationYardId: ET,
      bookingContainers: lines.map((l) => ({
        containerTypeId: l.containerTypeId,
        quantity: l.quantity,
        vgmPerUnitTons: 10,
      })),
    }) as unknown as Booking;

  const price = async (
    lines: Array<{ containerTypeId: string; quantity: number }>,
  ) => {
    const service = makeService();
    const result = await service.computePriceForBooking(booking(lines));
    return result.lineItems.filter((l) => l.code === 'CONTAINER_IMPORT');
  };

  it('bills 2× 20ft as one wagon', async () => {
    const [line] = await price([{ containerTypeId: 'ct-20', quantity: 2 }]);
    expect(line.unit).toBe('PER_WAGON');
    expect(line.quantity).toBe(1);
    expect(line.amount).toBe(1690);
  });

  it('bills 10× 20ft as five wagons', async () => {
    const [line] = await price([{ containerTypeId: 'ct-20', quantity: 10 }]);
    expect(line.quantity).toBe(5);
    expect(line.amount).toBe(5 * 1690);
  });

  it('does not charge the 20ft line for the 40ft line’s wagons', async () => {
    const lines = await price([
      { containerTypeId: 'ct-20', quantity: 4 },
      { containerTypeId: 'ct-40', quantity: 1 },
    ]);
    const twenty = lines.find((l) => l.description.startsWith('C20'))!;
    const forty = lines.find((l) => l.description.startsWith('C40'))!;
    // 4× 20ft = 2 wagons, NOT the booking-wide 3.
    expect(twenty.quantity).toBe(2);
    expect(twenty.amount).toBe(2 * 1690);
    // The 40ft line keeps billing per container.
    expect(forty.quantity).toBe(1);
    expect(forty.amount).toBe(1676);
  });

  it('rounds an odd 20ft count up to a whole wagon', async () => {
    const [line] = await price([{ containerTypeId: 'ct-20', quantity: 5 }]);
    expect(line.quantity).toBe(3);
    expect(line.amount).toBe(3 * 1690);
  });
});
