import { Booking } from '../bookings/entities/booking.entity';
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
import {
  applyWagonOrderReversal,
  planWagonsWithStock,
} from './wagon-plan-flex.util';
import type { WagonPlanSlot } from './wagon-plan.util';

const nw6: WagonType = {
  id: 'wt-nw6',
  code: 'NW6',
  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 containerBooking = (id: string, quantity: number, wagonsRequired: number): Booking =>
  ({
    id,
    reference: id,
    freightType: 'CONTAINER',
    cargoTotalWeightVgm: quantity * 25,
    bookingContainers: [
      {
        id: `${id}-line-0`,
        containerTypeId: 'ct-1',
        quantity,
        wagonsRequired,
        vgmPerUnitTons: 25,
      },
    ],
  }) as Booking;

describe('planWagonsWithStock — shortage detail', () => {
  it('defers with a structured per-type shortage when container stock runs out', () => {
    const result = planWagonsWithStock({
      bookings: [containerBooking('BKG-1', 2, 1)],
      allowed: {
        byContainerTypeId: new Map([['ct-1', [nw6]]]),
        byCargoTypeId: new Map(),
      },
      stock: {
        mode: 'YARD',
        remainingByTypeId: new Map([[nw6.id, 0]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
    });

    expect(result.fitting).toHaveLength(0);
    expect(result.deferred).toHaveLength(1);
    const row = result.deferred[0]!;
    expect(row.reference).toBe('BKG-1');
    expect(row.reason).toContain('No available NW6 wagon at the yard');
    expect(row.reason).toContain('short 1');
    expect(row.shortage).toEqual({
      wagonTypeCodes: 'NW6',
      wagonsNeeded: 1,
      wagonsAvailable: 0,
      wagonsShort: 1,
    });
  });

  it('counts the stock the deferred booking actually saw, not its rolled-back usage', () => {
    // Two wagons needed (2 × 40ft), one in stock: booking rolls back entirely,
    // the shortage reports 1 available / 1 short.
    const fortyFooter = containerBooking('BKG-2', 2, 2);
    fortyFooter.bookingContainers![0]!.containerType = {
      code: '40GP',
      sizeFt: 40,
    } as never;
    const result = planWagonsWithStock({
      bookings: [fortyFooter],
      allowed: {
        byContainerTypeId: new Map([['ct-1', [nw6]]]),
        byCargoTypeId: new Map(),
      },
      stock: {
        mode: 'YARD',
        remainingByTypeId: new Map([[nw6.id, 1]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
    });

    expect(result.deferred).toHaveLength(1);
    expect(result.deferred[0]?.shortage).toEqual({
      wagonTypeCodes: 'NW6',
      wagonsNeeded: 2,
      wagonsAvailable: 1,
      wagonsShort: 1,
    });
    // The rolled-back wagon is plannable again for later bookings.
    expect(result.plan).toHaveLength(0);
  });

  it('leaves shortage unset for configuration problems', () => {
    const bulkBooking = {
      id: 'BKG-3',
      reference: 'BKG-3',
      freightType: 'BULK',
      cargoTotalWeightVgm: 40,
      cargoTypeId: 'cargo-1',
      cargoType: { id: 'cargo-1', cargoTypeName: 'Fertilizer' },
      bookingContainers: [],
    } as unknown as Booking;

    const result = planWagonsWithStock({
      bookings: [bulkBooking],
      allowed: {
        byContainerTypeId: new Map(),
        byCargoTypeId: new Map(), // no wagon types configured → config issue
      },
      stock: {
        mode: 'YARD',
        remainingByTypeId: new Map([[cw3.id, 5]]),
        codesByTypeId: new Map([[cw3.id, cw3.code]]),
      },
    });

    expect(result.configIssues).toHaveLength(1);
    expect(result.deferred[0]?.shortage).toBeNull();
  });
});

describe('applyWagonOrderReversal', () => {
  const slot = (
    seq: number,
    wagonTypeId: string,
    bookingId: string,
  ): WagonPlanSlot =>
    ({
      sequenceNo: seq,
      wagonTypeId,
      capacityTons: 70,
      lengthMeters: 14,
      assignedWeightTons: 25,
      allocations: [{ bookingId }],
    }) as unknown as WagonPlanSlot;

  const plan: WagonPlanSlot[] = [
    slot(1, 'wt-a', 'BKG-A'),
    slot(2, 'wt-b', 'BKG-B'),
    slot(3, 'wt-c', 'BKG-C'),
  ];

  it('returns the plan unchanged when the flag is false/absent', () => {
    expect(applyWagonOrderReversal(plan, false)).toBe(plan);
    expect(applyWagonOrderReversal(plan, undefined)).toBe(plan);
    expect(applyWagonOrderReversal(plan, null)).toBe(plan);
  });

  it('flips the order and renumbers sequenceNo 1..N when the flag is true', () => {
    const reversed = applyWagonOrderReversal(plan, true);
    // Physically-last wagon (was seq 3, wt-c) is now position 1.
    expect(reversed.map((s) => s.wagonTypeId)).toEqual(['wt-c', 'wt-b', 'wt-a']);
    expect(reversed.map((s) => s.sequenceNo)).toEqual([1, 2, 3]);
  });

  it('keeps each booking with its own wagon — only the position changes', () => {
    const reversed = applyWagonOrderReversal(plan, true);
    // The booking that was in the last wagon now sits at sequenceNo 1.
    expect(reversed[0].sequenceNo).toBe(1);
    expect(
      (reversed[0].allocations as { bookingId: string }[])[0].bookingId,
    ).toBe('BKG-C');
    expect(
      (reversed[2].allocations as { bookingId: string }[])[0].bookingId,
    ).toBe('BKG-A');
  });

  it('does not mutate the input plan', () => {
    applyWagonOrderReversal(plan, true);
    expect(plan.map((s) => s.sequenceNo)).toEqual([1, 2, 3]);
    expect(plan.map((s) => s.wagonTypeId)).toEqual(['wt-a', 'wt-b', 'wt-c']);
  });
});

describe('planWagonsWithStock — leg-aware stock (intercity ride-along)', () => {
  const allowed = {
    byContainerTypeId: new Map([['ct-1', [nw6]]]),
    byCargoTypeId: new Map(),
  };
  // Corridor Gelan(0) → Adama(1) → Doraleh(2): edges 0 and 1.
  const legs = (entries: Array<[string, { from: number; to: number }]>) =>
    new Map(entries);

  it('lets an intercity booking ride the empty leg of a train that is full on the other leg', () => {
    // 1 wagon in stock. Export rides edge 1 only; intercity rides edge 0 only.
    const result = planWagonsWithStock({
      bookings: [
        containerBooking('EXPORT-1', 1, 1),
        containerBooking('INTERCITY-1', 1, 1),
      ],
      allowed,
      stock: {
        mode: 'TRAIN',
        remainingByTypeId: new Map([[nw6.id, 1]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
      legs: legs([
        ['EXPORT-1', { from: 1, to: 2 }],
        ['INTERCITY-1', { from: 0, to: 1 }],
      ]),
      edgeCount: 2,
    });

    expect(result.deferred).toHaveLength(0);
    expect(result.fitting.map((b) => b.id).sort()).toEqual([
      'EXPORT-1',
      'INTERCITY-1',
    ]);
    // Two slots planned, but both drawn from the single physical wagon.
    expect(result.plan).toHaveLength(2);
  });

  it('still defers when the legs overlap and stock is exhausted', () => {
    const result = planWagonsWithStock({
      bookings: [
        containerBooking('EXPORT-1', 1, 1),
        containerBooking('INTERCITY-1', 1, 1),
      ],
      allowed,
      stock: {
        mode: 'TRAIN',
        remainingByTypeId: new Map([[nw6.id, 1]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
      legs: legs([
        // Both ride edge 0 — they compete for the one wagon.
        ['EXPORT-1', { from: 0, to: 2 }],
        ['INTERCITY-1', { from: 0, to: 1 }],
      ]),
      edgeCount: 2,
    });

    expect(result.fitting.map((b) => b.id)).toEqual(['EXPORT-1']);
    expect(result.deferred).toHaveLength(1);
    expect(result.deferred[0]!.reference).toBe('INTERCITY-1');
    expect(result.deferred[0]!.reason).toContain('Train has no free NW6 wagon left');
  });

  it('never packs bookings with different legs into the same wagon slot', () => {
    // Two 20ft units with room to share one wagon by TEU — but disjoint legs
    // must open separate slots (each with its own leg), not one mixed slot.
    const result = planWagonsWithStock({
      bookings: [
        containerBooking('EXPORT-1', 1, 1),
        containerBooking('INTERCITY-1', 1, 1),
      ],
      allowed,
      stock: {
        mode: 'TRAIN',
        remainingByTypeId: new Map([[nw6.id, 2]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
      legs: legs([
        ['EXPORT-1', { from: 1, to: 2 }],
        ['INTERCITY-1', { from: 0, to: 1 }],
      ]),
      edgeCount: 2,
    });

    expect(result.plan).toHaveLength(2);
    const bookingsPerSlot = result.plan.map((s) =>
      [...new Set(s.allocations.map((a) => a.bookingId))].sort(),
    );
    expect(bookingsPerSlot).toEqual([['EXPORT-1'], ['INTERCITY-1']]);
  });

  it('behaves exactly like the whole-route planner when no legs are given', () => {
    const result = planWagonsWithStock({
      bookings: [
        containerBooking('EXPORT-1', 1, 1),
        containerBooking('INTERCITY-1', 1, 1),
      ],
      allowed,
      stock: {
        mode: 'TRAIN',
        remainingByTypeId: new Map([[nw6.id, 1]]),
        codesByTypeId: new Map([[nw6.id, nw6.code]]),
      },
    });

    // One wagon, two 20ft bookings: they TEU-share the single slot (legacy).
    expect(result.deferred).toHaveLength(0);
    expect(result.plan).toHaveLength(1);
  });
});
