/**
 * Segment weight & tolerance journeys — per-edge capacity on one corridor
 * (Mojo Dry Port → Dire Dawa Yard → Nagad Terminal), two dedicated
 * trains departing the same day (seed-segment-weight.sql):
 *
 *   TRN-SEG-W "tolerance train" — 240T pull; one loco carries a 90T overage
 *   tolerance, the second has NONE CONFIGURED (the S-2026-00024 regression
 *   pair: the set's tolerance must stay 90, not collapse to 0).
 *
 *     1. 130T wheat Mojo→Djibouti  → 179.6T gross (2 CW4) on both legs
 *     2. intercity 2×20ft VGM 24 Mojo→Dire → 70.4T gross (1 NW5); the shared
 *        Mojo→Dire leg hits 250T — OVER the 240T base, inside the 330T
 *        ceiling. Wagon allocation must succeed (allocation is what silently
 *        failed on S-2026-00024: PAID + SCHEDULED, zero allocations).
 *     3. a second identical ride-along → 320.4T, still inside the ceiling —
 *        the tolerance admits whole bookings repeatedly until spent
 *     4. the schedule detail strip shows the per-leg gross vs the ceiling
 *
 *   TRN-SEG-F "border-full train" — 200T pull, no tolerance, 4 NW5.
 *
 *     5. 8×20ft export boarding at DIRE (sub-corridor) commits all 4 wagons
 *        on the border edge (W's border edge only has 2 free, so FCFS lands
 *        it on F) → the window goes FULL for the trade direction
 *     6. the FULL train still accepts an intercity ride-along Mojo→Dire on
 *        its free home leg (the old whole-train sum — 169.6 + 70.4 = 240T >
 *        200T pull — rejected the accept outright; per-edge math admits it).
 *        Wagon ALLOCATION of that shared wagon is a known gap: physical
 *        pinning is slot-exclusive, so the test asserts accept + link only.
 *
 * Contracts are seeded FULLY_EXECUTED straight into SQL (stamped references,
 * re-runnable) — contract lifecycle is covered by the other flow specs; this
 * spec is about the scheduling engine. Run against a fresh e2e stack: the
 * trains' capacity math assumes empty consists.
 *
 * 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";

// Own corridor (…→ Nagad, not Djibouti Port): other specs schedule TRN-E2E-1
// on the Djibouti Port route, and an earlier-departing same-day train there
// would steal these FCFS bookings.
const ORIGIN_YARD = "Mojo Dry Port";
const MID_YARD = "Dire Dawa Yard";
const PORT_YARD = "Nagad Terminal, Djibouti";
const TRAIN_W = "TRN-SEG-W";
const TRAIN_F = "TRN-SEG-F";

const stamp = String(Date.now());
const isoNumber = (prefix: string, offset: number) =>
  `${prefix}${String(Number(stamp.slice(-7)) + offset).padStart(7, "0")}`;

// A ONE_TIME contract is spent after one booking, so every run seeds a fresh
// set with stamped references. Lookups go by SUFFIX + newest row: Cypress
// re-evaluates the spec bundle on cross-origin reloads, so a module-scope
// stamp drifts between tests and must never key a lookup.
const REF = {
  exportMojo: "EXP1",
  exportDire: "EXP2",
  ic1: "IC1",
  ic2: "IC2",
  ic3: "IC3",
} as const;
const stampedRef = (suffix: string) => `CTR-SEG-${stamp}-${suffix}`;

/** Both trains depart just past the 24h export lead — windows open in minutes. */
const DEPART_W = new Date(Date.now() + 24 * 3_600_000 + 4 * 60_000);
const DEPART_F = new Date(Date.now() + 24 * 3_600_000 + 7 * 60_000);

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

