import { AllocationLoadType } from '@edr/types';

import { Booking } from '../bookings/entities/booking.entity';
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
import {
  buildBulkWagonPlan,
  buildContainerWagonPlan,
  buildMixedWagonPlan,
  containerWagonsForLines,
  expandBookingContainerUnits,
  expandContainerItems,
  maxEdgeConsistUsage,
  roundTons,
  sumWagonsRequired,
  validate20ftContainerRules,
  validateContainerPlacements,
} from './wagon-plan.util';

const nw5: WagonType = {
  id: 'wt-nw5',
  code: 'NW5',
  name: 'Flat Wagon',
  capacityTons: 70,
  lengthMeters: 14,
  supportedLoadTypes: ['CONTAINER'],
  isActive: true,
  supportsContainer: true,
} as WagonType;

const cw3: WagonType = {
  id: 'wt-cw3',
  code: 'CW3',
  name: 'Covered Wagon',
  capacityTons: 60,
  lengthMeters: 14,
  supportedLoadTypes: ['BULK'],
  isActive: true,
  supportsContainer: false,
} as WagonType;

const makeContainerBooking = (
  id: string,
  lines: Array<{ quantity: number; wagonsRequired: number; vgmPerUnitTons?: number }>,
): Booking =>
  ({
    id,
    reference: id,
    freightType: 'CONTAINER',
    cargoTotalWeightVgm: lines.reduce(
      (sum, line) => sum + line.quantity * (line.vgmPerUnitTons ?? 25),
      0,
    ),
    bookingContainers: lines.map((line, index) => ({
      id: `${id}-line-${index}`,
      containerTypeId: `ct-${index}`,
      quantity: line.quantity,
      wagonsRequired: line.wagonsRequired,
      vgmPerUnitTons: line.vgmPerUnitTons ?? 25,
    })),
  }) as Booking;

