/**
 * BOOKING — cancel lifecycle + the commit gate:
 *
 *   `POST /bookings/:id/cancel` is only legal for a booking that has not yet
 *   been committed to a train (DRAFT … OPERATION_REQUEST_PENDING). This spec
 *   proves both sides end to end, which the unit tests never do:
 *
 *     – an un-accepted booking (OPERATION_REQUEST_PENDING) cancels → CANCELLED,
 *       and its open payable invoice is expired (no dangling payable)
 *     – cancelling it again is rejected (status CANCELLED not allowed)
 *     – a PAID + allocated booking CANNOT be cancelled — the guard blocks it.
 *       (There is deliberately no cancel-after-allocation / refund path; a
 *       committed booking leaves only by EXPIRE, never customer cancel.)
 *
 * EXPORT bulk corridor (FCFS: accept reserves immediately, mark-paid allocates).
 * Retries off — sequential steps of one journey.
 */

import {
  acceptExport,
  apiPost,
  bookBulk,
  createImportSchedule,
  db,
  departureAt,
  eatDayStr,
  ensureExportRoute,
  EXP_DEST,
  EXP_ORIGIN,
  forceWindowOpen,
  markPaid,
  pollAllocations,
  resetCorridorDay,
  seedImportContract,
  superAdmin,
  withBooking,
  withExportSchedule,
} from "./import-utils";

const DEPARTURE = departureAt(6);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const stamp = String(Date.now());

function seedBulkExport(suffix: string) {
  seedImportContract({
    suffix,
    reference: `CTR-IMP-${stamp}-${suffix}`,
    freight: "BULK",
    direction: "EXPORT",
    originCode: EXP_ORIGIN,
    destCode: EXP_DEST,
  });
}

/** Fire cancel with a reason; caller decides whether to fail on non-2xx. */
function cancel(bookingId: string, failOnStatusCode = true) {
  return apiPost(
    superAdmin,
    `/api/bookings/${bookingId}/cancel`,
    { reason: `E2E cancel ${stamp}` },
    failOnStatusCode,
  );
}

describe("booking cancel: pre-commit only, blocked once allocated", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    ["CXA", "CXB"].forEach(seedBulkExport);
  });

  it("operations opens the export corridor + a D+6 CW4 train", () => {
    ensureExportRoute();
    resetCorridorDay(DEPARTURE, EXP_DEST, EXP_ORIGIN);
    createImportSchedule({
      departure: DEPARTURE,
      locoPair: ["LOCO-EXP-1", "LOCO-EXP-2"],
      kind: "bulk",
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
    withExportSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
  });

  it("an un-accepted booking cancels, its invoice is expired, and re-cancel is rejected", () => {
    bookBulk({ suffix: "CXA", tons: 700, scheduledDate: BOOKING_DAY });

    // Fresh non-customs export booking sits at OPERATION_REQUEST_PENDING.
    withBooking("CXA", (b) => {
      expect(b.status, "pre-accept status").to.eq("OPERATION_REQUEST_PENDING");
      cancel(b.id).then((res) => expect(res.status, "cancelled").to.be.oneOf([200, 201]));
    });

    withBooking("CXA", (b) => {
      expect(b.status, "now CANCELLED").to.eq("CANCELLED");
      // No open payable invoice may survive a cancel.
      db<{ n: string }>(
        `SELECT count(*) AS n FROM freight.invoices
          WHERE source = 'booking' AND source_id = $1
            AND status NOT IN ('EXPIRED','CANCELLED','PAID')
            AND deleted_at IS NULL`,
        [b.id],
      ).then(({ rows }) => expect(Number(rows[0].n), "no open invoice left").to.eq(0));

      // Cancelling a CANCELLED booking is rejected by the status guard.
      cancel(b.id, false).then((res) => {
        expect(res.status, "re-cancel rejected").to.be.within(400, 422);
        expect(JSON.stringify(res.body)).to.include("Cannot perform this action");
        expect(JSON.stringify(res.body)).to.include("CANCELLED");
      });
    });
  });

  it("a PAID + allocated booking cannot be cancelled — the commit gate blocks it", () => {
    bookBulk({ suffix: "CXB", tons: 700, scheduledDate: BOOKING_DAY });
    acceptExport("CXB");
    markPaid("CXB");
    pollAllocations("CXB", 10); // 700T / 70T = 10 CW4 wagons

    withBooking("CXB", (b) => {
      expect(b.status, "committed + paid").to.eq("PAID");
      cancel(b.id, false).then((res) => {
        expect(res.status, "cancel blocked after allocation").to.be.within(400, 422);
        expect(JSON.stringify(res.body)).to.include("Cannot perform this action");
        expect(JSON.stringify(res.body)).to.include("PAID");
      });
    });

    // The block is real: the booking still rides, wagons still allocated.
    withBooking("CXB", (b) => expect(b.status, "still PAID").to.eq("PAID"));
    pollAllocations("CXB", 10);
  });
});

export {};