/** Seed one FULLY_EXECUTED ONE_TIME contract (contract + route + cargo scope). */
function seedContract(opts: {
  suffix: string;
  reference: string;
  direction: "EXPORT" | "DOMESTIC";
  freight: "CONTAINER" | "BULK";
  originCode: string;
  destCode: string;
}) {
  cy.task("db:query", {
    sql: `WITH c AS (
            INSERT INTO freight.contracts
              (reference, company_id, company_profile_id, contract_kind,
               trade_direction, freight_type, service_type_id, payment_currency,
               status, fully_executed_at, contract_valid_from,
               contract_valid_until, contract_summary)
            SELECT $1, comp.id,
                   -- bookings.company_profile_id is NOT NULL and inherits from
                   -- the contract: exporter profile for exports, any active
                   -- profile otherwise.
                   (SELECT p.id FROM freight.company_profiles p
                     WHERE p.company_id = comp.id AND p.deleted_at IS NULL
                     ORDER BY CASE
                       WHEN $2 = 'EXPORT' AND p.type = 'exporter' THEN 0
                       ELSE 1
                     END
                     LIMIT 1),
                   'ONE_TIME', $2, $3,
                   (SELECT st.id FROM freight.service_types st ORDER BY st.created_at LIMIT 1),
                   'ETB', 'FULLY_EXECUTED', now(), now() - interval '1 day',
                   now() + interval '60 days', 'E2E segment-weight fixture contract'
            FROM freight.companies comp
            WHERE comp.tin = $4
              -- before() re-runs on Cypress reloads: skip when this run
              -- already seeded a fresh, still-unbooked contract for the
              -- suffix, so lookups keep pointing at one stable row.
              AND NOT EXISTS (
                SELECT 1 FROM freight.contracts c2
                WHERE c2.reference LIKE 'CTR-SEG-%-' || $7
                  AND c2.deleted_at IS NULL
                  AND c2.created_at > now() - interval '15 minutes'
                  AND NOT EXISTS (
                    SELECT 1 FROM freight.bookings b2 WHERE b2.contract_id = c2.id
                  )
              )
            RETURNING id
          ), r AS (
            INSERT INTO freight.contract_routes
              (contract_id, origin_yard_id, destination_yard_id, sort_order)
            SELECT c.id, o.id, d.id, 0 FROM c
            JOIN freight.yards o ON o.code = $5
            JOIN freight.yards d ON d.code = $6
            RETURNING id
          )
          INSERT INTO freight.contract_cargo_scope
            (contract_id, container_size, cargo_type_id, cargo_free_text)
          SELECT c.id,
                 CASE WHEN $3 = 'CONTAINER' THEN '20ft' END,
                 CASE WHEN $3 = 'BULK' THEN
                   (SELECT ct.id FROM freight.cargo_types ct WHERE ct.code = 'E2E_WHEAT' LIMIT 1)
                 END,
                 'E2E segment-weight cargo'
          FROM c`,
    params: [
      opts.reference,
      opts.direction,
      opts.freight,
      companyTin,
      opts.originCode,
      opts.destCode,
      opts.suffix,
    ],
  });
}

/** Newest seeded contract for a suffix — stamp-agnostic (see REF). */
function dbContractId(suffix: string) {
  return cy
    .task<{ rows: Array<{ id: string }> }>("db:query", {
      sql: `SELECT id FROM freight.contracts
            WHERE reference LIKE 'CTR-SEG-%-' || $1
            ORDER BY created_at DESC LIMIT 1`,
      params: [suffix],
    })
    .then(({ rows }) => {
      expect(rows, `seeded contract *-${suffix}`).to.have.length(1);
      return cy.wrap(rows[0].id, { log: false });
    });
}

type BookingRow = {
  id: string;
  reference: string;
  status: string;
  scheduling_status: string;
  train_schedule_id: string | null;
  payment_deadline: string | null;
};

