/**
 * Shared helpers for the IMPORT corridor flow specs.
 *
 * Corridor (A→B→C→D→E→T, DJ→ET = IMPORT):
 *   DJIB_PORT → NAGAD → DIRE_DAWA → E2E_AWASH → MOJO → KALITY
 *
 * Philosophy (same as segment_weight.cy.ts): these specs test the
 * scheduling/window/clearance ENGINE, not the contract wizard — contracts are
 * seeded FULLY_EXECUTED in SQL with stamped references; bookings, staff
 * reviews, window phases, payment, dispatch and clearance run through the real
 * API + UI. Window *timestamps* are arranged via db:query (the specs arrange
 * window state, they don't test the wall clock), and every transition is then
 * performed by the app's own 10s window tick or its staff endpoints.
 *
 * No module-level state besides constants: Cypress re-evaluates the spec
 * bundle on cross-origin reloads, so helpers look rows up by stamped-reference
 * SUFFIX + newest row, never by captured ids.
 */

export const customer = "user@gmail.com";
export const companyTin = "0102030405"; // seed-company.sql
export const opsStaff = "operation@edr.local";
/** isSuperAdmin bypasses assertFreightPermission — used for GL endpoints so a
 *  missing preset permission never masks an engine regression. */
export const superAdmin = "superadmin@tria.com";

export const CORRIDOR = ["DJIB_PORT", "NAGAD", "DIRE_DAWA", "E2E_AWASH", "MOJO", "KALITY"] as const;
export const ORIGIN = "DJIB_PORT";
export const DEST = "KALITY";
/** Export rides the same corridor reversed (ET → DJ). */
export const EXP_ORIGIN = "KALITY";
export const EXP_DEST = "DJIB_PORT";

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

// ---------------------------------------------------------------------------
// small generic plumbing
// ---------------------------------------------------------------------------

export type Row = Record<string, string | number | null>;

export function db<T = Row>(sql: string, params: unknown[] = []) {
  return cy.task<{ rowCount: number; rows: T[] }>("db:query", { sql, params }, { log: false });
}

/** Bearer token for a staff/customer account (portal users use the demo pwd). */
export function tokenFor(email: string): Cypress.Chainable<string> {
  const pass =
    email.endsWith("@gmail.com") ? (Cypress.env("demoPassword") as string) : undefined;
  return cy.apiLogin(email, pass).then(({ token }) => cy.wrap(token, { log: false }));
}

export function apiPost(
  email: string,
  path: string,
  body?: unknown,
  failOnStatusCode = true,
) {
  return tokenFor(email).then((token) =>
    cy.request({
      method: "POST",
      url: `${apiUrl()}${path}`,
      headers: { Authorization: `Bearer ${token}` },
      body: body ?? {},
      failOnStatusCode,
    }),
  );
}

export function apiGet(email: string, path: string, failOnStatusCode = true) {
  return tokenFor(email).then((token) =>
    cy.request({
      method: "GET",
      url: `${apiUrl()}${path}`,
      headers: { Authorization: `Bearer ${token}` },
      failOnStatusCode,
    }),
  );
}

export function apiPatch(
  email: string,
  path: string,
  body?: unknown,
  failOnStatusCode = true,
) {
  return tokenFor(email).then((token) =>
    cy.request({
      method: "PATCH",
      url: `${apiUrl()}${path}`,
      headers: { Authorization: `Bearer ${token}` },
      body: body ?? {},
      failOnStatusCode,
    }),
  );
}

/** Poll a 1-row query until `check` passes (10s window tick ⇒ 3s cadence). */
export function pollDb<T = Row>(
  label: string,
  sql: string,
  params: unknown[],
  check: (row: T | undefined) => boolean,
  attempts = 40,
) {
  const read = (attempt: number): void => {
    db<T>(sql, params).then(({ rows }) => {
      if (check(rows[0])) return;
      expect(attempt, label).to.be.lessThan(attempts);
      cy.wait(3000, { log: false }).then(() => read(attempt + 1));
    });
  };
  read(0);
}

// ---------------------------------------------------------------------------
// time — departures pinned to 12:00 EAT so the EAT day key is unambiguous
// ---------------------------------------------------------------------------

export function departureAt(dayOffset: number): Date {
  const eatNow = new Date(Date.now() + 3 * 3_600_000);
  return new Date(
    Date.UTC(
      eatNow.getUTCFullYear(),
      eatNow.getUTCMonth(),
      eatNow.getUTCDate() + dayOffset,
      9, // 09:00 UTC = 12:00 EAT
      0,
      0,
    ),
  );
}

/** The EAT calendar day (`YYYY-MM-DD`) of an instant — the booking day key. */
export const eatDayStr = (d: Date) =>
  new Date(d.getTime() + 3 * 3_600_000).toISOString().slice(0, 10);

// ---------------------------------------------------------------------------
// contracts — seeded FULLY_EXECUTED (see file header)
// ---------------------------------------------------------------------------

export interface SeedContractOpts {
  suffix: string;
  reference: string;
  currency?: "ETB" | "USD";
  customs?: boolean;
  direction?: "IMPORT" | "EXPORT" | "DOMESTIC";
  freight?: "CONTAINER" | "BULK";
  originCode?: string;
  destCode?: string;
  /** GENERAL = multi-booking drawdown contract (status CONTRACT_ACTIVE). */
  kind?: "ONE_TIME" | "GENERAL";
  /** GENERAL only: quantity cap on the 20ft scope row (40ft stays uncapped). */
  cap20?: number;
}

