/**
 * Intercity ONE_TIME journey — the full life of a domestic ride-along shipment:
 *
 *   1. portal    — customer creates an INTERCITY / One-Time / Container contract
 *                  (Mojo Dry Port → Dire Dawa Yard) and submits it
 *   2. backoffice — marketer REJECTS it with a reason
 *   3. portal    — customer sees the rejection banner + reason, then submits a
 *                  fresh contract
 *   4. backoffice — marketer accepts + approves LINE_STAFF, director approves
 *                  → CONTRACT_READY
 *   5. portal    — customer OTP-signs → SIGNED_CUSTOMER
 *   6. backoffice — marketer counter-signs → AWAITING_CLEARANCE_DOCUMENTS
 *                  (ONE_TIME intercity always routes through the
 *                  intercity-documents step; the fixture seeds one REQUIRED
 *                  document so the step is real)
 *   6b. portal   — customer uploads the required intercity document
 *                  → CLEARANCE_UNDER_REVIEW
 *   7. backoffice — operations approves the document, then finalizes document
 *                  approval → FULLY_EXECUTED
 *   8. portal    — customer books 2 × 20ft under the contract (intercity has
 *                  no shipment date) → booking OPERATION_REQUEST_PENDING
 *   9. backoffice — operations accepts the operation request → booking
 *                  FULLY_EXECUTED (intercity waiting pool)
 *  10. backoffice — operations creates the EXPORT route
 *                  Mojo → Dire Dawa → Djibouti Port (distances seeded)
 *  11. backoffice — operations schedules the export train (built Train-Builder
 *                  train seeded by seed-intercity.sql)
 *  12. backoffice — operations accepts the intercity booking onto the train
 *                  (Workspace → Intercity ride-along) → SELECTED_FOR_BATCH with
 *                  a pay deadline that never outlives the export window close
 *  13. staff mark-paid (API — the batch panel has no mounted UI button)
 *                  → PAID + SCHEDULED + linked to the schedule
 *
 * 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 DEST_YARD = "Dire Dawa Yard";
const PORT_YARD = "Djibouti Port Terminal";
const TRAIN_CODE = "TRN-E2E-1";

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

/** Latest contract of the seeded company — the journey's contract. */
function dbContract() {
  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 = 'DOMESTIC'
            ORDER BY ct.created_at DESC LIMIT 1`,
      params: [companyTin],
    },
  );
}

function withContract(fn: (c: { id: string; reference: string; status: string }) => void) {
  dbContract().then(({ rows }) => {
    expect(rows, "latest contract for the seeded company").to.have.length(1);
    fn(rows[0]);
  });
}

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

/** Latest DOMESTIC booking of the seeded company — the journey's booking. */
function dbBooking() {
  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 = 'DOMESTIC'
          ORDER BY b.created_at DESC LIMIT 1`,
    params: [companyTin],
  });
}

function withBooking(
  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().then(({ rows }) => {
    expect(rows, "intercity booking for the seeded company").to.have.length(1);
    fn(rows[0]);
  });
}

/**
 * The export schedule this journey rides — scoped to a MOJO-origin EXPORT run,
 * the same shape the create-guard below counts. Taking the newest row in the
 * whole table instead picked up whatever schedule another spec happened to
 * create last (an IMPORT one, in a full-suite run).
 */
function dbSchedule() {
  return cy.task<{
    rows: Array<{ id: string; status: string; direction: string; window_closes_at: string }>;
  }>("db:query", {
    sql: `SELECT ts.id, ts.status, ts.direction, ts.window_closes_at
          FROM freight.train_schedules ts
          JOIN freight.routes r ON r.id = ts.route_id
          JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = 'MOJO'
          WHERE ts.direction = 'EXPORT' AND ts.deleted_at IS NULL
          ORDER BY ts.created_at DESC LIMIT 1`,
  });
}

/**
 * Fill the N-th labelled Mantine input (label[for] → input id). Indexed
 * because the booking form renders one "Quantity *" per container size.
 */
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 });
}