describe('wagon-plan.util', () => {
  it('uses slot-based planning: 2×20ft = 1 wagon slot', () => {
    const booking = makeContainerBooking('b1', [{ quantity: 2, wagonsRequired: 1 }]);
    const plan = buildContainerWagonPlan([booking], nw5);
    expect(plan).toHaveLength(1);
    expect(plan[0]?.allocations[0]?.loadType).toBe(AllocationLoadType.Container);
  });

  it('uses slot-based planning: 1×40ft = 1 wagon slot', () => {
    const booking = makeContainerBooking('b2', [{ quantity: 1, wagonsRequired: 1 }]);
    const plan = buildContainerWagonPlan([booking], nw5);
    expect(plan).toHaveLength(1);
  });

  it('sums wagons across multiple container lines', () => {
    const booking = makeContainerBooking('b3', [
      { quantity: 2, wagonsRequired: 1 },
      { quantity: 1, wagonsRequired: 1 },
    ]);
    expect(sumWagonsRequired(booking)).toBe(2);
    const plan = buildContainerWagonPlan([booking], nw5);
    expect(plan).toHaveLength(2);
  });

  it('counts a bulk booking\'s wagons from the plan, not a flat 1', () => {
    // 700T of sugar on 60T CW3 gondolas = 12 wagons; the stored wagonsRequired
    // must carry all of them so gross weight charges 12 tares downstream.
    const booking = {
      id: 'bulk-700',
      reference: 'bulk-700',
      freightType: 'BULK',
      cargoTotalWeightVgm: 700,
      bookingContainers: [],
    } as unknown as Booking;
    const plan = buildBulkWagonPlan([booking], cw3);
    expect(plan).toHaveLength(12);
    expect(sumWagonsRequired(booking, plan)).toBe(12);
    // Without a plan the pre-plan fallback still applies.
    expect(sumWagonsRequired(booking)).toBe(1);
  });

  it('counts container wagons from the plan TEU packing', () => {
    const booking = makeContainerBooking('c-plan', [{ quantity: 6, wagonsRequired: 3 }]);
    const plan = buildContainerWagonPlan([booking], nw5);
    expect(sumWagonsRequired(booking, plan)).toBe(3);
  });

  it('6×20ft containers = 3 wagon slots (2 per wagon)', () => {
    // 20ft containers take half a wagon each, so 6 * 0.5 = 3 wagons
    const booking = makeContainerBooking('b6x20', [{ quantity: 6, wagonsRequired: 3 }]);
    expect(sumWagonsRequired(booking)).toBe(3);
    const plan = buildContainerWagonPlan([booking], nw5);
    expect(plan).toHaveLength(3);
    // Verify sequence numbers are 1, 2, 3
    expect(plan.map((s) => s.sequenceNo)).toEqual([1, 2, 3]);
  });

  it('expands container items per quantity', () => {
    const booking = makeContainerBooking('b4', [{ quantity: 3, wagonsRequired: 3 }]);
    const items = expandContainerItems(booking, 'alloc-1');
    expect(items).toHaveLength(3);
    expect(items[0]?.wagonBookingAllocationId).toBe('alloc-1');
  });

  it('rounds tons to three decimal places', () => {
    expect(roundTons(1.23456)).toBe(1.235);
    expect(roundTons('bad')).toBe(0);
  });

  it('builds mixed plan with container block before bulk', () => {
    const containerBooking = makeContainerBooking('c1', [{ quantity: 2, wagonsRequired: 2 }]);
    const bulkBooking = {
      id: 'b1',
      reference: 'BKG-BULK',
      freightType: 'BULK',
      cargoTotalWeightVgm: 120,
      bookingContainers: [],
    } as unknown as Booking;

    const plan = buildMixedWagonPlan([containerBooking], [bulkBooking], nw5, cw3);
    expect(plan).toHaveLength(4);
    expect(plan[0]?.slotLoadType).toBe('CONTAINER');
    expect(plan[2]?.slotLoadType).toBe('BULK');
    expect(plan.map((s) => s.sequenceNo)).toEqual([1, 2, 3, 4]);
  });

  it('expands booking container units for UI rows', () => {
    const booking = makeContainerBooking('c2', [{ quantity: 3, wagonsRequired: 3 }]);
    const units = expandBookingContainerUnits([booking]);
    expect(units).toHaveLength(3);
    expect(units[1]?.unitIndex).toBe(1);
    expect(units[1]?.bookingContainerId).toBe('c2-line-0');
  });

  it('validates required placements per container unit', () => {
    const booking = makeContainerBooking('c3', [{ quantity: 2, wagonsRequired: 2 }]);
    const plan = buildContainerWagonPlan([booking], nw5);
    const violations = validateContainerPlacements([booking], plan, []);
    expect(violations.some((v) => v.includes('required'))).toBe(true);

    const units = expandBookingContainerUnits([booking]);
    const placements = units.map((unit, index) => ({
      bookingContainerId: unit.bookingContainerId,
      unitIndex: unit.unitIndex,
      sequenceNo: plan[index]?.sequenceNo ?? 1,
      containerNumber: `CNTR-${index + 1}`,
    }));
    expect(validateContainerPlacements([booking], plan, placements)).toEqual([]);
  });

  it('rejects 20ft container over max individual weight', () => {
    const booking = makeContainerBooking('c20', [{ quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 35 }]);
    const units = expandBookingContainerUnits([booking]);
    const placements = units.map((unit, index) => ({
      bookingContainerId: unit.bookingContainerId,
      unitIndex: unit.unitIndex,
      sequenceNo: 1,
      containerNumber: `CNTR-${index + 1}`,
    }));

    const violations = validate20ftContainerRules(units, placements, {
      max20ftContainerWeightTons: 30,
      max20ftPairWeightDiffTons: 10,
    });

    expect(violations.some((v) => v.includes('exceeds max 30T'))).toBe(true);
  });

  it('rejects 20ft pair when weight difference exceeds limit', () => {
    const booking = makeContainerBooking('c21', [
      { quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 25 },
    ]);
    booking.bookingContainers![0]!.vgmPerUnitTons = 25;
    const units = expandBookingContainerUnits([booking]);
    units[1]!.grossWeightTons = 10;
    const placements = units.map((unit) => ({
      bookingContainerId: unit.bookingContainerId,
      unitIndex: unit.unitIndex,
      sequenceNo: 1,
      containerNumber: `CNTR-${unit.unitIndex}`,
    }));

    const violations = validate20ftContainerRules(units, placements, {
      max20ftContainerWeightTons: 30,
      max20ftPairWeightDiffTons: 10,
    });

    expect(violations.some((v) => v.includes('weight difference'))).toBe(true);
  });

  it('builds bulk-only plan as degenerate mixed case', () => {
    const bulkBooking = {
      id: 'b2',
      reference: 'BKG-BULK-2',
      freightType: 'BULK',
      cargoTotalWeightVgm: 60,
      bookingContainers: [],
    } as unknown as Booking;
    const plan = buildMixedWagonPlan([], [bulkBooking], nw5, cw3);
    expect(plan).toHaveLength(1);
    expect(plan[0]?.slotLoadType).toBe('BULK');
    expect(buildBulkWagonPlan([bulkBooking], cw3)).toHaveLength(1);
  });
});