export function seedImportContract(opts: SeedContractOpts) {
  const currency = opts.currency ?? "ETB";
  const customs = opts.customs ?? false;
  const direction = opts.direction ?? "IMPORT";
  const freight = opts.freight ?? "CONTAINER";
  const kind = opts.kind ?? "ONE_TIME";
  const status = kind === "GENERAL" ? "CONTRACT_ACTIVE" : "FULLY_EXECUTED";
  // Pre-booking boundary milestone for Path B contracts differs by direction.
  const boundary = direction === "EXPORT" ? "EXPORT_RELEASED" : "DO_COLLECTED";
  db(
    `WITH c AS (
       INSERT INTO freight.contracts
         (reference, company_id, company_profile_id, contract_kind,
          trade_direction, freight_type, service_type_id, payment_currency,
          customs_clearing_enabled, clearance_status, status,
          fully_executed_at, contract_valid_from, contract_valid_until,
          contract_summary)
       SELECT $1, comp.id,
              (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::text = 'EXPORT' AND p.type = 'exporter' THEN 0
                  WHEN $2::text <> 'EXPORT' AND p.type = 'importer' THEN 0
                  ELSE 1
                END
                LIMIT 1),
              $11::text, $2::text, $9::text,
              (SELECT st.id FROM freight.service_types st ORDER BY st.created_at LIMIT 1),
              $3, $4,
              CASE WHEN $4 THEN 'CLEARANCE_READY_FOR_BOOKING' ELSE 'NOT_APPLICABLE' END,
              $12::text, now(), now() - interval '1 day',
              now() + interval '60 days', 'E2E import-corridor fixture contract'
       FROM freight.companies comp
       WHERE comp.tin = $5
         -- before() re-runs on cross-origin reloads: keep one stable fresh row
         -- per suffix (skip when this run already seeded an unbooked one).
         AND NOT EXISTS (
           SELECT 1 FROM freight.contracts c2
           WHERE c2.reference LIKE 'CTR-IMP-%-' || $8
             AND c2.deleted_at IS NULL
             AND c2.created_at > now() - interval '30 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 = $6
       JOIN freight.yards d ON d.code = $7
       RETURNING id
     ), scope_container AS (
       INSERT INTO freight.contract_cargo_scope
         (contract_id, container_size, quantity_cap, cargo_free_text)
       SELECT c.id, v.size,
              CASE WHEN v.size = '20ft' THEN $13::numeric END,
              'E2E import corridor cargo'
       FROM c CROSS JOIN (VALUES ('20ft'), ('40ft')) AS v(size)
       WHERE $9::text = 'CONTAINER'
     ), scope_bulk AS (
       INSERT INTO freight.contract_cargo_scope
         (contract_id, cargo_type_id, cargo_free_text)
       SELECT c.id, ct.id, 'E2E import wheat'
       FROM c JOIN freight.cargo_types ct ON ct.code = 'E2E_IMP_WHEAT'
       WHERE $9::text = 'BULK'
     )
     -- Path B gate: ONE_TIME customs bookings require the pre-booking boundary
     -- milestone (IMPORT → DO_COLLECTED, EXPORT → EXPORT_RELEASED) COMPLETED.
     INSERT INTO freight.clearance_milestones
       (contract_id, milestone_code, milestone_label, status, triggered_at, sort_order)
     SELECT c.id, $10, 'Pre-booking clearance boundary', 'COMPLETED', now(), 0
     FROM c WHERE $4`,
    [
      opts.reference,
      direction,
      currency,
      customs,
      companyTin,
      opts.originCode ?? ORIGIN,
      opts.destCode ?? DEST,
      opts.suffix,
      freight,
      boundary,
      kind,
      status,
      opts.cap20 ?? null,
    ],
  );
  // Backfill the boundary milestone when the insert above was skipped because
  // a prior run's still-unbooked contract row is being reused.
  if (customs) {
    db(
      `INSERT INTO freight.clearance_milestones
         (contract_id, milestone_code, milestone_label, status, triggered_at, sort_order)
       SELECT ct.id, $2::text, 'Pre-booking clearance boundary', 'COMPLETED', now(), 0
       FROM freight.contracts ct
       WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND ct.deleted_at IS NULL
         AND NOT EXISTS (
           SELECT 1 FROM freight.clearance_milestones m
           WHERE m.contract_id = ct.id AND m.milestone_code = $2::text
             AND m.deleted_at IS NULL
         )`,
      [opts.suffix, boundary],
    );
  }
}

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

// ---------------------------------------------------------------------------
// bookings
// ---------------------------------------------------------------------------

export interface BookingRow {
  id: string;
  reference: string;
  status: string;
  scheduling_status: string;
  train_schedule_id: string | null;
  payment_deadline: string | null;
  priority_score: number;
  is_split: boolean;
  contract_id: string;
}

export function dbBooking(suffix: string) {
  return db<BookingRow>(
    `SELECT b.id, b.reference, b.status, b.scheduling_status,
            b.train_schedule_id, b.payment_deadline, b.priority_score,
            b.is_split, b.contract_id
     FROM freight.bookings b
     JOIN freight.contracts ct ON ct.id = b.contract_id
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND b.deleted_at IS NULL
     ORDER BY b.created_at DESC LIMIT 1`,
    [suffix],
  );
}

export function withBooking(suffix: string, fn: (b: BookingRow) => void) {
  dbBooking(suffix).then(({ rows }) => {
    expect(rows, `booking under *-${suffix}`).to.have.length(1);
    fn(rows[0]);
  });
}

export function expectBookingStatus(suffix: string, status: string | string[]) {
  const want = Array.isArray(status) ? status : [status];
  withBooking(suffix, (b) =>
    expect(b.status, `${suffix} booking status`).to.be.oneOf(want),
  );
}

