/**
 * EXPORT journeys E3 + E4 — pay-or-lose on the reversed corridor:
 *
 *   E3 (D+5): PA + PB reserve 40 wagons and pay. PC reserves the last 14 but
 *   never pays; a late exporter PD is rejected for space while PC's hold
 *   lives. PC's pay deadline passes → EXPIRED → the freed 14 wagons are
 *   instantly FCFS-bookable again: PD rebooks, accepts, pays, allocates.
 *
 *   E4 (D+6): five bookings reserve the whole train, only three pay. The
 *   window CLOSE passes → phase DONE, and the day sweep expires the two
 *   unpaid reservations. Export days never reopen — no second cycle, the
 *   cycle counter stays at 1 and the three paid bookings ride.
 *
 * Sequential steps — retries off.
 */

import {
  acceptExport,
  bookContainers,
  createImportSchedule,
  db,
  departureAt,
  eatDayStr,
  ensureExportRoute,
  EXP_DEST,
  EXP_ORIGIN,
  forceReservationExpiry,
  forceWindowOpen,
  markPaid,
  pollAllocations,
  pollBookingStatus,
  pollDb,
  resetCorridorDay,
  seedImportContract,
  withBooking,
  withExportSchedule,
  type ScheduleRow,
} from "./import-utils";

const DEPARTURE = departureAt(5);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const CLOSE_DEPARTURE = departureAt(6);
const CLOSE_DAY = eatDayStr(CLOSE_DEPARTURE);

const stamp = String(Date.now());
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;

function seedExport(suffix: string) {
  seedImportContract({
    suffix,
    reference: stampedRef(suffix),
    direction: "EXPORT",
    originCode: EXP_ORIGIN,
    destCode: EXP_DEST,
  });
}

/** QA..QE reserve 12+12+12+12+6 = 54; only QA/QB/QC pay. */
const CLOSERS = [
  { suffix: "QA", forty: 12, wagons: 12, pays: true },
  { suffix: "QB", forty: 12, wagons: 12, pays: true },
  { suffix: "QC", forty: 12, wagons: 12, pays: true },
  { suffix: "QD", forty: 12, wagons: 12, pays: false },
  { suffix: "QE", forty: 6, wagons: 6, pays: false },
];

describe("export pay-or-lose: expiry frees space; window close expires the unpaid", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    ["PA", "PB", "PC", "PD", ...CLOSERS.map((c) => c.suffix)].forEach(seedExport);
  });

  it("operations prepares the export corridor and the D+5 train with an open window", () => {
    ensureExportRoute();
    resetCorridorDay(DEPARTURE, EXP_DEST, EXP_ORIGIN);
    resetCorridorDay(CLOSE_DEPARTURE, EXP_DEST, EXP_ORIGIN);
    createImportSchedule({
      departure: DEPARTURE,
      locoPair: ["LOCO-EXP-7", "LOCO-EXP-8"],
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
    withExportSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
  });

  it("PA and PB reserve and pay 40 wagons; PC reserves the last 14 unpaid", () => {
    bookContainers({
      suffix: "PA",
      runStamp: stamp,
      isoSeed: 7600,
      twenty: 40,
      scheduledDate: BOOKING_DAY,
    });
    acceptExport("PA");
    markPaid("PA");
    pollAllocations("PA", 20);

    bookContainers({
      suffix: "PB",
      runStamp: stamp,
      isoSeed: 7700,
      forty: 20,
      scheduledDate: BOOKING_DAY,
    });
    acceptExport("PB");
    markPaid("PB");
    pollAllocations("PB", 20);

    bookContainers({
      suffix: "PC",
      runStamp: stamp,
      isoSeed: 7800,
      twenty: 28,
      scheduledDate: BOOKING_DAY,
    });
    acceptExport("PC");
    // Clamp: PC's deadline never outlives the window close.
    withExportSchedule(DEPARTURE, (s) => {
      withBooking("PC", (b) => {
        expect(new Date(b.payment_deadline!).getTime()).to.be.at.most(
          new Date(s.window_closes_at!).getTime(),
        );
      });
    });
  });

  it("a late exporter is rejected while PC's unpaid reservation holds the space", () => {
    bookContainers({
      suffix: "PD",
      runStamp: stamp,
      isoSeed: 7900,
      twenty: 28,
      scheduledDate: BOOKING_DAY,
      expectFailure: /space|window/i,
    });
  });

  it("PC misses its pay window — EXPIRED — and PD immediately books the freed 14 wagons", () => {
    forceReservationExpiry("PC");
    bookContainers({
      suffix: "PD",
      runStamp: stamp,
      isoSeed: 8000,
      twenty: 28,
      scheduledDate: BOOKING_DAY,
    });
    acceptExport("PD");
    markPaid("PD");
    pollAllocations("PD", 14);
    withExportSchedule(DEPARTURE, (s) => {
      withBooking("PD", (b) => expect(b.train_schedule_id, "PD took PC's seat").to.eq(s.id));
    });
  });

  it("window-close day: five reservations, three payments", () => {
    createImportSchedule({
      departure: CLOSE_DEPARTURE,
      locoPair: ["LOCO-EXP-9", "LOCO-EXP-10"],
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
    withExportSchedule(CLOSE_DEPARTURE, (s) => forceWindowOpen(s.id, 60));

    let isoSeed = 8200;
    CLOSERS.forEach((c) => {
      bookContainers({
        suffix: c.suffix,
        runStamp: stamp,
        isoSeed,
        forty: c.forty,
        scheduledDate: CLOSE_DAY,
      });
      isoSeed += c.forty;
      acceptExport(c.suffix);
    });
    CLOSERS.filter((c) => c.pays).forEach((c) => {
      markPaid(c.suffix);
      pollAllocations(c.suffix, c.wagons);
    });
  });

  it("the window CLOSES — phase DONE, the two unpaid expire, and export never reopens", () => {
    withExportSchedule(CLOSE_DEPARTURE, (s) => {
      db(
        `UPDATE freight.train_schedules
         SET window_closes_at = now() - interval '1 second'
         WHERE id = $1 AND window_phase = 'OPEN'`,
        [s.id],
      );
      pollDb<ScheduleRow>(
        "export window DONE (no reopen)",
        `SELECT window_phase, booking_cycle_no FROM freight.train_schedules WHERE id = $1`,
        [s.id],
        (row) => row?.window_phase === "DONE" && Number(row?.booking_cycle_no) === 1,
      );
    });
    // The unpaid deadlines were clamped to the ORIGINAL close; forcing the
    // close earlier means forcing their deadlines with it (same semantics).
    CLOSERS.filter((c) => !c.pays).forEach((c) => forceReservationExpiry(c.suffix));
    CLOSERS.filter((c) => !c.pays).forEach((c) => pollBookingStatus(c.suffix, "EXPIRED"));
    CLOSERS.filter((c) => c.pays).forEach((c) =>
      withBooking(c.suffix, (b) => expect(b.status, `${c.suffix} rides`).to.eq("PAID")),
    );
  });
});

export {};
