/**
 * Export ONE_TIME journeys — two contracts ride the same export train
 * (Mojo Dry Port → Dire Dawa Yard → Djibouti Port Terminal):
 *
 *   A. CONTAINER (20ft + 40ft):
 *      portal wizard → staff approval chain → OTP sign → counter-sign
 *      → AWAITING_CLEARANCE_DOCUMENTS (export self-clear has no required
 *      docs in e2e) → ops finalize → FULLY_EXECUTED → customer books
 *      2 × 20ft + 1 × 40ft picking a real Shipment day → ops accepts the
 *      operation request → EXPORT is FCFS, so accept reserves the train slot
 *      immediately: SELECTED_FOR_BATCH with a pay deadline clamped to the
 *      export window close → staff mark-paid → PAID + SCHEDULED + linked.
 *
 *   B. BULK (E2E Wheat, 60 tons): same journey through the bulk wizard and
 *      bulk booking form, riding CW4 covered wagons on the same train.
 *
 * Infrastructure (route, built train, distances, rates, cargo types) comes
 * from seed-intercity.sql + seed-export.sql; the export route and the
 * departing-today schedule are created through the UI when missing.
 *
 * Sequential steps of one journey — retries off (steps are not idempotent).
 */

const customer = "user@gmail.com";
const companyTin = "0102030405"; // seed-company.sql
const opsStaff = "operation@edr.local";

const ORIGIN_YARD = "Mojo Dry Port";
const MID_YARD = "Dire Dawa Yard";
const PORT_YARD = "Djibouti Port Terminal";
const TRAIN_CODE = "TRN-E2E-1";

// Container numbers must be ISO (4 letters + 7 digits) and unused — stamp per run.
const stamp = String(Date.now());
const isoNumber = (prefix: string, offset: number) =>
  `${prefix}${String(Number(stamp.slice(-7)) + offset).padStart(7, "0")}`;

const apiUrl = () => Cypress.env("apiUrl") as string;

function dbContract(freight: "CONTAINER" | "BULK") {
  return cy.task<{ rows: Array<{ id: string; reference: string; status: string }> }>(
    "db:query",
    {
      sql: `SELECT ct.id, ct.reference, ct.status
            FROM freight.contracts ct
            JOIN freight.companies c ON c.id = ct.company_id
            WHERE c.tin = $1 AND ct.trade_direction = 'EXPORT' AND ct.freight_type = $2
            ORDER BY ct.created_at DESC LIMIT 1`,
      params: [companyTin, freight],
    },
  );
}

function withContract(
  freight: "CONTAINER" | "BULK",
  fn: (c: { id: string; reference: string; status: string }) => void,
) {
  dbContract(freight).then(({ rows }) => {
    expect(rows, `latest EXPORT ${freight} contract`).to.have.length(1);
    fn(rows[0]);
  });
}

function expectContractStatus(freight: "CONTAINER" | "BULK", expected: string) {
  dbContract(freight).then(({ rows }) => {
    expect(rows[0]?.status, "contract status").to.eq(expected);
  });
}

function dbBooking(freight: "CONTAINER" | "BULK") {
  return cy.task<{
    rows: Array<{
      id: string;
      reference: string;
      status: string;
      scheduling_status: string;
      train_schedule_id: string | null;
      payment_deadline: string | null;
      scheduled_date: string | null;
    }>;
  }>("db:query", {
    sql: `SELECT b.id, b.reference, b.status, b.scheduling_status,
                 b.train_schedule_id, b.payment_deadline, b.scheduled_date
          FROM freight.bookings b
          JOIN freight.companies c ON c.id = b.company_id
          WHERE c.tin = $1 AND b.trade_direction = 'EXPORT' AND b.freight_type = $2
          ORDER BY b.created_at DESC LIMIT 1`,
    params: [companyTin, freight],
  });
}

function withBooking(
  freight: "CONTAINER" | "BULK",
  fn: (b: {
    id: string;
    reference: string;
    status: string;
    scheduling_status: string;
    train_schedule_id: string | null;
    payment_deadline: string | null;
    scheduled_date: string | null;
  }) => void,
) {
  dbBooking(freight).then(({ rows }) => {
    expect(rows, `EXPORT ${freight} booking`).to.have.length(1);
    fn(rows[0]);
  });
}