export function pollBookingStatus(suffix: string, status: string | string[], attempts = 40) {
  const want = Array.isArray(status) ? status : [status];
  pollDb<BookingRow>(
    `${suffix} → ${want.join("|")}`,
    `SELECT b.status FROM freight.bookings b
     JOIN freight.contracts ct ON ct.id = b.contract_id
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND b.deleted_at IS NULL
     ORDER BY b.created_at DESC LIMIT 1`,
    [suffix],
    (row) => !!row && want.includes(row.status as string),
    attempts,
  );
}

/** ISO 6346-shaped container number, unique per run+seed (checksum unchecked). */
export function isoNumber(runStamp: string, seed: number): string {
  return `MSCU${String((Number(runStamp.slice(-6)) * 100 + seed) % 10_000_000).padStart(7, "0")}`;
}

/**
 * Customer books containers under a seeded contract via the API (the portal
 * booking form is exercised by export_one_time/intercity specs; a 22-wagon
 * booking means 44 ISO inputs — not a UI journey).
 */
export function bookContainers(opts: {
  suffix: string;
  runStamp: string;
  isoSeed: number;
  twenty?: number;
  forty?: number;
  scheduledDate?: string; // omit for DOMESTIC (intercity)
  vgmTons?: number;
  expectFailure?: string | RegExp; // substring/regex of the expected 4xx error
}) {
  const vgm = opts.vgmTons ?? 10;
  const lines: Array<Record<string, unknown>> = [];
  let unit = 0;
  if (opts.twenty) {
    lines.push({
      containerSize: "20ft",
      quantity: opts.twenty,
      units: Array.from({ length: opts.twenty }, () => ({
        containerNumber: isoNumber(opts.runStamp, opts.isoSeed + unit++),
        vgmTons: vgm,
      })),
    });
  }
  if (opts.forty) {
    lines.push({
      containerSize: "40ft",
      quantity: opts.forty,
      units: Array.from({ length: opts.forty }, () => ({
        containerNumber: isoNumber(opts.runStamp, opts.isoSeed + unit++),
        vgmTons: vgm,
      })),
    });
  }
  db<{ id: string; customs_clearing_enabled: boolean }>(
    `SELECT id, customs_clearing_enabled FROM freight.contracts
     WHERE reference LIKE 'CTR-IMP-%-' || $1
     ORDER BY created_at DESC LIMIT 1`,
    [opts.suffix],
  ).then(({ rows }) => {
    expect(rows, `seeded contract *-${opts.suffix}`).to.have.length(1);
    // Path B: customs-clearance contracts are booked by Global Logistics on
    // behalf of the customer — the portal user is rejected with a 403.
    const actor = rows[0].customs_clearing_enabled ? superAdmin : customer;
    apiPost(
      actor,
      `/api/contracts/${rows[0].id}/bookings`,
      {
        ...(opts.scheduledDate ? { scheduledDate: opts.scheduledDate } : {}),
        containers: lines,
      },
      !opts.expectFailure,
    ).then((res) => {
      if (opts.expectFailure) {
        expect(res.status, `${opts.suffix} booking rejected`).to.be.within(400, 422);
        if (opts.expectFailure instanceof RegExp) {
          expect(JSON.stringify(res.body)).to.match(opts.expectFailure);
        } else {
          expect(JSON.stringify(res.body)).to.include(opts.expectFailure);
        }
      } else {
        expect(res.status, `${opts.suffix} booking created`).to.be.oneOf([200, 201]);
      }
    });
  });
}

/**
 * Book bulk tons under a seeded BULK contract via the API. Wagon demand =
 * ceil(tons / 70) on CW4 covered gondolas.
 */
export function bookBulk(opts: {
  suffix: string;
  tons: number;
  scheduledDate?: string; // omit for DOMESTIC (intercity)
  /** Cargo code — WHEAT rides CW4, GRAINS rides PW2. Defaults to wheat. */
  cargoCode?: "E2E_IMP_WHEAT" | "E2E_IMP_GRAINS";
  expectFailure?: string | RegExp;
}) {
  db<{ id: string; customs_clearing_enabled: boolean; cargo_type_id: string }>(
    `SELECT ct.id, ct.customs_clearing_enabled,
            (SELECT t.id FROM freight.cargo_types t WHERE t.code = $2) AS cargo_type_id
     FROM freight.contracts ct
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1
     ORDER BY ct.created_at DESC LIMIT 1`,
    [opts.suffix, opts.cargoCode ?? "E2E_IMP_WHEAT"],
  ).then(({ rows }) => {
    expect(rows, `seeded contract *-${opts.suffix}`).to.have.length(1);
    const actor = rows[0].customs_clearing_enabled ? superAdmin : customer;
    apiPost(
      actor,
      `/api/contracts/${rows[0].id}/bookings`,
      {
        ...(opts.scheduledDate ? { scheduledDate: opts.scheduledDate } : {}),
        bulkLines: [{ cargoTypeId: rows[0].cargo_type_id, cargoWeightTons: opts.tons }],
        cargoFreeText: "E2E import wheat",
      },
      !opts.expectFailure,
    ).then((res) => {
      if (opts.expectFailure) {
        expect(res.status, `${opts.suffix} booking rejected`).to.be.within(400, 422);
        if (opts.expectFailure instanceof RegExp) {
          expect(JSON.stringify(res.body)).to.match(opts.expectFailure);
        } else {
          expect(JSON.stringify(res.body)).to.include(opts.expectFailure);
        }
      } else {
        expect(res.status, `${opts.suffix} booking created`).to.be.oneOf([200, 201]);
      }
    });
  });
}

