/**
 * IMPORT journey 3 — split offer, remainder rebooking, pay-window expiry, and
 * priority-ordered waiting-list promotion, all on one 54-wagon corridor train:
 *
 *   reserved by the batch (priority order):
 *     SA 40×20ft = 20w, SB 14×40ft = 14w, SD 24×20ft = 12w        → 46w
 *   SC 48×20ft = 24w does NOT fit whole → the batch offers a PARTIAL of the
 *     remaining 8 wagons (16×20ft). SC pays → the split applies (is_split +
 *     pre_split_quantities), and the customer must later rebook EXACTLY the
 *     whole remainder (32×20ft) — a wrong quantity is rejected.
 *   SD never pays — its pay deadline passes and it EXPIRES; the freed 12
 *     wagons promote the waiting list in priority order: SW1 (12×20ft = 6w)
 *     and SW2 (6×40ft = 6w) get pay windows ("payment sent"); SW3 (40×20ft =
 *     20w) never fits and expires with the day.
 *
 *   Final consist: SA 20 + SB 14 + SC(split) 8 + SW1 6 + SW2 6 = 54/54.
 *
 * Sequential steps of one journey — retries off.
 */

import {
  resetCorridorDay,
  acceptOperation,
  bookContainers,
  closeBookingWindow,
  completeDocReview,
  createImportSchedule,
  db,
  departureAt,
  eatDayStr,
  endPaymentPhase,
  ensureCorridorRoute,
  forceReservationExpiry,
  forceWindowOpen,
  markPaid,
  pollAllocations,
  pollBookingStatus,
  pollDb,
  seedImportContract,
  settleViaGateway,
  setPriority,
  withBooking,
  withSchedule,
  type ScheduleRow,
} from "./import-utils";

const DEPARTURE = departureAt(6);
const BOOKING_DAY = eatDayStr(DEPARTURE);
/** The split remainder is rebooked onto a LATER train on the same corridor. */
const REMAINDER_DEPARTURE = departureAt(8);
const REMAINDER_DAY = eatDayStr(REMAINDER_DEPARTURE);

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

const ORDER = ["SA", "SB", "SD", "SC", "SW1", "SW2", "SW3"] as const;

