/**
 * BULK EXPORT — pay-or-lose (mirror of export_pay_or_lose):
 *
 *   D+11: ZA + ZB reserve 40 wagons of wheat and pay. ZC reserves the last 14
 *   (980 T) but never pays; late ZD is rejected for space while ZC's hold
 *   lives; ZC expires → ZD immediately books the freed 980 T, pays, allocates.
 *
 *   D+12: five bookings reserve the whole train (840×4 + 420 T), only three
 *   pay. The window CLOSE passes → phase DONE, the two unpaid expire, export
 *   never reopens (cycle counter stays 1).
 *
 * Sequential steps — retries off.
 */

import {
  acceptExport,
  bookBulk,
  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(11);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const CLOSE_DEPARTURE = departureAt(12);
const CLOSE_DAY = eatDayStr(CLOSE_DEPARTURE);

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

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

const CLOSERS = [
  { suffix: "ZQA", tons: 840, wagons: 12, pays: true },
  { suffix: "ZQB", tons: 840, wagons: 12, pays: true },
  { suffix: "ZQC", tons: 840, wagons: 12, pays: true },
  { suffix: "ZQD", tons: 840, wagons: 12, pays: false },
  { suffix: "ZQE", tons: 420, wagons: 6, pays: false },
];

describe("bulk export pay-or-lose: expiry frees space; close expires the unpaid", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    ["ZA", "ZB", "ZC", "ZD", ...CLOSERS.map((c) => c.suffix)].forEach(seedBulkExport);
  });

  it("operations prepares the export corridor and the D+11 CW4 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"],
      kind: "bulk",
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
    withExportSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
  });

  it("ZA and ZB reserve and pay 40 wagons; ZC reserves the last 14 unpaid (clamped)", () => {
    bookBulk({ suffix: "ZA", tons: 1400, scheduledDate: BOOKING_DAY });
    acceptExport("ZA");
    markPaid("ZA");
    pollAllocations("ZA", 20);

    bookBulk({ suffix: "ZB", tons: 1400, scheduledDate: BOOKING_DAY });
    acceptExport("ZB");
    markPaid("ZB");
    pollAllocations("ZB", 20);

    bookBulk({ suffix: "ZC", tons: 980, scheduledDate: BOOKING_DAY });
    acceptExport("ZC");
    withExportSchedule(DEPARTURE, (s) => {
      withBooking("ZC", (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 ZC's unpaid reservation holds the space", () => {
    bookBulk({
      suffix: "ZD",
      tons: 980,
      scheduledDate: BOOKING_DAY,
      expectFailure: /space|window/i,
    });
  });

  it("ZC misses its pay window — EXPIRED — and ZD immediately books the freed wagons", () => {
    forceReservationExpiry("ZC");
    bookBulk({ suffix: "ZD", tons: 980, scheduledDate: BOOKING_DAY });
    acceptExport("ZD");
    markPaid("ZD");
    pollAllocations("ZD", 14);
    withExportSchedule(DEPARTURE, (s) => {
      withBooking("ZD", (b) => expect(b.train_schedule_id, "ZD took ZC'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"],
      kind: "bulk",
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
    withExportSchedule(CLOSE_DEPARTURE, (s) => forceWindowOpen(s.id, 60));

    CLOSERS.forEach((c) => {
      bookBulk({ suffix: c.suffix, tons: c.tons, scheduledDate: CLOSE_DAY });
      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,
      );
    });
    // Forced close ⇒ force the matching deadline clamp on the unpaid pair.
    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 {};