/**
 * Walk a GENERAL booking through its PER-BOOKING clearance chain (Path A):
 * AWAITING_DOCUMENTS → upload one doc → GL approves it → finalize →
 * CLEARANCE_READY → customer proceeds with the shipment day →
 * OPERATION_REQUEST_PENDING → ops accept → FULLY_EXECUTED (pool). The e2e
 * seed configures no required documents, so one ad-hoc doc satisfies the
 * 100%-approved gate.
 */
export function clearGeneralBooking(
  suffix: string,
  scheduledDate: string,
  /** EXPORT ends at acceptExport (FCFS reserves on accept), not the pool. */
  mode: "import" | "export" = "import",
) {
  withBooking(suffix, (b) => {
    expect(b.status, `${suffix} starts in the clearance gate`).to.eq("AWAITING_DOCUMENTS");
    glUpload(`/api/bookings/${b.id}/clearance/documents`, {}, "custom_e2e");
    apiPost(superAdmin, `/api/bookings/${b.id}/clearance/review`, {
      fileKey: "custom_e2e",
      status: "APPROVED",
    })
      .its("status")
      .should("be.oneOf", [200, 201]);
    apiPost(superAdmin, `/api/bookings/${b.id}/clearance/finalize`)
      .its("status")
      .should("be.oneOf", [200, 201]);
    apiPost(customer, `/api/bookings/${b.id}/clearance/proceed`, { scheduledDate })
      .its("status")
      .should("be.oneOf", [200, 201]);
  });
  if (mode === "export") acceptExport(suffix);
  else acceptOperation(suffix);
}

/** Ops accepts the operation request → FULLY_EXECUTED (enters the day pool). */
export function acceptOperation(suffix: string) {
  withBooking(suffix, (b) => {
    apiPost(opsStaff, `/api/bookings/${b.id}/operation/review`, { decision: "ACCEPT" })
      .its("status")
      .should("be.oneOf", [200, 201]);
  });
  pollBookingStatus(suffix, "FULLY_EXECUTED", 10);
}

/** Batch fill reserves by priority DESC — order 1 = first pick. */
export function setPriority(suffix: string, order: number) {
  withBooking(suffix, (b) =>
    db(`UPDATE freight.bookings SET priority_score = $2 WHERE id = $1`, [
      b.id,
      1000 - order,
    ]),
  );
}

/**
 * Customs bookings pay their clearance service fee ON the booking invoice —
 * there is no separate prepaid clearance invoice. Asserts the booking's
 * invoice carries a CUSTOMS_CLEARANCE line.
 */
export function expectClearanceOnBookingInvoice(suffix: string) {
  pollDb<{ n: string }>(
    `${suffix} clearance fee on booking invoice`,
    `SELECT COUNT(*)::text AS n
     FROM freight.invoice_lines l
     JOIN freight.invoices i ON i.id = l.invoice_id
     JOIN freight.bookings b ON b.id::text = i.source_id::text
     JOIN freight.contracts ct ON ct.id = b.contract_id
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND b.deleted_at IS NULL
       AND i.source = 'booking'
       AND l.charge_type LIKE 'CUSTOMS_CLEARANCE%'`,
    [suffix],
    (row) => Number(row?.n ?? 0) > 0,
    10,
  );
}

/** Staff force-pay; polls PAID + SCHEDULED. */
export function markPaid(suffix: string) {
  withBooking(suffix, (b) => {
    apiPost(opsStaff, `/api/train-scheduling/bookings/${b.id}/mark-paid`)
      .its("status")
      .should("be.oneOf", [200, 201]);
  });
  pollDb<BookingRow>(
    `${suffix} PAID+SCHEDULED`,
    `SELECT b.status, b.scheduling_status FROM freight.bookings b
     JOIN freight.contracts ct ON ct.id = b.contract_id
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND b.deleted_at IS NULL
     ORDER BY b.created_at DESC LIMIT 1`,
    [suffix],
    (row) => row?.status === "PAID" && row?.scheduling_status === "SCHEDULED",
    20,
  );
}

/**
 * Settle a reservation through the REAL payment pipeline: seed the gateway
 * intent projection (the payment microservice is absent in e2e), link it to
 * the open invoice, then deliver the `payment.succeeded` event to the public
 * internal endpoint. This drives billing settle → `booking.invoice.paid` →
 * `advanceBookingOnPayment` → `ensurePaidBookingAllocated`, which is the ONLY
 * path that applies a pending split offer (staff mark-paid skips it).
 */
export function settleViaGateway(suffix: string) {
  withBooking(suffix, (b) => {
    db<{ intent_id: string; currency: string; total: string }>(
      `WITH inv AS (
         SELECT id, currency, total_amount FROM freight.invoices
         WHERE source_id = $1 AND deleted_at IS NULL AND paid_at IS NULL
         ORDER BY created_at DESC LIMIT 1
       ), intent AS (
         INSERT INTO freight.payments
           (id, ref_id, type, reference_type, method, currency, amount,
            reason, raw_initiation, merchant_order_id, status)
         SELECT gen_random_uuid(), $1, 'FREIGHT', 'SHIPMENT',
                'telebirr'::freight.payments_method_enum,
                inv.currency::freight.payments_currency_enum, 1,
                'e2e gateway settle', '{}'::jsonb, 'E2E_' || $2,
                'processing'::freight.payments_status_enum
         FROM inv
         RETURNING id
       ), link AS (
         UPDATE freight.invoices SET payment_id = intent.id
         FROM intent WHERE freight.invoices.id = (SELECT id FROM inv)
         RETURNING payment_id
       )
       SELECT intent.id AS intent_id, inv.currency, inv.total_amount AS total
       FROM intent, inv`,
      [b.id, `${suffix}-${b.id.slice(0, 8)}`],
    ).then(({ rows }) => {
      expect(rows, `${suffix} gateway intent`).to.have.length(1);
      cy.request({
        method: "POST",
        url: `${apiUrl()}/api/internal/payments/mark-paid`,
        body: {
          version: 1,
          eventId: crypto.randomUUID(),
          eventType: "payment.succeeded",
          occurredAt: new Date().toISOString(),
          service: "FREIGHT",
          intentId: rows[0].intent_id,
          referenceType: "SHIPMENT",
          referenceId: b.id,
          merchantOrderId: `E2E_${suffix}_${b.id.slice(0, 8)}`,
          provider: "TELEBIRR",
          amountMinor: 1,
          currency: rows[0].currency,
        },
      }).then((res) => {
        expect(res.status, `${suffix} payment event accepted`).to.eq(200);
        // The global response interceptor wraps payloads in { success, data }.
        const raw = res.body as { processed?: boolean; data?: { processed?: boolean } };
        expect(raw.processed ?? raw.data?.processed, "event processed").to.eq(true);
      });
    });
  });
  pollDb<BookingRow>(
    `${suffix} PAID via gateway`,
    `SELECT b.status FROM freight.bookings b
     JOIN freight.contracts ct ON ct.id = b.contract_id
     WHERE ct.reference LIKE 'CTR-IMP-%-' || $1 AND b.deleted_at IS NULL
     ORDER BY b.created_at DESC LIMIT 1`,
    [suffix],
    (row) => row?.status === "PAID",
    20,
  );
}