describe("import: split offer, remainder rebooking, expiry + promotion", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    ORDER.forEach((suffix) => seedImportContract({ suffix, reference: stampedRef(suffix) }));
  });

  it("operations prepares the corridor train with an open first window", () => {
    ensureCorridorRoute();
    resetCorridorDay(DEPARTURE);
    resetCorridorDay(REMAINDER_DEPARTURE);
    createImportSchedule({ departure: DEPARTURE, locoPair: ["LOCO-IMP-5", "LOCO-IMP-6"] });
    withSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 45));
  });

  it("seven customers book in the first window; operations accepts them in priority order", () => {
    const shapes: Record<string, { twenty: number; forty: number }> = {
      SA: { twenty: 40, forty: 0 },
      SB: { twenty: 0, forty: 14 },
      SD: { twenty: 24, forty: 0 },
      SC: { twenty: 48, forty: 0 },
      SW1: { twenty: 12, forty: 0 },
      SW2: { twenty: 0, forty: 6 },
      SW3: { twenty: 40, forty: 0 },
    };
    let isoSeed = 1500;
    ORDER.forEach((suffix) => {
      const s = shapes[suffix];
      bookContainers({
        suffix,
        runStamp: stamp,
        isoSeed,
        twenty: s.twenty,
        forty: s.forty,
        scheduledDate: BOOKING_DAY,
      });
      isoSeed += s.twenty + s.forty;
      acceptOperation(suffix);
    });
    ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
  });

  it("the batch reserves SA/SB/SD whole and offers SC a PARTIAL for the last 8 wagons", () => {
    withSchedule(DEPARTURE, (s) => {
      closeBookingWindow(s.id);
      completeDocReview(s.id);
    });
    ["SA", "SB", "SD", "SC"].forEach((suffix) =>
      pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
    );
    // SC's reservation is a partial OFFER (16×20ft of its 48).
    withBooking("SC", (b) => {
      pollDb<{ status: string }>(
        "SC open partial offer",
        `SELECT status FROM freight.booking_batch_offers
         WHERE booking_id = $1 AND deleted_at IS NULL
         ORDER BY created_at DESC LIMIT 1`,
        [b.id],
        (row) => row?.status === "OFFERED",
        10,
      );
    });
    ["SW1", "SW2", "SW3"].forEach((suffix) =>
      withBooking(suffix, (b) => {
        expect(b.status, `${suffix} waiting`).to.eq("FULLY_EXECUTED");
      }),
    );
  });

  it("SA and SB pay; SC pays its partial — the split applies and the remainder is snapshotted", () => {
    markPaid("SA");
    pollAllocations("SA", 20);
    markPaid("SB");
    pollAllocations("SB", 14);

    // SC must settle through the real payment pipeline — only the settle path
    // applies the pending split offer (staff mark-paid allocates whole).
    settleViaGateway("SC");
    pollAllocations("SC", 8);
    withBooking("SC", (b) => {
      expect(b.is_split, "SC is split").to.eq(true);
      db<{ pre_split_quantities: { bySize?: Record<string, number> } | null; n: string }>(
        `SELECT pre_split_quantities FROM freight.bookings WHERE id = $1`,
        [b.id],
      ).then(({ rows }) => {
        expect(rows[0].pre_split_quantities, "pre-split snapshot").to.not.be.null;
      });
      // The booking itself shrank to the offered 16×20ft.
      db<{ q: string }>(
        `SELECT sum(quantity) AS q FROM freight.booking_container
         WHERE booking_id = $1 AND deleted_at IS NULL`,
        [b.id],
      ).then(({ rows }) => expect(Number(rows[0].q), "SC shrank to 16 boxes").to.eq(16));
    });
  });

  it("SD misses its pay window — EXPIRED, and the freed wagons promote SW1 + SW2 (payment sent)", () => {
    forceReservationExpiry("SD");
    // The settle promotes the waiting list in priority order into the freed 12
    // wagons: SW1 (6w) and SW2 (6w) fit; SW3 (20w) does not.
    ["SW1", "SW2"].forEach((suffix) =>
      pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
    );
    ["SW1", "SW2"].forEach((suffix) =>
      withBooking(suffix, (b) => {
        expect(b.payment_deadline, `${suffix} got a pay window`).to.be.a("string");
      }),
    );
    withBooking("SW3", (b) => {
      expect(b.status, "SW3 still has no seat").to.eq("FULLY_EXECUTED");
    });
  });

  it("SW1 and SW2 pay — the train is FULL at 54; SW3 expires with the day", () => {
    markPaid("SW1");
    pollAllocations("SW1", 6);
    markPaid("SW2");
    pollAllocations("SW2", 6);

    withSchedule(DEPARTURE, (s) => {
      endPaymentPhase(s.id);
      pollDb<ScheduleRow>(
        "window FULL + DONE",
        `SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
        [s.id],
        (row) => row?.booking_window_status === "FULL" && row?.window_phase === "DONE",
      );
      db<{ n: string }>(
        `SELECT count(DISTINCT wba.train_set_wagon_id) AS n
         FROM freight.wagon_booking_allocations wba
         JOIN freight.train_schedule_bookings tsb
           ON tsb.booking_id = wba.booking_id AND tsb.train_schedule_id = $1
         WHERE wba.deleted_at IS NULL AND tsb.deleted_at IS NULL`,
        [s.id],
      ).then(({ rows }) => expect(Number(rows[0].n), "54 wagons allocated").to.eq(54));
    });
    pollBookingStatus("SW3", "EXPIRED");
  });

  it("the split customer must rebook EXACTLY the whole remainder — wrong quantity rejected, exact accepted", () => {
    createImportSchedule({
      departure: REMAINDER_DEPARTURE,
      locoPair: ["LOCO-IMP-7", "LOCO-IMP-8"],
    });
    withSchedule(REMAINDER_DEPARTURE, (s) => forceWindowOpen(s.id, 45));

    // 48 booked − 16 shipped-by-split = 32×20ft outstanding. 8 ≠ 32 → rejected.
    bookContainers({
      suffix: "SC",
      runStamp: stamp,
      isoSeed: 3000,
      twenty: 8,
      scheduledDate: REMAINDER_DAY,
      expectFailure: "must take the whole remainder",
    });

    bookContainers({
      suffix: "SC",
      runStamp: stamp,
      isoSeed: 3100,
      twenty: 32,
      scheduledDate: REMAINDER_DAY,
    });
    pollBookingStatus("SC", "OPERATION_REQUEST_PENDING", 5);
  });
});

export {};