/** The (only) booking under this run's seeded contract for a suffix. */
function withBooking(suffix: string, fn: (b: BookingRow) => void) {
  cy.task<{ rows: BookingRow[] }>("db:query", {
    sql: `SELECT b.id, b.reference, b.status, b.scheduling_status,
                 b.train_schedule_id, b.payment_deadline
          FROM freight.bookings b
          JOIN freight.contracts ct ON ct.id = b.contract_id
          WHERE ct.reference LIKE 'CTR-SEG-%-' || $1
          ORDER BY b.created_at DESC LIMIT 1`,
    params: [suffix],
  }).then(({ rows }) => {
    expect(rows, `booking under *-${suffix}`).to.have.length(1);
    fn(rows[0]);
  });
}

type ScheduleRow = {
  id: string;
  booking_window_status: string;
  window_closes_at: string;
};

/** The live schedule riding a given built train on the NAGAD corridor. */
function dbScheduleFor(trainCode: string) {
  return cy.task<{ rows: ScheduleRow[] }>("db:query", {
    sql: `SELECT ts.id, ts.booking_window_status, ts.window_closes_at
          FROM freight.train_schedules ts
          JOIN freight.train_sets se ON se.id = ts.train_set_id
          JOIN freight.trains t ON t.id = se.train_id
          JOIN freight.yards d ON d.id = ts.destination_station_id
          WHERE t.code = $1 AND d.code = 'NAGAD' AND ts.deleted_at IS NULL
          ORDER BY ts.created_at DESC LIMIT 1`,
    params: [trainCode],
  });
}

function withSchedule(trainCode: string, fn: (s: ScheduleRow) => void) {
  dbScheduleFor(trainCode).then(({ rows }) => {
    expect(rows, `schedule for ${trainCode}`).to.have.length(1);
    fn(rows[0]);
  });
}

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 });
    });
}

/** Pick the departure day on the booking form's inline calendar. */
function pickShipmentDay(date: Date) {
  cy.contains(/available day/, { timeout: 30000 }).should("exist");
  const day = String(date.getDate());
  cy.get("button:not(:disabled)", { timeout: 15000 })
    .contains(new RegExp(`^${day}$`))
    .click({ force: true });
}

/** Create one schedule from a built train, departing at the given moment. */
function createSchedule(trainCode: string, departure: Date) {
  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");
  // Anchored at BOTH ends. /Nagad/ alone also matches the reverse (import)
  // route, which carries a different scheduling lead; anchoring only the
  // origin still collides with the other Mojo-origin corridor that runs on
  // through to Djibouti Port, whose schedules this spec's lookups ignore.
  cy.mantineSelect(
    /^Route$/,
    new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
  );
  const local = new Date(departure.getTime() - departure.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(trainCode));
  cy.get(".mantine-Modal-content").contains("button", "Create").click();
  cy.location("pathname", { timeout: 30000 }).should(
    "match",
    /\/dashboard\/operations\/train-scheduling-v2\/.+/,
  );
}