/** Push a reservation's pay deadline into the past — the 10s tick expires it. */
export function forceReservationExpiry(suffix: string) {
  withBooking(suffix, (b) =>
    db(
      `UPDATE freight.bookings SET payment_deadline = now() - interval '1 second'
       WHERE id = $1`,
      [b.id],
    ),
  );
  pollBookingStatus(suffix, "EXPIRED");
}

/**
 * Type-integrity assert for mixed trains: every wagon slot allocated to the
 * booking is of ONE expected wagon type (containers → NW5, bulk → CW4), and
 * the slot count matches. No wheat on a flat wagon, no box in a gondola.
 */
export function expectWagonType(suffix: string, code: string, wagons: number) {
  withBooking(suffix, (b) =>
    pollDb<{ code: string; n: string }>(
      `${suffix} rides ${wagons}× ${code}`,
      `SELECT wt.code, count(*) AS n
       FROM freight.wagon_booking_allocations wba
       JOIN freight.train_set_wagons tsw ON tsw.id = wba.train_set_wagon_id
       JOIN freight.wagon_types wt ON wt.id = tsw.wagon_type_id
       WHERE wba.booking_id = $1 AND wba.deleted_at IS NULL
       GROUP BY wt.code`,
      [b.id],
      (row) => row?.code === code && Number(row?.n) === wagons,
      20,
    ),
  );
  // A grouped second row would mean mixed wagon types under one booking.
  withBooking(suffix, (b) =>
    db<{ k: string }>(
      `SELECT count(DISTINCT tsw.wagon_type_id) AS k
       FROM freight.wagon_booking_allocations wba
       JOIN freight.train_set_wagons tsw ON tsw.id = wba.train_set_wagon_id
       WHERE wba.booking_id = $1 AND wba.deleted_at IS NULL`,
      [b.id],
    ).then(({ rows }) =>
      expect(Number(rows[0].k), `${suffix} single wagon type`).to.eq(1),
    ),
  );
}

export function pollAllocations(suffix: string, minWagons = 1) {
  withBooking(suffix, (b) =>
    pollDb<{ n: string }>(
      `${suffix} wagon allocations`,
      `SELECT count(*) AS n FROM freight.wagon_booking_allocations
       WHERE booking_id = $1 AND deleted_at IS NULL`,
      [b.id],
      (row) => Number(row?.n ?? 0) >= minWagons,
      30,
    ),
  );
}

// ---------------------------------------------------------------------------
// route + schedule
// ---------------------------------------------------------------------------

export function dbRouteId(originCode = ORIGIN, destCode = DEST) {
  return db<{ id: string }>(
    `SELECT r.id FROM freight.routes r
     JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = $1
     JOIN freight.yards d ON d.id = r.destination_yard_id AND d.code = $2
     WHERE r.deleted_at IS NULL
     ORDER BY r.created_at DESC LIMIT 1`,
    [originCode, destCode],
  );
}

/** Create the 6-stop corridor route through the API if it doesn't exist yet. */
export function ensureCorridorRoute() {
  dbRouteId().then(({ rows }) => {
    if (rows.length > 0) return;
    db<{ id: string; code: string }>(
      `SELECT id, code FROM freight.yards WHERE code = ANY($1::text[])`,
      [[...CORRIDOR]],
    ).then(({ rows: yards }) => {
      expect(yards, "corridor yards").to.have.length(CORRIDOR.length);
      const byCode = new Map(yards.map((y) => [y.code, y.id]));
      apiPost(opsStaff, "/api/routes", {
        milestones: CORRIDOR.map((code) => ({ yardId: byCode.get(code) })),
      })
        .its("status")
        .should("be.oneOf", [200, 201]);
    });
    // Direction is frozen from the endpoint countries: DJ → ET = IMPORT.
    db<{ direction: string }>(
      `SELECT r.direction FROM freight.routes r
       JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = $1
       JOIN freight.yards d ON d.id = r.destination_yard_id AND d.code = $2
       WHERE r.deleted_at IS NULL ORDER BY r.created_at DESC LIMIT 1`,
      [ORIGIN, DEST],
    ).then(({ rows: created }) => {
      expect(created[0]?.direction, "corridor direction").to.eq("IMPORT");
    });
  });
}