/**
 * Run the portal wizard for an INTERCITY / One-Time / Container contract and
 * submit it. Reused for the initial (to-be-rejected) and the second contract.
 */
function createIntercityContract() {
  cy.loginPortal(customer);
  cy.visitPortal("/contracts/new");

  // Step 0 — Setup. Intercity forces ETB and hides the customs section.
  cy.mantineSelect(/^Operation Type/, /^Intercity$/);
  cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
  cy.mantineSelect(/^New or Renewal/, "New Contract");
  cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click();
  cy.mantineSelect(/^Payment Currency/, /^ETB/);
  cy.contains("button", "Continue").click({ force: true });

  // Step 1 — Cargo & Route (Ethiopian yards only for intercity). 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/, DEST_YARD);
  cy.contains("button", "Continue").click({ force: true });

  // Step 2 — Review & Submit → quotation modal.
  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");

  dbContract().then(({ rows }) => {
    expect(rows, "contract row").to.have.length(1);
    expect(rows[0].status).to.eq("SUBMITTED");
    expect(rows[0].reference).to.match(/^CTR-/);
  });
}

describe("intercity one-time journey: contract → booking → export train", { retries: 0 }, () => {
  before(() => {
    // Container types, locomotives, built train, yard distances — the
    // infrastructure the UI journey cannot create in-flow.
    cy.task("db:seedFile", "seed-intercity.sql");
    // Re-runnable against a warm DB: an earlier run's DOMESTIC ONE_TIME
    // contract on this lane would 409 this run's at creation (a spent one no
    // longer blocks, but an unbooked leftover does).
    cy.retireStaleContracts({
      tin: companyTin,
      kind: "ONE_TIME",
      direction: "DOMESTIC",
    });
  });

  // ── Contract: submit → reject → resubmit → approve → sign ────────────────

  it("customer submits an intercity one-time contract", () => {
    createIntercityContract();
  });

  it("marketer rejects the submission with a reason", () => {
    cy.loginBackoffice("marketer@edr.local");
    withContract((c) => cy.visit(`/dashboard/contract-requests/${c.id}`));

    cy.contains("button", "Reject contract", { timeout: 20000 }).click();
    cy.get(".mantine-Modal-content")
      .contains("label", "Reason for rejection")
      .invoke("attr", "for")
      .then((id) => {
        cy.get(`[id="${id}"]`).type("E2E rejection — cargo details incomplete");
      });
    cy.get(".mantine-Modal-content").contains("button", /^Reject$/).click();

    // Modal closes on success; the status pill can sit inside clipped layout,
    // so the authoritative check is the DB row.
    cy.get(".mantine-Modal-content", { timeout: 20000 }).should("not.exist");
    expectContractStatus("REJECTED");
  });

  it("customer sees the rejection reason on the contracts list", () => {
    cy.loginPortal(customer);
    cy.visitPortal("/contracts");

    // The list is a collapsed table — expand the rejected contract's row to
    // reveal its step banner with the staff reason.
    withContract((c) => {
      cy.contains("tr", c.reference, { timeout: 20000 })
        .find("button")
        .first()
        .click();
    });
    cy.contains("This contract was rejected.", { timeout: 20000 }).should("be.visible");
    cy.contains("Reason: E2E rejection — cargo details incomplete").should("be.visible");
  });

  it("customer submits a fresh intercity contract", () => {
    createIntercityContract();
  });

  it("marketer accepts the submission and approves the LINE_STAFF step", () => {
    cy.loginBackoffice("marketer@edr.local");
    withContract((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");

    expectContractStatus("PENDING_APPROVAL");
  });

  it("director approves the final step — contract PDF becomes ready", () => {
    cy.loginBackoffice("director@edr.local");
    withContract((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("CONTRACT_READY");
  });

  it("customer signs the contract with OTP", () => {
    cy.loginPortal(customer);
    withContract((c) => cy.visitPortal(`/contracts/${c.id}/view`));

    // Scroll the contract iframe to the bottom so the consent bar unlocks.
    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("SIGNED_CUSTOMER");
  });

  it("marketer counter-signs — intercity one-time enters the documents step", () => {
    cy.loginBackoffice("marketer@edr.local");
    withContract((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");

    // DOMESTIC one-time always routes through the intercity-documents step —
    // unlike GENERAL, it does NOT go straight to CONTRACT_ACTIVE.
    expectContractStatus("AWAITING_CLEARANCE_DOCUMENTS");
  });

  it("customer uploads the required intercity document", () => {
    cy.loginPortal(customer);
    // Deep link auto-opens the clearance documents modal.
    withContract((c) => cy.visitPortal(`/contracts/${c.id}?action=clearance`));

    cy.contains("Cargo Manifest", { timeout: 30000 }).should("exist");
    cy.get('.mantine-Modal-content input[type="file"]')
      .first()
      .selectFile("cypress/fixtures/docs/license.pdf", { force: true });
    cy.contains("button", "Submit documents", { timeout: 15000 })
      .should("not.be.disabled")
      .click();

    // Upload hands the contract to Operations review — the card flips to
    // "Under review" (the host modal may linger while queries refetch).
    cy.contains("Under review", { timeout: 30000 }).should("exist");
    expectContractStatus("CLEARANCE_UNDER_REVIEW");
  });

  it("operations approves the document and finalizes — contract fully executed", () => {
    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((c) =>
      cy.visit(`/dashboard/contracts/clearance-documents/${c.id}`),
    );

    // Approve the uploaded Cargo Manifest, then finalize.
    cy.contains("button", /Approve all/, { timeout: 30000 }).click();
    cy.contains("1/1 approved", { timeout: 30000 }).should("exist");

    cy.contains("button", "Finalize document approval", { timeout: 30000 })
      .should("not.be.disabled")
      .click();

    cy.contains("finalized", { timeout: 30000 }).should("be.visible");
    expectContractStatus("FULLY_EXECUTED");
  });

  // ── Booking under the contract ────────────────────────────────────────────

  it("customer books 2 × 20ft under the contract (no shipment date for intercity)", () => {
    cy.loginPortal(customer);
    withContract((c) => cy.visitPortal(`/contracts/${c.id}/bookings/new`));

    cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");

    // Container contracts cover BOTH sizes, so the form renders a 20ft and a
    // 40ft line, each seeded quantity 1. Book 2 × 20ft (pairs share a wagon,
    // so the count must be even) and zero the 40ft line — otherwise its
    // default unit adds a third container-number input.
    fillSizeQuantity("20ft", "2");
    fillSizeQuantity("40ft", "0");

    // One ISO container number per unit.
    cy.get('input[placeholder*="MSCU"]', { timeout: 15000 }).should("have.length", 2);
    cy.get('input[placeholder*="MSCU"]').eq(0).type("MSCU1234567");
    cy.get('input[placeholder*="MSCU"]').eq(1).type("TCLU7654321");

    // VGM per unit — column inputs carry a placeholder, not a linked label.
    cy.get('input[placeholder*="24.5"]').each(($input) => {
      cy.wrap($input).clear({ force: true }).type("10", { force: true });
    });

    // Intercity: no "Shipment day" picker — the ride-along note renders instead.
    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\/.+/);

    withBooking((b) => {
      expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
      expect(b.scheduled_date, "intercity bookings carry no scheduled date").to.eq(null);
    });
  });

  it("operations accepts the operation request — booking joins the intercity pool", () => {
    cy.loginBackoffice(opsStaff);
    withBooking((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();

    withBooking((b) => {
      expect(b.status, "accepted intercity booking waits in the pool").to.eq("FULLY_EXECUTED");
      expect(b.train_schedule_id).to.eq(null);
    });
  });

  // ── Route + export schedule ───────────────────────────────────────────────

  it("operations creates the export route Mojo → Dire Dawa → Djibouti Port", () => {
    cy.loginBackoffice(opsStaff);

    // Skip creation when a previous run already added the route (unique yards
    // pair) — the journey stays re-runnable against a warm DB.
    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");

      // Third stop row, then fill Origin / Milestone / Destination in order.
      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 });
        // Three yard selects share option texts — only the open dropdown counts.
        cy.get('[role="option"]:visible').contains(yard).click();
      };
      pickYard(0, ORIGIN_YARD);
      pickYard(1, DEST_YARD);
      pickYard(2, PORT_YARD);

      // Distances (when the build has them) resolve from the seeded rows.
      cy.get(".mantine-Modal-content").contains("button", "Save").click();
    });

    cy.task<{ rows: Array<{ direction: string }> }>("db:query", {
      sql: `SELECT r.direction
            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 }) => {
      expect(rows, "export route").to.have.length.at.least(1);
      expect(rows[0].direction).to.eq("EXPORT");
    });
  });

  it("operations schedules the export train from the built consist", () => {
    cy.loginBackoffice(opsStaff);

    // One departure per route per day — a warm DB from a previous run already
    // has this train scheduled, so only create when none is live.
    cy.task<{ rows: Array<{ n: string }> }>("db:query", {
      sql: `SELECT count(*) AS n
            FROM freight.train_schedules ts
            JOIN freight.routes r ON r.id = ts.route_id
            JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = 'MOJO'
            WHERE ts.status IN ('DRAFT', 'SCHEDULED') AND ts.deleted_at IS NULL`,
    }).then(({ rows }) => {
      if (Number(rows[0].n) > 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");

      // Anchored: the reverse (import) route ends at this same yard, and
      // picking it would schedule against the import lead rule.
      cy.mantineSelect(
        /^Route$/,
        new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
      );

      // Two days out, local datetime-local format.
      const departure = new Date(Date.now() + 2 * 86400000);
      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(TRAIN_CODE));
      cy.get(".mantine-Modal-content").contains("button", "Create").click();

      // Create navigates straight to the new schedule's detail page.
      cy.location("pathname", { timeout: 30000 }).should(
        "match",
        /\/dashboard\/operations\/train-scheduling-v2\/.+/,
      );
    });

    dbSchedule().then(({ rows }) => {
      expect(rows, "created schedule").to.have.length(1);
      expect(rows[0].direction).to.eq("EXPORT");
    });
  });

  // ── Intercity ride-along: accept → pay → allocated ────────────────────────

  it("operations accepts the intercity booking onto the export train", () => {
    cy.loginBackoffice(opsStaff);
    dbSchedule().then(({ rows: schedules }) => {
      cy.visit(`/dashboard/operations/train-scheduling-v2/${schedules[0].id}`);
    });

    cy.contains('[role="tab"]', "Workspace", { timeout: 30000 }).click();
    // Presence, not viewport visibility — the panel can sit below the fold /
    // inside clipped layout once earlier runs' rows stack up.
    cy.contains("Intercity ride-along", { timeout: 30000 }).should("exist");

    withBooking((b) => {
      cy.contains("tr", b.reference, { timeout: 30000 })
        .find('input[type="checkbox"]')
        .check({ force: true });
      cy.contains("button", /Accept .*onto this train/).click();

      // Accepted table shows the pay-window state (inside a horizontal
      // Table.ScrollContainer — assert presence, not viewport visibility).
      cy.contains("Awaiting payment", { timeout: 30000 }).should("exist");
    });

    // Export parity: the ride-along's pay deadline never outlives the export
    // booking window (reserve() clamps it to window_closes_at).
    dbSchedule().then(({ rows: schedules }) => {
      withBooking((b) => {
        expect(b.status).to.eq("SELECTED_FOR_BATCH");
        expect(b.train_schedule_id).to.eq(schedules[0].id);
        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(),
        );
      });
    });
  });

  it("staff mark the ride-along paid — booking allocates onto the train", () => {
    // ScheduleBatchPanel (the only "Mark paid" button) is not mounted in the
    // current UI, so drive the staff override endpoint directly.
    withBooking((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((b) => {
      expect(b.status).to.eq("PAID");
      expect(b.scheduling_status).to.eq("SCHEDULED");
      expect(b.train_schedule_id, "still pinned to the export train").to.be.a("string");

      // The schedule↔booking link row is what makes the booking visible on the
      // train board, in yard work, and to the wagon planner.
      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);
      });
    });
  });
});

export {};