/** Ops accepts a booking's operation request from the booking-requests page. */
function acceptOperationRequest(contractRef: string) {
  cy.loginBackoffice(opsStaff);
  withBooking(contractRef, (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");
}

/** Book 2×20ft VGM 24T each under a seeded DOMESTIC contract (48T cargo, 1 NW5). */
function bookIntercityPair(contractRef: string, isoOffset: number) {
  cy.loginPortal(customer);
  dbContractId(contractRef).then((id) => cy.visitPortal(`/contracts/${id}/bookings/new`));
  cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");

  fill(/^Quantity/, "2");
  cy.get('input[placeholder*="MSCU"]', { timeout: 15000 }).should("have.length", 2);
  cy.get('input[placeholder*="MSCU"]').eq(0).type(isoNumber("MSCU", isoOffset));
  cy.get('input[placeholder*="MSCU"]').eq(1).type(isoNumber("TCLU", isoOffset + 1));
  cy.get('input[placeholder*="24.5"]').each(($input) => {
    cy.wrap($input).clear({ force: true }).type("24", { force: true });
  });
  cy.contains("Shipment day").should("not.exist");

  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\/.+/);
}

/** Accept a waiting intercity booking from a schedule's ride-along panel. */
function acceptRideAlong(trainCode: string, contractRef: string) {
  cy.loginBackoffice(opsStaff);
  withSchedule(trainCode, (s) =>
    cy.visit(`/dashboard/operations/train-scheduling-v2/${s.id}`),
  );
  cy.contains('[role="tab"]', "Workspace", { timeout: 30000 }).click();
  cy.contains("Intercity ride-along", { timeout: 30000 }).should("exist");
  withBooking(contractRef, (b) => {
    cy.contains("tr", b.reference, { timeout: 30000 })
      .find('input[type="checkbox"]')
      .check({ force: true });
    cy.contains("button", /Accept .*onto this train/).click();
    cy.contains("Awaiting payment", { timeout: 30000 }).should("exist");
  });
}

/** Staff mark-paid (no mounted UI button), then wait for wagon allocation. */
function markPaidAndAssertAllocated(contractRef: string) {
  withBooking(contractRef, (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(contractRef, (b) => {
    expect(b.status).to.eq("PAID");
    expect(b.scheduling_status).to.eq("SCHEDULED");

    // Wagon allocation runs async after allocate() — poll for its rows.
    // Zero allocations with a PAID/SCHEDULED booking is exactly the
    // S-2026-00024 failure shape this spec guards against.
    const waitForAllocation = (attempt: number) => {
      cy.task<{ rows: Array<{ n: string }> }>("db:query", {
        sql: `SELECT count(*) AS n FROM freight.wagon_booking_allocations
              WHERE booking_id = $1 AND deleted_at IS NULL`,
        params: [b.id],
      }).then(({ rows }) => {
        if (Number(rows[0].n) > 0) return;
        expect(attempt, `wagon allocations for ${contractRef}`).to.be.lessThan(20);
        cy.wait(2000).then(() => waitForAllocation(attempt + 1));
      });
    };
    waitForAllocation(0);
  });
}

describe(
  "segment weight: per-edge caps, loco tolerance, directional FULL",
  { retries: 0 },
  () => {
    before(() => {
      cy.task("db:seedFile", "seed-intercity.sql");
      cy.task("db:seedFile", "seed-export.sql");
      cy.task("db:seedFile", "seed-segment-weight.sql");
      seedContract({
        suffix: REF.exportMojo,
        reference: stampedRef(REF.exportMojo),
        direction: "EXPORT",
        freight: "BULK",
        originCode: "MOJO",
        destCode: "NAGAD",
      });
      seedContract({
        suffix: REF.exportDire,
        reference: stampedRef(REF.exportDire),
        direction: "EXPORT",
        freight: "CONTAINER",
        originCode: "DIRE_DAWA",
        destCode: "NAGAD",
      });
      for (const ref of [REF.ic1, REF.ic2, REF.ic3]) {
        seedContract({
          suffix: ref,
          reference: stampedRef(ref),
          direction: "DOMESTIC",
          freight: "CONTAINER",
          originCode: "MOJO",
          destCode: "DIRE_DAWA",
        });
      }
    });

    // ── 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 = 'NAGAD'
              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 both segment trains — same route, same day", () => {
      cy.loginBackoffice(opsStaff);
      // Two different physical trains may share a route+day (the guard only
      // blocks the SAME train twice); export windows are per-schedule.
      dbScheduleFor(TRAIN_W).then(({ rows }) => {
        if (rows.length === 0) createSchedule(TRAIN_W, DEPART_W);
      });
      dbScheduleFor(TRAIN_F).then(({ rows }) => {
        if (rows.length === 0) createSchedule(TRAIN_F, DEPART_F);
      });

      // The export window opens at departure − 24h CLAMPED into the booking
      // desk hours (8–17 EAT), and the engine can take minutes to advance a
      // second same-day train. Force both windows OPEN directly — the spec
      // arranges window state, it does not test the window engine.
      for (const trainCode of [TRAIN_W, TRAIN_F]) {
        dbScheduleFor(trainCode).then(({ rows }) => {
          expect(rows, `schedule for ${trainCode}`).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 them OPEN.
      const waitForOpen = (trainCode: string, attempt: number) => {
        dbScheduleFor(trainCode).then(({ rows }) => {
          expect(rows, `schedule for ${trainCode}`).to.have.length(1);
          if (rows[0].booking_window_status === "OPEN") return;
          expect(attempt, `${trainCode} window OPEN`).to.be.lessThan(60);
          cy.wait(10000).then(() => waitForOpen(trainCode, attempt + 1));
        });
      };
      waitForOpen(TRAIN_W, 0);
      waitForOpen(TRAIN_F, 0);
    });

    // ── Tolerance train (TRN-SEG-W) ──────────────────────────────────────────

    it("customer books 130T of wheat Mojo→Djibouti", () => {
      cy.loginPortal(customer);
      dbContractId(REF.exportMojo).then((id) =>
        cy.visitPortal(`/contracts/${id}/bookings/new`),
      );
      cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");
      fill(/^Quantity \(tons\)/, "130");
      pickShipmentDay(DEPART_W);
      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\/.+/);
    });

    it("the wheat lands on the tolerance train and allocates", () => {
      acceptOperationRequest(REF.exportMojo);

      // FCFS picks the earliest fitting train of the day — the W train.
      withSchedule(TRAIN_W, (s) => {
        withBooking(REF.exportMojo, (b) => {
          expect(b.status).to.eq("SELECTED_FOR_BATCH");
          expect(b.train_schedule_id, "reserved on the tolerance train").to.eq(s.id);
        });
      });
      markPaidAndAssertAllocated(REF.exportMojo);
    });

    it("customer books the first intercity pair Mojo→Dire", () => {
      bookIntercityPair(REF.ic1, 0);
    });

    it("the ride-along boards the shared leg through the overage tolerance", () => {
      // Mojo→Dire now carries 179.6T (wheat). Adding 70.4T (2×20ft VGM 24 on
      // one NW5) puts the leg at 250T — over the 240T base, inside 240+90.
      // Before the tolerance fix the second loco's NULL tolerance zeroed the
      // set and this exact allocation failed at "3500" scale.
      acceptOperationRequest(REF.ic1);
      acceptRideAlong(TRAIN_W, REF.ic1);

      withSchedule(TRAIN_W, (s) => {
        withBooking(REF.ic1, (b) => {
          expect(b.status).to.eq("SELECTED_FOR_BATCH");
          expect(b.train_schedule_id).to.eq(s.id);
          expect(new Date(b.payment_deadline!).getTime()).to.be.at.most(
            new Date(s.window_closes_at).getTime(),
          );
        });
      });
      markPaidAndAssertAllocated(REF.ic1);
    });

    it("customer books the second intercity pair Mojo→Dire", () => {
      bookIntercityPair(REF.ic2, 2);
    });

    it("a second ride-along still fits whole — tolerance spends per booking, not once", () => {
      // 250T + 70.4T = 320.4T on Mojo→Dire — still under the 330T ceiling.
      acceptOperationRequest(REF.ic2);
      acceptRideAlong(TRAIN_W, REF.ic2);
      markPaidAndAssertAllocated(REF.ic2);

      // The border edge (Dire→Djibouti) still has room, so the tolerance
      // train's window must NOT be FULL — fullness is directional.
      withSchedule(TRAIN_W, (s) => {
        expect(s.booking_window_status, "W window stays open").to.eq("OPEN");
      });
    });

    it("the schedule detail strip shows per-leg gross against the tolerance ceiling", () => {
      cy.loginBackoffice(opsStaff);
      withSchedule(TRAIN_W, (s) =>
        cy.visit(`/dashboard/operations/train-scheduling-v2/${s.id}`),
      );
      // Mojo→Dire: 179.6 (wheat) + 70.4 + 70.4 (ride-alongs) = 320.4T gross;
      // ceiling = 240 base + 90 tolerance = 330T (the weakest CONFIGURED
      // tolerance governs — the second loco has none set).
      cy.contains("320.4 / 330 T gross", { timeout: 30000 }).should("exist");
      // Border leg carries only the wheat.
      cy.contains("179.6 / 330 T gross").should("exist");
    });

    // ── Border-full train (TRN-SEG-F) ────────────────────────────────────────

    it("customer books an 8×20ft export from the MID yard", () => {
      cy.loginPortal(customer);
      dbContractId(REF.exportDire).then((id) =>
        cy.visitPortal(`/contracts/${id}/bookings/new`),
      );
      cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");
      fill(/^Quantity/, "8");
      cy.get('input[placeholder*="MSCU"]', { timeout: 15000 }).should("have.length", 8);
      for (let i = 0; i < 8; i += 1) {
        cy.get('input[placeholder*="MSCU"]').eq(i).type(isoNumber("MSCU", 10 + i));
      }
      cy.get('input[placeholder*="24.5"]').each(($input) => {
        cy.wrap($input).clear({ force: true }).type("10", { force: true });
      });
      pickShipmentDay(DEPART_F);
      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\/.+/);
    });

    it("the export fills the border edge — the window goes FULL for the direction", () => {
      acceptOperationRequest(REF.exportDire);

      // 8×20ft = 4 wagons. W's border edge has only 2 wagons free (the wheat
      // holds the other 2), so FCFS lands this Dire→Djibouti sub-corridor
      // booking on the F train — all 4 of its wagons, but only past Dire.
      withSchedule(TRAIN_F, (s) => {
        withBooking(REF.exportDire, (b) => {
          expect(b.status).to.eq("SELECTED_FOR_BATCH");
          expect(b.train_schedule_id, "reserved on the border-full train").to.eq(s.id);
        });
      });
      markPaidAndAssertAllocated(REF.exportDire);

      // Every wagon on the border edge is committed: the train is FULL for
      // its trade direction even though Mojo→Dire runs completely empty.
      const waitForFull = (attempt: number) => {
        dbScheduleFor(TRAIN_F).then(({ rows }) => {
          if (rows[0]?.booking_window_status === "FULL") return;
          expect(attempt, "F window FULL").to.be.lessThan(20);
          cy.wait(3000).then(() => waitForFull(attempt + 1));
        });
      };
      waitForFull(0);
    });

    it("customer books the third intercity pair Mojo→Dire", () => {
      bookIntercityPair(REF.ic3, 4);
    });

    it("the FULL train still accepts and allocates an intercity ride-along on its free leg", () => {
      // Mojo→Dire on the F train is empty (the export boards at Dire): the
      // ride-along uses the SAME wagons there and alights before they load.
      // The old whole-train sum — 169.6 + 70.4 = 240T > 200T pull — rejected
      // this; per-edge math sees 70.4T on Mojo→Dire and 169.6T on the border,
      // both within the cap. The FULL flag closes the export window only.
      acceptOperationRequest(REF.ic3);
      acceptRideAlong(TRAIN_F, REF.ic3);

      withSchedule(TRAIN_F, (s) => {
        withBooking(REF.ic3, (b) => {
          expect(b.status).to.eq("SELECTED_FOR_BATCH");
          expect(b.train_schedule_id, "accepted onto the FULL train").to.eq(s.id);
        });
      });

      // Mark paid: PAID + SCHEDULED + linked. Wagon allocation is asserted
      // only as far as today's model supports: physical wagon pinning is
      // slot-exclusive (one wagon serves ONE slot), so the same wagon cannot
      // yet be pinned to the intercity's Mojo→Dire slot AND the export's
      // Dire→Nagad slot even though the per-edge budget admits both. KNOWN
      // GAP — when wagon↔slot pinning becomes leg-aware, restore
      // markPaidAndAssertAllocated(REF.ic3) here.
      withBooking(REF.ic3, (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(REF.ic3, (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);
        });
      });

      // The ride-along never reopens the export window.
      withSchedule(TRAIN_F, (s) => {
        expect(s.booking_window_status, "F window stays FULL").to.eq("FULL");
      });
    });
  },
);

export {};