/**
 * Make a corridor departure-day re-runnable: soft-delete any schedule a prior
 * run left on that day and expire its leftover fixture bookings (reference
 * scope CTR-IMP-% only — never touches other suites' data). A wiped schedule
 * must never leave bookings pointing at it (ghost refs break assign).
 */
export function resetCorridorDay(departure: Date, destCode = DEST, originCode = ORIGIN) {
  db(
    `WITH stale AS (
       SELECT ts.id FROM freight.train_schedules ts
       JOIN freight.yards o ON o.id = ts.origin_station_id AND o.code = $1
       JOIN freight.yards d ON d.id = ts.destination_station_id AND d.code = $2
       WHERE ts.deleted_at IS NULL
         AND abs(extract(epoch FROM (ts.scheduled_departure_date - $3::timestamptz))) < 43200
     ), unlink AS (
       UPDATE freight.bookings b
       SET train_schedule_id = NULL,
           status = CASE WHEN b.status IN ('FULLY_EXECUTED','SELECTED_FOR_BATCH','AWAITING_PAYMENT')
                         THEN 'EXPIRED' ELSE b.status END,
           scheduling_status = 'NOT_SCHEDULED'
       FROM freight.contracts ct
       WHERE ct.id = b.contract_id AND ct.reference LIKE 'CTR-IMP-%'
         AND 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)`,
    [originCode, destCode, departure.toISOString()],
  );
  // Prior-run pool leftovers (never reserved, so no schedule ref) would
  // contaminate this run's batch — a stale high-priority booking steals the
  // top-up slot from this run's waiting list. Reset runs before this run
  // books anything, so every unpinned fixture booking is debris: expire all.
  db(
    `UPDATE freight.bookings b
     SET status = 'EXPIRED'
     FROM freight.contracts ct
     WHERE ct.id = b.contract_id AND ct.reference LIKE 'CTR-IMP-%'
       AND b.status = 'FULLY_EXECUTED' AND b.train_schedule_id IS NULL`,
    [],
  );
}

/** Create the reversed 6-stop corridor (ET → DJ = EXPORT) if missing. */
export function ensureExportRoute() {
  dbRouteId(EXP_ORIGIN, EXP_DEST).then(({ rows }) => {
    if (rows.length > 0) return;
    const stops = [...CORRIDOR].reverse();
    db<{ id: string; code: string }>(
      `SELECT id, code FROM freight.yards WHERE code = ANY($1::text[])`,
      [stops],
    ).then(({ rows: yards }) => {
      expect(yards, "corridor yards").to.have.length(stops.length);
      const byCode = new Map(yards.map((y) => [y.code, y.id]));
      apiPost(opsStaff, "/api/routes", {
        milestones: stops.map((code) => ({ yardId: byCode.get(code) })),
      })
        .its("status")
        .should("be.oneOf", [200, 201]);
    });
    db<{ direction: string }>(
      `SELECT r.direction FROM freight.routes r
       JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = $1
       JOIN freight.yards d ON d.id = r.destination_yard_id AND d.code = $2
       WHERE r.deleted_at IS NULL ORDER BY r.created_at DESC LIMIT 1`,
      [EXP_ORIGIN, EXP_DEST],
    ).then(({ rows: created }) => {
      expect(created[0]?.direction, "export corridor direction").to.eq("EXPORT");
    });
  });
}

/**
 * Ops accepts an EXPORT operation request — FCFS: the accept itself reserves
 * the train slot and opens a pay window clamped to the window close.
 */
export function acceptExport(suffix: string) {
  withBooking(suffix, (b) => {
    apiPost(opsStaff, `/api/bookings/${b.id}/operation/review`, { decision: "ACCEPT" })
      .its("status")
      .should("be.oneOf", [200, 201]);
  });
  pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"], 10);
}

export interface ScheduleRow {
  id: string;
  status: string;
  window_phase: string;
  booking_window_status: string;
  booking_cycle_no: number;
  max_wagons: number;
  window_opens_at: string | null;
  window_closes_at: string | null;
  payment_phase_ends_at: string | null;
  scheduled_departure_date: string;
}

const SCHEDULE_COLS = `ts.id, ts.status, ts.window_phase, ts.booking_window_status,
       ts.booking_cycle_no, ts.max_wagons, ts.window_opens_at, ts.window_closes_at,
       ts.payment_phase_ends_at, ts.scheduled_departure_date`;

/** The corridor schedule departing within ±1h of `departure` (12:00 EAT pin). */
export function dbSchedule(departure: Date, destCode = DEST, originCode = ORIGIN) {
  return db<ScheduleRow>(
    `SELECT ${SCHEDULE_COLS}
     FROM freight.train_schedules ts
     JOIN freight.yards o ON o.id = ts.origin_station_id AND o.code = $1
     JOIN freight.yards d ON d.id = ts.destination_station_id AND d.code = $2
     WHERE ts.deleted_at IS NULL
       AND abs(extract(epoch FROM (ts.scheduled_departure_date - $3::timestamptz))) < 3600
     ORDER BY ts.created_at DESC LIMIT 1`,
    [originCode, destCode, departure.toISOString()],
  );
}

export function withSchedule(departure: Date, fn: (s: ScheduleRow) => void) {
  dbSchedule(departure).then(({ rows }) => {
    expect(rows, `schedule departing ${departure.toISOString()}`).to.have.length(1);
    fn(rows[0]);
  });
}

/** Export-corridor (KALITY → DJIB_PORT) variant of withSchedule. */
export function withExportSchedule(departure: Date, fn: (s: ScheduleRow) => void) {
  dbSchedule(departure, EXP_DEST, EXP_ORIGIN).then(({ rows }) => {
    expect(rows, `export schedule departing ${departure.toISOString()}`).to.have.length(1);
    fn(rows[0]);
  });
}