/**
 * The journey's export schedule. Export trains must be scheduled ≥ the booking
 * lead (24h) ahead, and their window OPENS at departure − lead — so the spec
 * departs at now + 24h + a couple of minutes: creatable now, window opens
 * minutes later. (The intercity spec's train leaves in 2 days — outside 25h.)
 */
function dbUpcomingSchedule() {
  return cy.task<{
    rows: Array<{ id: string; window_closes_at: string; booking_window_status: string }>;
  }>("db:query", {
    // Scoped to THIS journey's corridor (… → Djibouti Port). Direction + a 25h
    // window alone also matches the segment-weight spec's trains, which run
    // Mojo → Nagad on the same day — and one of those is deliberately driven
    // FULL, so the unscoped query handed this spec a full schedule whose
    // booking window never opens.
    sql: `SELECT ts.id, ts.window_closes_at, ts.booking_window_status
          FROM freight.train_schedules ts
          JOIN freight.yards d ON d.id = ts.destination_station_id
          WHERE ts.direction = 'EXPORT' AND ts.deleted_at IS NULL
            AND d.code = 'DJIB_PORT'
            AND ts.scheduled_departure_date > now()
            AND ts.scheduled_departure_date < now() + interval '25 hours'
          ORDER BY ts.created_at DESC LIMIT 1`,
  });
}

/** The train departs ~24h out — bookings ride its departure day (tomorrow). */
const SHIPMENT_DAY = new Date(Date.now() + 24 * 3_600_000 + 150_000);

function fill(label: string | RegExp, value: string) {
  cy.contains("label", label)
    .invoke("attr", "for")
    .then((id) => {
      cy.get(`[id="${id}"]`).clear({ force: true }).type(value, { force: true });
    });
}

/**
 * Set one container-size line's quantity, found by its "20ft containers" /
 * "40ft containers" heading rather than by position in the list.
 */
function fillSizeQuantity(size: "20ft" | "40ft", value: string) {
  cy.contains(`${size} containers`, { timeout: 15000 })
    .closest("div.rounded-xl")
    .find('input[type="number"]')
    .first()
    .clear({ force: true })
    .type(value, { force: true });
}

/**
 * Choose the train's departure day on the Schedule card's INLINE calendar
 * (cargo-aware: days only unlock once the cargo details above are valid).
 */
function pickShipmentDay() {
  cy.contains(/available day/, { timeout: 30000 }).should("exist");
  // Day cells are plain buttons in a div grid; only bookable days are enabled
  // (out-of-month duplicates stay disabled).
  const day = String(SHIPMENT_DAY.getDate());
  cy.get("button:not(:disabled)", { timeout: 15000 })
    .contains(new RegExp(`^${day}$`))
    .click({ force: true });
}

