/**
 * GENERAL two-trains-one-day — 20 GENERAL bookings against TWO 54-wagon
 * trains sharing one route-day (D+24): ONE window timeline, ONE batch
 * release, overflow cascading earliest-departure-first, and a full DUAL
 * lifecycle (both trains dispatch, run the corridor and arrive the same day).
 *
 *   – both schedules share the group window (identical clocks); one
 *     doc-review-complete releases the WHOLE day
 *   – 20 × 6w = 120w demand vs 108: train 1 takes 9 bookings, the overflow
 *     lands on train 2 (9 more), 2 wait
 *   – all 18 pay → 54/54 on EACH train; the 2 waiters expire only after BOTH
 *     trains conclude (the sweep defers while a sibling is open)
 *   – both trains finalize, gate-pass, dispatch, checkpoint and arrive —
 *     every booking ARRIVED under its own train, movements ledger per train
 *
 * Sequential steps of one journey — retries off.
 */

import {
  apiPost,
  bookContainers,
  clearGeneralBooking,
  completeDocReview,
  createImportSchedule,
  db,
  departureAt,
  eatDayStr,
  ensureCorridorRoute,
  forceReservationExpiry,
  markPaid,
  opsStaff,
  pollAllocations,
  pollBookingStatus,
  pollDb,
  resetCorridorDay,
  seedImportContract,
  setPriority,
  withBooking,
  type ScheduleRow,
} from "./import-utils";

const DEPARTURE_1 = departureAt(24);
const DEPARTURE_2 = new Date(DEPARTURE_1.getTime() + 2 * 3_600_000); // same EAT day
const BOOKING_DAY = eatDayStr(DEPARTURE_1);

const stamp = String(Date.now());
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;

const USERS = Array.from({ length: 20 }, (_, i) =>
  `GT${String(i + 1).padStart(2, "0")}`,
);
const RIDERS = USERS.slice(0, 18); // 9 per train
const WAITERS = USERS.slice(18); // 2 expire after both trains conclude

interface DayScheduleRow {
  id: string;
  window_phase: string;
  booking_window_status: string;
  status: string;
  scheduled_departure_date: string;
}

/** Both schedules of the day, earliest departure first. */
function dayShedules() {
  return db<DayScheduleRow>(
    `SELECT ts.id, ts.window_phase, ts.booking_window_status, ts.status,
            ts.scheduled_departure_date
     FROM freight.train_schedules ts
     JOIN freight.yards o ON o.id = ts.origin_station_id AND o.code = 'DJIB_PORT'
     JOIN freight.yards d ON d.id = ts.destination_station_id AND d.code = 'KALITY'
     WHERE ts.deleted_at IS NULL
       AND abs(extract(epoch FROM (ts.scheduled_departure_date - $1::timestamptz))) < 14400
     ORDER BY ts.scheduled_departure_date ASC`,
    [DEPARTURE_1.toISOString()],
  );
}

function withBothSchedules(fn: (first: DayScheduleRow, second: DayScheduleRow) => void) {
  dayShedules().then(({ rows }) => {
    expect(rows, "two schedules on the day").to.have.length(2);
    fn(rows[0], rows[1]);
  });
}