/**
 * Ops creates an import schedule on the corridor via the API — loco-pair mode
 * (no built train): capacity comes from maxWagonsPerTrain (54, the corridor
 * standard) and wagon stock is drawn from the origin yard at allocation time.
 */
export function createImportSchedule(opts: {
  departure: Date;
  /** Loco-pair mode — capacity comes from maxWagonsPerTrain. */
  locoPair?: [string, string];
  /**
   * Built-train mode — the Train-Builder consist IS the capacity (its coupled
   * wagon count), immune to the loco-length slot recompute.
   */
  trainCode?: string;
  maxWagons?: number;
  kind?: "container" | "bulk";
  originCode?: string;
  destCode?: string;
}) {
  const originCode = opts.originCode ?? ORIGIN;
  const destCode = opts.destCode ?? DEST;
  const endpoint = `/api/train-scheduling/${opts.kind ?? "container"}/schedules`;
  dbSchedule(opts.departure, destCode, originCode).then(({ rows }) => {
    if (rows.length > 0) return;
    dbRouteId(originCode, destCode).then(({ rows: routes }) => {
      expect(routes, "corridor route").to.have.length(1);
      if (opts.trainCode) {
        db<{ id: string }>(`SELECT id FROM freight.trains WHERE code = $1`, [
          opts.trainCode,
        ]).then(({ rows: trains }) => {
          expect(trains, `built train ${opts.trainCode}`).to.have.length(1);
          apiPost(opsStaff, endpoint, {
            routeId: routes[0].id,
            scheduleDate: opts.departure.toISOString(),
            trainId: trains[0].id,
          })
            .its("status")
            .should("be.oneOf", [200, 201]);
        });
        return;
      }
      db<{ id: string }>(
        `SELECT id FROM freight.locomotives WHERE code = ANY($1::text[]) ORDER BY code`,
        [opts.locoPair ?? []],
      ).then(({ rows: locos }) => {
        expect(locos, `locomotives ${(opts.locoPair ?? []).join(",")}`).to.have.length(2);
        apiPost(opsStaff, endpoint, {
          routeId: routes[0].id,
          scheduleDate: opts.departure.toISOString(),
          locomotiveIds: locos.map((l) => l.id),
          maxWagonsPerTrain: opts.maxWagons ?? 54,
        })
          .its("status")
          .should("be.oneOf", [200, 201]);
      });
    });
  });
  dbSchedule(opts.departure, destCode, originCode).then(({ rows }) => {
    expect(rows, "created schedule").to.have.length(1);
    expect(rows[0].max_wagons, "consist size").to.eq(opts.maxWagons ?? 54);
  });
}

// ---------------------------------------------------------------------------
// day ledger — who booked, who rides, who expired, who was refused
// ---------------------------------------------------------------------------

export interface LedgerRow {
  suffix: string;
  reference: string;
  kind: string;
  freight: string;
  currency: string;
  status: string;
  wagons: number;
  wagon_type: string | null;
  train: string | null;
  fate: string;
}

/**
 * Classify every booking made under this run's stamped contracts into its
 * final fate. Bookings the engine REFUSED at create never exist as rows —
 * the caller passes those suffixes in (they are only observable as 4xx at
 * request time), plus any suffix that was refused and later rebooked.
 */
export function dayLedger(
  runStamp: string,
  opts: { rejected?: string[]; redeemed?: string[] } = {},
) {
  return db<LedgerRow>(
    `SELECT split_part(ct.reference, '-', 4) AS suffix,
            b.reference,
            ct.contract_kind AS kind,
            b.freight_type   AS freight,
            b.payment_currency AS currency,
            b.status,
            COALESCE(a.wagons, 0)::int AS wagons,
            a.wagon_type,
            ts.reference AS train,
            CASE
              WHEN b.status IN ('PAID','IN_TRANSIT','ARRIVED','COMPLETED') THEN 'PAID_RIDING'
              WHEN b.status = 'EXPIRED' THEN 'EXPIRED_UNPAID'
              WHEN b.status IN ('SELECTED_FOR_BATCH','AWAITING_PAYMENT') THEN 'RESERVED_UNPAID'
              ELSE 'PENDING_' || b.status
            END AS fate
     FROM freight.bookings b
     JOIN freight.contracts ct ON ct.id = b.contract_id
     LEFT JOIN freight.train_schedules ts ON ts.id = b.train_schedule_id
     LEFT JOIN LATERAL (
       SELECT count(*)::int AS wagons, max(wt.code) AS wagon_type
       FROM freight.wagon_booking_allocations wba
       JOIN freight.train_set_wagons tsw ON tsw.id = wba.train_set_wagon_id
       JOIN freight.wagon_types wt ON wt.id = tsw.wagon_type_id
       WHERE wba.booking_id = b.id AND wba.deleted_at IS NULL
     ) a ON true
     WHERE ct.reference LIKE 'CTR-IMP-' || $1 || '-%' AND b.deleted_at IS NULL
     ORDER BY b.created_at`,
    [runStamp],
  ).then(({ rows }) => {
    const rejected = (opts.rejected ?? []).map((suffix) => ({
      suffix,
      reference: "—",
      kind: "—",
      freight: "—",
      currency: "—",
      status: "NOT_CREATED",
      wagons: 0,
      wagon_type: null,
      train: null,
      fate: "REJECTED_NO_SPACE",
    })) as LedgerRow[];
    // A refused customer who later rebooked shows BOTH lines: the refusal
    // above and the live booking here, re-labelled.
    const ledger = [
      ...rows.map((r) =>
        (opts.redeemed ?? []).includes(r.suffix) && r.fate === "PAID_RIDING"
          ? { ...r, fate: "REBOOKED_PAID" }
          : r,
      ),
      ...rejected,
    ];
    return cy.wrap(ledger, { log: false });
  });
}