/** Shared staff steps: accept + LINE_STAFF approve, then director approve. */
function approveChain(freight: "CONTAINER" | "BULK") {
  cy.loginBackoffice("marketer@edr.local");
  withContract(freight, (c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
  cy.contains("button", "Accept for approval", { timeout: 20000 }).click();
  // The modal takes an explicit validity window and pre-fills neither date.
  cy.acceptValidityWindow();
  cy.contains("button", "Accept & start approval", { timeout: 20000 })
    .should("not.be.disabled")
    .click();
  cy.contains("Approval chain", { timeout: 20000 }).should("be.visible");
  cy.contains("button", "Approve", { timeout: 20000 }).click();
  cy.contains("button", "Confirm approval").click();
  cy.contains("1/2", { timeout: 20000 }).should("be.visible");

  cy.loginBackoffice("director@edr.local");
  withContract(freight, (c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
  cy.contains("button", "Approve", { timeout: 20000 }).click();
  cy.contains("button", "Confirm approval").click();
  cy.contains("button", "View & sign", { timeout: 30000 }).should("exist");
  expectContractStatus(freight, "CONTRACT_READY");
}

/** Shared customer OTP-signature step. */
function customerSigns(freight: "CONTAINER" | "BULK") {
  cy.loginPortal(customer);
  withContract(freight, (c) => cy.visitPortal(`/contracts/${c.id}/view`));

  const unlockConsent = (attempt: number) => {
    cy.get('iframe[title="Contract document"]', { timeout: 30000 }).then(($f) => {
      const win = ($f[0] as HTMLIFrameElement).contentWindow;
      const el = win?.document?.scrollingElement ?? win?.document?.documentElement;
      if (win && el) {
        el.scrollTop = el.scrollHeight;
        win.dispatchEvent(new Event("scroll"));
      }
    });
    cy.wait(500).then(() => {
      cy.get("body").then(($b) => {
        // Only rendered once hasScrolledToBottom flips; the consent label is
        // always present, so matching it raced ahead of the scroll handler.
        if ($b.text().includes("You may now sign the contract")) return;
        expect(attempt, "consent bar unlocked").to.be.lessThan(20);
        unlockConsent(attempt + 1);
      });
    });
  };
  unlockConsent(0);

  // Check the input itself — clicking the label text does not toggle it.
  cy.contains("I have read the entire contract", { timeout: 15000 })
    .closest(".mantine-Checkbox-root")
    .find('input[type="checkbox"]')
    .check({ force: true });
  cy.contains("button", /^Sign contract$|^Approve & sign$/).click();
  cy.contains("label", "Full name")
    .invoke("attr", "for")
    .then((id) => {
      cy.get(`[id="${id}"]`).clear().type("Demo User");
    });
  cy.drawSignature();
  cy.uploadCompanyStamp();
  cy.contains("button", "Continue to verification").click();
  cy.contains("Verify it's you", { timeout: 20000 }).should("be.visible");
  cy.getOtp(customer).then((otp) => cy.typeOtp(otp));
  cy.contains("button", "Verify & sign").click();
  cy.contains("Your signature has been recorded", { timeout: 30000 }).should("be.visible");
  expectContractStatus(freight, "SIGNED_CUSTOMER");
}

/** Shared counter-sign + ops finalize (export self-clear: no required docs in e2e). */
function counterSignAndFinalize(freight: "CONTAINER" | "BULK") {
  cy.loginBackoffice("marketer@edr.local");
  withContract(freight, (c) => cy.visit(`/dashboard/contract-requests/${c.id}/view`));
  cy.contains("button", /^Sign as staff$|^Approve & sign$/, { timeout: 30000 }).click();
  cy.contains("label", "Full name")
    .invoke("attr", "for")
    .then((id) => {
      cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
    });
  cy.drawSignature();
  // Staff counter-sign gates on a stamp too, same as the customer's modal.
  cy.uploadCompanyStamp();
  cy.get(".mantine-Modal-content")
    .contains("button", /^Confirm signature$|^Approve & sign$/)
    .click();
  cy.contains("counter-signed", { timeout: 30000 }).should("be.visible");
  expectContractStatus(freight, "AWAITING_CLEARANCE_DOCUMENTS");

  cy.loginBackoffice(opsStaff);
  // Clearance review + finalize live solely on the Operations "Clearance
  // Documents" hub — the contract detail page no longer embeds a tab for it.
  withContract(freight, (c) =>
    cy.visit(`/dashboard/contracts/clearance-documents/${c.id}`),
  );
  cy.contains("button", "Finalize document approval", { timeout: 30000 })
    .should("not.be.disabled")
    .click();
  cy.contains("finalized", { timeout: 30000 }).should("be.visible");
  expectContractStatus(freight, "FULLY_EXECUTED");
}

/** Ops accept: EXPORT is FCFS — accept reserves the slot and opens the pay window. */
function acceptAndAssertReserved(freight: "CONTAINER" | "BULK") {
  cy.loginBackoffice(opsStaff);
  withBooking(freight, (b) => cy.visit(`/dashboard/booking-requests/${b.id}`));
  cy.contains("button", /Accept operation|^Accept$/, { timeout: 20000 }).click();
  cy.contains("Accept operation request?", { timeout: 15000 }).should("be.visible");
  cy.get(".mantine-Modal-content").contains("button", /^Accept$/).click();
  cy.get(".mantine-Modal-content", { timeout: 30000 }).should("not.exist");

  dbUpcomingSchedule().then(({ rows: schedules }) => {
    expect(schedules, "departing-today export schedule").to.have.length(1);
    withBooking(freight, (b) => {
      expect(b.status, "FCFS reservation").to.eq("SELECTED_FOR_BATCH");
      expect(b.train_schedule_id).to.eq(schedules[0].id);
      // Export parity: the pay window never outlives the booking window close.
      expect(b.payment_deadline, "pay deadline set").to.be.a("string");
      expect(new Date(b.payment_deadline!).getTime()).to.be.at.most(
        new Date(schedules[0].window_closes_at).getTime(),
      );
    });
  });
}

function markPaidAndAssertAllocated(freight: "CONTAINER" | "BULK") {
  withBooking(freight, (b) => {
    cy.apiLogin(opsStaff).then(({ token }) => {
      cy.request({
        method: "POST",
        url: `${apiUrl()}/api/train-scheduling/bookings/${b.id}/mark-paid`,
        headers: { Authorization: `Bearer ${token}` },
      })
        .its("status")
        .should("be.oneOf", [200, 201]);
    });
  });
  withBooking(freight, (b) => {
    expect(b.status).to.eq("PAID");
    expect(b.scheduling_status).to.eq("SCHEDULED");
    cy.task<{ rows: Array<{ n: string }> }>("db:query", {
      sql: `SELECT count(*) AS n FROM freight.train_schedule_bookings
            WHERE booking_id = $1 AND deleted_at IS NULL`,
      params: [b.id],
    }).then(({ rows }) => {
      expect(Number(rows[0].n), "train_schedule_bookings link").to.eq(1);
    });
  });
}

describe("export one-time journeys: container + bulk on one train", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-intercity.sql");
    cy.task("db:seedFile", "seed-export.sql");
    // Re-runnable against a warm DB: one active contract per customer +
    // service type + route, so an earlier run's ONE_TIME export contract
    // 409s this run at creation. A SPENT one (already booked) no longer
    // blocks, but a run that died before booking leaves a live one behind.
    cy.retireStaleContracts({
      tin: companyTin,
      kind: "ONE_TIME",
      direction: "EXPORT",
    });
    // The schedule step reuses an upcoming export schedule when one exists, but
    // a previous run's train is already loaded — and an export booking must
    // ride a single train WHOLE, so this run's booking then fails the space
    // check with "no open train on this day can carry it". Retire the older
    // schedules (unlinking their bookings) so this run gets an empty train.
    // Once per run: before() fires again on cross-origin visits, and a second
    // pass would delete the schedule this run had just created.
    cy.task("db:queryOnce", {
      key: "export_one_time:reset-upcoming-export-schedule",
      sql: `WITH stale AS (
              SELECT ts.id
                FROM freight.train_schedules ts
                JOIN freight.yards d
                  ON d.id = ts.destination_station_id AND d.code = 'DJIB_PORT'
               WHERE ts.direction = 'EXPORT'
                 AND ts.deleted_at IS NULL
                 AND ts.scheduled_departure_date > now()
                 AND ts.scheduled_departure_date < now() + interval '25 hours'
            ), unlink AS (
              UPDATE freight.bookings b
                 SET train_schedule_id = NULL,
                     scheduling_status = 'NOT_SCHEDULED',
                     status = CASE
                       WHEN b.status IN ('FULLY_EXECUTED','SELECTED_FOR_BATCH','AWAITING_PAYMENT')
                       THEN 'EXPIRED' ELSE b.status END
               WHERE b.train_schedule_id IN (SELECT id FROM stale)
            ), drop_links AS (
              UPDATE freight.train_schedule_bookings
                 SET deleted_at = now()
               WHERE train_schedule_id IN (SELECT id FROM stale)
                 AND deleted_at IS NULL
            )
            UPDATE freight.train_schedules
               SET deleted_at = now()
             WHERE id IN (SELECT id FROM stale)`,
    });
  });

  // ── Shared infrastructure ─────────────────────────────────────────────────

  it("operations ensures the export route exists", () => {
    cy.loginBackoffice(opsStaff);
    cy.task<{ rows: Array<{ n: string }> }>("db:query", {
      sql: `SELECT count(*) AS n
            FROM freight.routes r
            JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = 'MOJO'
            JOIN freight.yards d ON d.id = r.destination_yard_id AND d.code = 'DJIB_PORT'
            WHERE r.deleted_at IS NULL`,
    }).then(({ rows }) => {
      if (Number(rows[0].n) > 0) return;

      cy.visit("/dashboard/routes");
      cy.contains("button", "Add route", { timeout: 20000 }).click();
      cy.contains("Add Route", { timeout: 15000 }).should("be.visible");
      cy.get(".mantine-Modal-content").contains("button", "Add milestone").click();
      const pickYard = (index: number, yard: string) => {
        cy.get('.mantine-Modal-content input[placeholder="Select yard"]')
          .eq(index)
          .click({ force: true });
        cy.get('[role="option"]:visible').contains(yard).click();
      };
      pickYard(0, ORIGIN_YARD);
      pickYard(1, MID_YARD);
      pickYard(2, PORT_YARD);
      cy.get(".mantine-Modal-content").contains("button", "Save").click();
    });
  });

  it("operations schedules the export train — booking window opens", () => {
    cy.loginBackoffice(opsStaff);

    dbUpcomingSchedule().then(({ rows }) => {
      if (rows.length > 0) return;

      cy.visit("/dashboard/operations/train-scheduling-v2");
      cy.contains("button", "New schedule", { timeout: 20000 }).click();
      cy.contains("Create train schedule", { timeout: 15000 }).should("be.visible");
      // Anchor on the FIRST yard in the label. The corridor also has the
      // reverse (import) route, whose label ends at this same yard — an
      // unanchored match picks it, and the API then rejects the departure
      // against the 3-day import lead instead of the 24h export one.
      cy.mantineSelect(
        /^Route$/,
        new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
      );

      // Just past the 24h scheduling lead: creatable now, and the export
      // window (opens departure − lead) flips OPEN a couple of minutes later.
      const local = new Date(
        SHIPMENT_DAY.getTime() - SHIPMENT_DAY.getTimezoneOffset() * 60000,
      )
        .toISOString()
        .slice(0, 16);
      cy.get('.mantine-Modal-content input[type="datetime-local"]')
        .clear({ force: true })
        .type(local, { force: true });
      cy.mantineSelect(/^Train$/, new RegExp(TRAIN_CODE));
      cy.get(".mantine-Modal-content").contains("button", "Create").click();
      cy.location("pathname", { timeout: 30000 }).should(
        "match",
        /\/dashboard\/operations\/train-scheduling-v2\/.+/,
      );
    });

    // The export window opens at departure − 24h CLAMPED into the booking desk
    // hours (8–17 EAT), so a run outside those hours would sit at PRE_WINDOW
    // until the clamp time no matter how long it waited. Force it OPEN — this
    // spec exercises the FCFS reservation flow, not the window engine
    // (segment_weight arranges its windows the same way).
    dbUpcomingSchedule().then(({ rows }) => {
      expect(rows, "upcoming export schedule").to.have.length(1);
      cy.task("db:query", {
        sql: `UPDATE freight.train_schedules
              SET window_opens_at = LEAST(window_opens_at, now()),
                  window_phase = 'OPEN',
                  booking_window_status = 'OPEN'
              WHERE id = $1 AND booking_window_status <> 'FULL'`,
        params: [rows[0].id],
      });
    });

    // Belt-and-braces: confirm the engine keeps it OPEN.
    const waitForOpenWindow = (attempt: number) => {
      dbUpcomingSchedule().then(({ rows }) => {
        expect(rows, "upcoming export schedule").to.have.length(1);
        if (rows[0].booking_window_status === "OPEN") return;
        expect(attempt, "export booking window OPEN").to.be.lessThan(40);
        cy.wait(10000).then(() => waitForOpenWindow(attempt + 1));
      });
    };
    waitForOpenWindow(0);
  });

  // ── Journey A: container 20ft + 40ft ──────────────────────────────────────

  it("customer submits an export container contract (20ft + 40ft)", () => {
    cy.loginPortal(customer);
    cy.visitPortal("/contracts/new");

    cy.mantineSelect(/^Operation Type/, /^Export$/);
    cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
    cy.mantineSelect(/^New or Renewal/, "New Contract");
    cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
    cy.mantineSelect(/^Payment Currency/, /^ETB/);
    cy.contains("button", "Continue").click({ force: true });

    // Container contracts auto-cover both 20ft & 40ft (no size picker) and the
    // cargo description moved to booking time — the scope select is enough here.
    cy.mantineSelect(/^Cargo Scope/, /Containerized/);
    cy.mantineSelect(/^Origin Yard/, ORIGIN_YARD);
    cy.mantineSelect(/^Destination Yard/, PORT_YARD);
    cy.contains("button", "Continue").click({ force: true });

    cy.contains("button", "Submit").click({ force: true });
    cy.contains("Approve your quotation", { timeout: 30000 }).should("be.visible");
    cy.contains("button", "Approve & submit").click();
    cy.location("pathname", { timeout: 20000 }).should("eq", "/contracts");

    expectContractStatus("CONTAINER", "SUBMITTED");
  });

  it("staff approve the container contract (marketer + director)", () => {
    approveChain("CONTAINER");
  });

  it("customer signs the container contract with OTP", () => {
    customerSigns("CONTAINER");
  });

  it("staff counter-sign and operations finalize the container contract", () => {
    counterSignAndFinalize("CONTAINER");
  });

  it("customer books 2 × 20ft + 1 × 40ft with a shipment day", () => {
    cy.loginPortal(customer);
    withContract("CONTAINER", (c) => cy.visitPortal(`/contracts/${c.id}/bookings/new`));
    cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");

    // Address each size editor by its heading, not by position: the two lines
    // come out of the contract's cargo scope and are not guaranteed to be in
    // 20ft-then-40ft order. Reversed, this booked 1 × 20ft — an odd count,
    // which the form blocks (a lone 20ft can never be paired onto a wagon), so
    // "Review price & book" stayed disabled.
    fillSizeQuantity("20ft", "2");
    fillSizeQuantity("40ft", "1");

    cy.get('input[placeholder*="MSCU"]', { timeout: 15000 }).should("have.length", 3);
    cy.get('input[placeholder*="MSCU"]').eq(0).type(isoNumber("MSCU", 0));
    cy.get('input[placeholder*="MSCU"]').eq(1).type(isoNumber("TCLU", 1));
    cy.get('input[placeholder*="MSCU"]').eq(2).type(isoNumber("FSCU", 2));

    cy.get('input[placeholder*="24.5"]').each(($input) => {
      cy.wrap($input).clear({ force: true }).type("10", { force: true });
    });

    pickShipmentDay();

    cy.fillCargoDescription();
    cy.contains("button", "Review price & book").should("not.be.disabled").click();
    cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
    cy.contains("button", "Confirm & book").click();
    cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);

    withBooking("CONTAINER", (b) => {
      expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
      expect(b.scheduled_date, "export bookings carry a shipment day").to.be.a("string");
    });
  });

  it("operations accepts the container request — FCFS reserves today's train", () => {
    acceptAndAssertReserved("CONTAINER");
  });

  it("staff mark the container booking paid — allocated onto the train", () => {
    markPaidAndAssertAllocated("CONTAINER");
  });

  // ── Journey B: bulk (E2E Wheat) ───────────────────────────────────────────

  it("customer submits an export bulk contract (wheat)", () => {
    cy.loginPortal(customer);
    cy.visitPortal("/contracts/new");

    cy.mantineSelect(/^Operation Type/, /^Export$/);
    cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
    cy.mantineSelect(/^New or Renewal/, "New Contract");
    cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
    cy.mantineSelect(/^Payment Currency/, /^ETB/);
    cy.contains("button", "Continue").click({ force: true });

    cy.mantineSelect(/^Cargo Scope/, /General \/ Bulk cargo/);
    cy.mantineSelect(/^Bulk Cargo Type/, "E2E Grains");
    cy.mantineSelect(/^Commodity/, "E2E Wheat");
    cy.mantineSelect(/^Origin Yard/, ORIGIN_YARD);
    cy.mantineSelect(/^Destination Yard/, PORT_YARD);
    cy.contains("button", "Continue").click({ force: true });

    cy.contains("button", "Submit").click({ force: true });
    cy.contains("Approve your quotation", { timeout: 30000 }).should("be.visible");
    cy.contains("button", "Approve & submit").click();
    cy.location("pathname", { timeout: 20000 }).should("eq", "/contracts");

    expectContractStatus("BULK", "SUBMITTED");
  });

  it("staff approve the bulk contract (marketer + director)", () => {
    approveChain("BULK");
  });

  it("customer signs the bulk contract with OTP", () => {
    customerSigns("BULK");
  });

  it("staff counter-sign and operations finalize the bulk contract", () => {
    counterSignAndFinalize("BULK");
  });

  it("customer books 60 tons of wheat with a shipment day", () => {
    cy.loginPortal(customer);
    withContract("BULK", (c) => cy.visitPortal(`/contracts/${c.id}/bookings/new`));
    cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");

    fill(/^Quantity \(tons\)/, "60");
    pickShipmentDay();

    cy.fillCargoDescription();
    cy.contains("button", "Review price & book").should("not.be.disabled").click();
    cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
    cy.contains("button", "Confirm & book").click();
    cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);

    withBooking("BULK", (b) => {
      expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
    });
  });

  it("operations accepts the bulk request — FCFS reserves today's train", () => {
    acceptAndAssertReserved("BULK");
  });

  it("staff mark the bulk booking paid — allocated onto the train", () => {
    markPaidAndAssertAllocated("BULK");
  });
});

export {};