describe("GENERAL two trains, one day: shared window, overflow, dual lifecycle", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    USERS.forEach((suffix) =>
      seedImportContract({ suffix, reference: stampedRef(suffix), kind: "GENERAL" }),
    );
  });

  it("operations schedules TWO 54-wagon trains on one day — one shared window, forced open", () => {
    ensureCorridorRoute();
    resetCorridorDay(DEPARTURE_1);
    createImportSchedule({ departure: DEPARTURE_1, locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"] });
    createImportSchedule({ departure: DEPARTURE_2, locoPair: ["LOCO-IMP-15", "LOCO-IMP-16"] });

    // One clock for the whole day: force IDENTICAL window timestamps on both
    // siblings (the group rule's shared timeline, arranged deterministically).
    withBothSchedules((first, second) => {
      db(
        `UPDATE freight.train_schedules
         SET window_opens_at = now() - interval '1 minute',
             window_closes_at = now() + interval '60 minutes'
         WHERE id = ANY($1::uuid[])`,
        [[first.id, second.id]],
      );
      [first.id, second.id].forEach((id) =>
        pollDb<ScheduleRow>(
          `schedule ${id} OPEN`,
          `SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
          [id],
          (row) => row?.window_phase === "OPEN" && row?.booking_window_status === "OPEN",
        ),
      );
    });
  });

  it("20 users book into the day (never a specific train); every booking clears per booking", () => {
    USERS.forEach((suffix, i) => {
      bookContainers({
        suffix,
        runStamp: stamp,
        isoSeed: 17_000 + i * 15,
        twenty: 12,
        scheduledDate: BOOKING_DAY,
      });
      clearGeneralBooking(suffix, BOOKING_DAY);
    });
    USERS.forEach((suffix, i) => setPriority(suffix, i + 1));
  });

  it("ONE doc-review-complete releases the whole day — 18 reserved across both trains, 2 wait", () => {
    withBothSchedules((first, second) => {
      db(
        `UPDATE freight.train_schedules
         SET window_closes_at = now() - interval '1 second'
         WHERE id = ANY($1::uuid[]) AND window_phase = 'OPEN'`,
        [[first.id, second.id]],
      );
      [first.id, second.id].forEach((id) =>
        pollDb<ScheduleRow>(
          `schedule ${id} DOC_REVIEW`,
          `SELECT window_phase FROM freight.train_schedules WHERE id = $1`,
          [id],
          (row) => row?.window_phase === "DOC_REVIEW",
        ),
      );
      // Staff complete doc review on ONE train — the group stamp releases both.
      completeDocReview(first.id);
      pollDb<ScheduleRow>(
        "sibling released by the same action",
        `SELECT window_phase FROM freight.train_schedules WHERE id = $1`,
        [second.id],
        (row) => row?.window_phase === "PAYMENT" || row?.window_phase === "DONE",
      );
    });
    RIDERS.forEach((suffix) =>
      pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
    );
    WAITERS.forEach((suffix) =>
      withBooking(suffix, (b) => {
        expect(b.status, `${suffix} waiting`).to.eq("FULLY_EXECUTED");
      }),
    );
  });

  it("all 18 pay — 54/54 on EACH train, overflow filled earliest-departure-first", () => {
    RIDERS.forEach((suffix) => {
      markPaid(suffix);
      pollAllocations(suffix, 6);
    });
    withBothSchedules((first, second) => {
      [first.id, second.id].forEach((id) => {
        pollDb<{ n: string }>(
          `train ${id} carries 9 bookings`,
          `SELECT count(*) AS n FROM freight.train_schedule_bookings
           WHERE train_schedule_id = $1 AND deleted_at IS NULL`,
          [id],
          (row) => Number(row?.n) === 9,
          15,
        );
        db<{ n: string }>(
          `SELECT count(DISTINCT wba.train_set_wagon_id) AS n
           FROM freight.wagon_booking_allocations wba
           JOIN freight.train_schedule_bookings tsb
             ON tsb.booking_id = wba.booking_id AND tsb.train_schedule_id = $1
           WHERE wba.deleted_at IS NULL AND tsb.deleted_at IS NULL`,
          [id],
        ).then(({ rows }) => expect(Number(rows[0].n), "54 wagons").to.eq(54));
      });
      // The top-priority bookings ride the EARLIEST departure.
      withBooking("GT01", (b) =>
        expect(b.train_schedule_id, "GT01 on the first train").to.eq(first.id),
      );
    });
  });

  it("both trains conclude FULL — only then do the 2 waiters expire (sweep defers for siblings)", () => {
    withBothSchedules((first, second) => {
      db(
        `UPDATE freight.train_schedules
         SET payment_phase_ends_at = now() - interval '1 second'
         WHERE id = ANY($1::uuid[]) AND window_phase = 'PAYMENT'`,
        [[first.id, second.id]],
      );
      [first.id, second.id].forEach((id) =>
        pollDb<ScheduleRow>(
          `schedule ${id} FULL + DONE`,
          `SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
          [id],
          (row) => row?.booking_window_status === "FULL" && row?.window_phase === "DONE",
        ),
      );
    });
    WAITERS.forEach((suffix) => pollBookingStatus(suffix, "EXPIRED"));
  });

  it("dual lifecycle: both trains gate-pass, dispatch, run the corridor and arrive", () => {
    withBothSchedules((first, second) => {
      [first.id, second.id].forEach((id) => {
        pollDb<ScheduleRow>(
          `schedule ${id} finalized`,
          `SELECT status FROM freight.train_schedules WHERE id = $1`,
          [id],
          (row) => row?.status === "SCHEDULED",
          10,
        );
        apiPost(opsStaff, `/api/train-scheduling/schedules/${id}/import-djibouti/gatepass-granted`)
          .its("status")
          .should("be.oneOf", [200, 201]);
        apiPost(opsStaff, `/api/train-scheduling/schedules/${id}/dispatch`)
          .its("status")
          .should("be.oneOf", [200, 201]);
        [1, 2, 3, 4].forEach((seq) => {
          apiPost(opsStaff, `/api/train-scheduling/schedules/${id}/checkpoints`, {
            sequenceNo: seq,
            kind: "PASSED",
          })
            .its("status")
            .should("be.oneOf", [200, 201]);
        });
        apiPost(opsStaff, `/api/train-scheduling/schedules/${id}/checkpoints`, {
          sequenceNo: 5,
          kind: "ARRIVED",
        })
          .its("status")
          .should("be.oneOf", [200, 201]);
        pollDb<ScheduleRow>(
          `schedule ${id} ARRIVED`,
          `SELECT status FROM freight.train_schedules WHERE id = $1`,
          [id],
          (row) => row?.status === "ARRIVED",
          20,
        );
        db<{ n: string }>(
          `SELECT count(*) AS n FROM freight.wagon_movements WHERE train_schedule_id = $1`,
          [id],
        ).then(({ rows }) =>
          expect(Number(rows[0].n), `train ${id} movements`).to.be.at.least(54),
        );
      });
    });
    RIDERS.forEach((suffix) => pollBookingStatus(suffix, "ARRIVED", 20));
  });
});

export {};