/** Print the ledger as a table into the Cypress log and write a JSON artifact. */
export function writeLedgerReport(name: string, ledger: LedgerRow[]) {
  const counts = ledger.reduce<Record<string, number>>((acc, r) => {
    acc[r.fate] = (acc[r.fate] ?? 0) + 1;
    return acc;
  }, {});
  cy.log(`**LEDGER ${name}** — ${JSON.stringify(counts)}`);
  ledger.forEach((r) =>
    cy.log(
      `${r.suffix.padEnd(8)} ${r.kind.padEnd(9)} ${r.freight.padEnd(9)} ` +
        `${r.currency.padEnd(4)} ${String(r.wagons).padStart(2)}w ` +
        `${(r.wagon_type ?? "-").padEnd(4)} ${(r.train ?? "-").padEnd(14)} ${r.fate}`,
    ),
  );
  cy.writeFile(`cypress/reports/${name}.json`, {
    generatedAt: new Date().toISOString(),
    counts,
    bookings: ledger,
  });
}

// ---------------------------------------------------------------------------
// window choreography — arrange timestamps, let the engine do the transition
// ---------------------------------------------------------------------------

function pollSchedulePhase(
  scheduleId: string,
  want: string[],
  label: string,
  attempts = 40,
) {
  pollDb<ScheduleRow>(
    label,
    `SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
    [scheduleId],
    (row) => !!row && want.includes(row.window_phase as unknown as string),
    attempts,
  );
}

/** Pull the window-open moment into the past; the tick flips PRE_WINDOW→OPEN. */
export function forceWindowOpen(scheduleId: string, closesInMinutes = 45) {
  db(
    `UPDATE freight.train_schedules
     SET window_opens_at = now() - interval '1 minute',
         window_closes_at = now() + ($2 || ' minutes')::interval
     WHERE id = $1`,
    [scheduleId, String(closesInMinutes)],
  );
  pollSchedulePhase(scheduleId, ["OPEN"], `schedule ${scheduleId} window OPEN`);
  pollDb<ScheduleRow>(
    `schedule ${scheduleId} bookable`,
    `SELECT booking_window_status FROM freight.train_schedules WHERE id = $1`,
    [scheduleId],
    (row) => row?.booking_window_status === "OPEN",
  );
}

/** Pull the close moment into the past; the tick flips OPEN→DOC_REVIEW. */
export function closeBookingWindow(scheduleId: string) {
  db(
    `UPDATE freight.train_schedules
     SET window_closes_at = now() - interval '1 second'
     WHERE id = $1 AND window_phase = 'OPEN'`,
    [scheduleId],
  );
  pollSchedulePhase(scheduleId, ["DOC_REVIEW"], `schedule ${scheduleId} DOC_REVIEW`);
}

/**
 * Staff end document review early → PAYMENT: expires never-accepted bookings,
 * runs the priority batch over the route-day pool, reserves + issues invoices.
 * (Lands on DONE instead when the batch reserved nobody.)
 */
export function completeDocReview(scheduleId: string) {
  apiPost(opsStaff, `/api/train-scheduling/schedules/${scheduleId}/doc-review-complete`)
    .its("status")
    .should("be.oneOf", [200, 201]);
  pollSchedulePhase(
    scheduleId,
    ["PAYMENT", "DONE", "PRE_WINDOW"],
    `schedule ${scheduleId} payment phase`,
  );
}

/** End the payment phase now — the tick settles (allocate paid / expire unpaid). */
export function endPaymentPhase(scheduleId: string) {
  db(
    `UPDATE freight.train_schedules
     SET payment_phase_ends_at = now() - interval '1 second'
     WHERE id = $1 AND window_phase = 'PAYMENT'`,
    [scheduleId],
  );
}

// ---------------------------------------------------------------------------
// train journey + clearance
// ---------------------------------------------------------------------------

export function recordCheckpoint(scheduleId: string, sequenceNo: number, kind: string) {
  apiPost(opsStaff, `/api/train-scheduling/schedules/${scheduleId}/checkpoints`, {
    sequenceNo,
    kind,
  })
    .its("status")
    .should("be.oneOf", [200, 201]);
}

/** GL multipart upload via the Node-side task (cy.request can't send files). */
export function glUpload(
  path: string,
  fields: Record<string, string> = {},
  fileField = "files",
) {
  return tokenFor(superAdmin).then((token) =>
    cy
      .task<{ status: number; body: unknown }>("api:upload", {
        url: `${apiUrl()}${path}`,
        token,
        fields,
        files: [{ field: fileField, fixture: "docs/license.pdf", filename: "e2e-doc.pdf" }],
      })
      .then((res) => {
        expect(res.status, `upload ${path}`).to.be.within(200, 201);
        return cy.wrap(res.body, { log: false });
      }),
  );
}

export function completeBookingMilestone(suffix: string, code: string) {
  withBooking(suffix, (b) => {
    apiPost(superAdmin, `/api/contracts/bookings/${b.id}/milestones/${code}/complete`, {
      note: "e2e",
    })
      .its("status")
      .should("be.oneOf", [200, 201]);
  });
}

export function expectMilestoneDone(suffix: string, code: string) {
  withBooking(suffix, (b) =>
    pollDb<{ n: string }>(
      `${suffix} milestone ${code}`,
      `SELECT count(*) AS n FROM freight.clearance_milestones
       WHERE booking_id = $1 AND milestone_code = $2 AND status = 'COMPLETED'
         AND deleted_at IS NULL`,
      [b.id, code],
      (row) => Number(row?.n ?? 0) > 0,
      10,
    ),
  );
}
