import { Capacity, CorridorBudget } from './corridor-capacity.util';
import { sizePartialOfferWagons } from './train-capacity.util';

describe('corridor-capacity.util — overage tolerance', () => {
  const pw2 = { lengthMeters: 17.066, capacityTons: 70, tareWeightTons: 25.2 };
  const stops = ['yard-a', 'yard-b'];
  const base: Capacity = { wagons: 44, weightTons: 3500, lengthMeters: 760 };
  const tolerance = { weightTons: 90, lengthMeters: 0 };

  const need = (weightTons: number, wagons = 1, lengthMeters = 17): Capacity => ({
    wagons,
    weightTons,
    lengthMeters,
  });

  const budgetAt = (usedWeightTons: number): CorridorBudget => {
    const budget = new CorridorBudget(stops, base, tolerance);
    budget.subtract(need(usedWeightTons, 10, 170), budget.fullLeg());
    return budget;
  };

  it('admits a whole booking that overflows the base cap by less than the tolerance', () => {
    // 3500T train, 90T tolerance, 3560T committed: a 25T booking still boards
    // entire (3585 ≤ 3590).
    const budget = budgetAt(3560);
    expect(budget.fits(need(25), budget.fullLeg())).toBe(true);
  });

  it('rejects a whole booking that overflows past the tolerance — no partial admission', () => {
    // Same train at 3560T: a 210T booking would need 3770 > 3590 — skipped.
    const budget = budgetAt(3560);
    expect(budget.fits(need(210), budget.fullLeg())).toBe(false);
  });

  it('caps stacked overage admissions at base + tolerance', () => {
    // Small units may keep boarding inside the overage zone, but never past it.
    const budget = budgetAt(3560);
    budget.subtract(need(25), budget.fullLeg()); // now 3585 committed
    expect(budget.fits(need(5), budget.fullLeg())).toBe(true); // 3590 exactly
    expect(budget.fits(need(6), budget.fullLeg())).toBe(false); // 3591 > 3590
  });

  it('excludes the tolerance from remainingFor, so split room never reaches into it', () => {
    const budget = budgetAt(3400);
    expect(budget.remainingFor(budget.fullLeg()).weightTons).toBe(100);
    // Once a whole-unit admission spends the tolerance, base room goes negative.
    const over = budgetAt(3560);
    expect(over.remainingFor(over.fullLeg()).weightTons).toBe(-60);
  });

  it('yields no split offer once the base cap is spent — tolerance is whole-bookings-only', () => {
    // The batch engine sizes splits from remainingFor; at/over base capacity
    // that room cannot carry even one part-loaded wagon, so no offer opens.
    const over = budgetAt(3560);
    const room = over.remainingFor(over.fullLeg());
    expect(sizePartialOfferWagons(room, 15, pw2)).toBeNull();
  });

  it('still offers a split while committed weight is under the base cap', () => {
    // 744T of base room left: the boundary booking is offered the part that
    // fits up to 3500, not up to 3590.
    const budget = budgetAt(2756);
    const room = budget.remainingFor(budget.fullLeg());
    expect(sizePartialOfferWagons(room, 15, pw2)).toEqual({
      wagons: 8,
      maxCargoTons: 542.4,
    });
  });

  it('leaves fits() strict when no tolerance is configured', () => {
    const strict = new CorridorBudget(stops, base);
    strict.subtract(need(3500, 10, 170), strict.fullLeg());
    expect(strict.fits(need(1), strict.fullLeg())).toBe(false);
  });

  describe('isExhausted — train-wide FULL across all axes', () => {
    // Lightest wagon at rated payload: PW2 25.2T tare + 70T = 95.2T gross.
    const perWagon = {
      grossWeightTons: pw2.tareWeightTons + pw2.capacityTons,
      lengthMeters: pw2.lengthMeters,
    };

    it('reports FULL when weight binds first, with wagon slots still free', () => {
      // 37 loaded PW2 wagons = 3522.4T of 3500+90T. 7 length-derived slots
      // remain, but wagon 38 would need 95.2T against 67.6T of room — the
      // schedule must finalize and its window must disappear.
      const budget = new CorridorBudget(stops, base, tolerance);
      budget.subtract(need(3522.4, 37, 631.442), budget.fullLeg());
      expect(budget.maxRemaining().wagons).toBeGreaterThan(0); // slot check alone says "not full"
      expect(budget.isExhausted(perWagon)).toBe(true);
    });

    it('is not FULL while one more loaded wagon still fits within base + tolerance', () => {
      const budget = budgetAt(3300); // 200T base room + 90T tolerance ≥ 95.2T
      expect(budget.isExhausted(perWagon)).toBe(false);
    });

    it('reports FULL when wagon slots run out regardless of weight room', () => {
      const budget = new CorridorBudget(stops, base, tolerance);
      budget.subtract(need(1000, 44, 700), budget.fullLeg());
      expect(budget.isExhausted(perWagon)).toBe(true);
    });

    it('reports FULL when length room cannot take one more wagon', () => {
      const budget = new CorridorBudget(stops, base, tolerance);
      budget.subtract(need(1000, 30, 750), budget.fullLeg()); // 10m left < 17.066m
      expect(budget.isExhausted(perWagon)).toBe(true);
    });

    it('only counts an edge as open when EVERY axis has room on that same edge', () => {
      // Three stops → two edges. Edge 0 has weight but no slots; edge 1 has
      // slots but no weight. Neither can board a wagon, so the train is FULL
      // even though the per-axis maxima both look open.
      const budget = new CorridorBudget(['a', 'b', 'c'], base, tolerance);
      budget.subtract(need(0, 44, 0), { fromEdge: 0, toEdge: 1 });
      budget.subtract(need(3522.4, 0, 0), { fromEdge: 1, toEdge: 2 });
      expect(budget.isExhausted(perWagon)).toBe(true);
    });
  });
});