describe('containerWagonsForLines — TEU-aware, ceil booking total once', () => {
  const line = (quantity: number, wagonsPerUnit: number, wagonsRequired?: number) => ({
    quantity,
    wagonsRequired: wagonsRequired ?? quantity * wagonsPerUnit,
    containerType: { sizeFt: wagonsPerUnit >= 1 ? 40 : 20 },
  });

  it('20×20ft = 10 wagons (not 20)', () => {
    expect(containerWagonsForLines([line(20, 0.5)])).toBe(10);
  });

  it('38×20ft = 19 wagons', () => {
    expect(containerWagonsForLines([line(38, 0.5)])).toBe(19);
  });

  it('2×20ft = 1 wagon', () => {
    expect(containerWagonsForLines([line(2, 0.5)])).toBe(1);
  });

  it('odd 3×20ft = 2 wagons (single line ceils)', () => {
    expect(containerWagonsForLines([line(3, 0.5)])).toBe(2);
  });

  it('3×20ft + 3×20ft = 3 wagons (ceil TOTAL, not per line)', () => {
    // per-line ceil would give 2 + 2 = 4; the booking total is ceil(1.5+1.5)=3.
    expect(containerWagonsForLines([line(3, 0.5), line(3, 0.5)])).toBe(3);
  });

  it('three 1×20ft lines = 2 wagons (ceil TOTAL)', () => {
    // per-line ceil would give 1+1+1 = 3; total is ceil(0.5*3)=ceil(1.5)=2.
    expect(
      containerWagonsForLines([line(1, 0.5), line(1, 0.5), line(1, 0.5)]),
    ).toBe(2);
  });

  it('5×20ft + 2×40ft = 5 wagons', () => {
    expect(containerWagonsForLines([line(5, 0.5), line(2, 1)])).toBe(5);
  });

  it('21×40ft = 21 wagons', () => {
    expect(containerWagonsForLines([line(21, 1)])).toBe(21);
  });

  it('falls back to line wagonsRequired when containerType/sizeFt missing', () => {
    // No containerType relation loaded → use the stored (0.5-aware) fraction.
    expect(
      containerWagonsForLines([
        { quantity: 20, wagonsRequired: 10 } as never,
      ]),
    ).toBe(10);
  });

  it('empty line set = 0 wagons', () => {
    expect(containerWagonsForLines([])).toBe(0);
  });
});

describe('maxEdgeConsistUsage — the binding edge, not the whole-route sum', () => {
  const slot = (
    tare: number,
    cargo: number,
    length: number,
    board?: string | null,
    alight?: string | null,
  ) =>
    ({
      tareWeightTons: tare,
      assignedWeightTons: cargo,
      lengthMeters: length,
      boardYardId: board ?? null,
      alightYardId: alight ?? null,
    }) as never;

  const stops = ['a', 'b', 'c'];

  it('does not sum disjoint legs: intercity a→b + export b→c', () => {
    const plan = [
      slot(24, 65, 14, null, 'b'), // intercity, rides a→b only
      slot(24, 65, 14, 'b', null), // export, rides b→c only
    ];
    // Each edge carries one slot: 89T gross / 14m — never 178T.
    expect(maxEdgeConsistUsage(plan, stops)).toEqual({
      grossWeightTons: 89,
      lengthMeters: 14,
    });
  });

  it('sums overlapping legs on their shared edge (the S-2026-00024 shape)', () => {
    // 20 intercity a→b wagons + 20 export a→c wagons, 23.94T tare, 64.75T cargo:
    // shared edge a→b carries all 40 slots = 3547.6T gross.
    const plan = [
      ...Array.from({ length: 20 }, () => slot(23.94, 64.75, 14, null, 'b')),
      ...Array.from({ length: 20 }, () => slot(23.94, 64.75, 14, null, null)),
    ];
    const usage = maxEdgeConsistUsage(plan, stops);
    expect(usage.grossWeightTons).toBeCloseTo(3547.6, 1);
    expect(usage.lengthMeters).toBe(560);
  });

  it('degrades to whole-train totals on a two-stop route', () => {
    const plan = [slot(24, 65, 14), slot(24, 65, 14)];
    expect(maxEdgeConsistUsage(plan, ['a', 'b'])).toEqual({
      grossWeightTons: 178,
      lengthMeters: 28,
    });
  });
});
