/**
 * IMPORT critical-scenario matrix — the corridor edge cases that don't need a
 * full journey each. One 54-wagon train departing D+7, plus a same-day sibling:
 *
 *   1. hard gates at booking creation:
 *      – no open window on the requested day → rejected
 *      – duplicate ISO container number inside one booking → rejected
 *   2. route creation refuses a yard pair with no configured distance
 *   3. sub-corridor import booking (NAGAD → MOJO) rides the through-train:
 *      the batch is corridor-aware, the booking reserves only its own leg
 *   4. intercity ride-along (MOJO → KALITY, DOMESTIC): dateless booking, staff
 *      accept onto the import train's free leg, pay window opens, paid + linked
 *   5. same-route same-day sibling schedule JOINS the group window (shared
 *      open/close timeline — no cross-expiry), and staff can move a booking
 *      onto the sibling (move-schedule)
 *
 * Sequential steps — retries off.
 */

import {
  resetCorridorDay,
  acceptOperation,
  apiPost,
  bookContainers,
  closeBookingWindow,
  completeDocReview,
  createImportSchedule,
  db,
  departureAt,
  dbRouteId,
  eatDayStr,
  ensureCorridorRoute,
  forceWindowOpen,
  markPaid,
  opsStaff,
  pollAllocations,
  pollBookingStatus,
  pollDb,
  seedImportContract,
  withBooking,
  withSchedule,
  type ScheduleRow,
} from "./import-utils";

const DEPARTURE = departureAt(9);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const NO_WINDOW_DAY = eatDayStr(departureAt(11)); // no schedule exists there

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

describe("import critical matrix: gates, sub-corridor, intercity, sibling window", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    seedImportContract({ suffix: "MX1", reference: stampedRef("MX1") }); // through-corridor
    seedImportContract({ suffix: "MX2", reference: stampedRef("MX2") }); // gate probes
    seedImportContract({
      suffix: "MXSUB",
      reference: stampedRef("MXSUB"),
      originCode: "NAGAD",
      destCode: "MOJO",
    });
    seedImportContract({
      suffix: "MXIC",
      reference: stampedRef("MXIC"),
      direction: "DOMESTIC",
      originCode: "MOJO",
      destCode: "KALITY",
    });
    seedImportContract({ suffix: "MXMOVE", reference: stampedRef("MXMOVE") });
  });

  it("operations prepares the corridor and the D+7 train with an open window", () => {
    ensureCorridorRoute();
    resetCorridorDay(DEPARTURE);
    createImportSchedule({ departure: DEPARTURE, locoPair: ["LOCO-IMP-11", "LOCO-IMP-12"] });
    withSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
  });

  it("gate: a booking day with no open window is rejected", () => {
    bookContainers({
      suffix: "MX2",
      runStamp: stamp,
      isoSeed: 5000,
      twenty: 2,
      scheduledDate: NO_WINDOW_DAY,
      expectFailure: "booking window",
    });
  });

  it("gate: a duplicate ISO container number inside one booking is rejected", () => {
    // Two units, same number: build the payload by hand via the same API.
    apiPostDuplicate();

    function apiPostDuplicate() {
      const dupe = `MSCU${String(Number(stamp.slice(-6)) + 5100).padStart(7, "0")}`;
      db<{ id: string }>(
        `SELECT id FROM freight.contracts
         WHERE reference LIKE 'CTR-IMP-%-' || $1
         ORDER BY created_at DESC LIMIT 1`,
        ["MX2"],
      ).then(({ rows }) => {
        apiPost(
          "user@gmail.com",
          `/api/contracts/${rows[0].id}/bookings`,
          {
            scheduledDate: BOOKING_DAY,
            containers: [
              {
                containerSize: "20ft",
                quantity: 2,
                units: [
                  { containerNumber: dupe, vgmTons: 10 },
                  { containerNumber: dupe, vgmTons: 10 },
                ],
              },
            ],
          },
          false,
        ).then((res) => {
          expect(res.status, "duplicate ISO rejected").to.be.within(400, 422);
          expect(JSON.stringify(res.body)).to.include("Duplicate container number");
        });
      });
    }
  });

  it("gate: a route over a yard pair with no configured distance is rejected", () => {
    db<{ id: string; code: string }>(
      `SELECT id, code FROM freight.yards WHERE code = ANY($1::text[])`,
      [["E2E_AWASH", "DJIB_PORT"]],
    ).then(({ rows }) => {
      const byCode = new Map(rows.map((y) => [y.code, y.id]));
      // E2E_AWASH ↔ DJIB_PORT has no direct distance row.
      apiPost(
        opsStaff,
        "/api/routes",
        {
          milestones: [
            { yardId: byCode.get("DJIB_PORT") },
            { yardId: byCode.get("E2E_AWASH") },
          ],
        },
        false,
      ).then((res) => {
        expect(res.status, "distance-less route rejected").to.be.within(400, 422);
        expect(JSON.stringify(res.body)).to.include("No distance configured");
      });
    });
  });

  it("a through-corridor booking and a NAGAD→MOJO sub-corridor booking share the train", () => {
    // All windowed bookings (incl. MXMOVE for the later move test) go in while
    // the window is still OPEN — the create gate closes with it.
    bookContainers({
      suffix: "MX1",
      runStamp: stamp,
      isoSeed: 5200,
      twenty: 40, // 20 wagons DJIB_PORT → KALITY
      scheduledDate: BOOKING_DAY,
    });
    acceptOperation("MX1");
    bookContainers({
      suffix: "MXSUB",
      runStamp: stamp,
      isoSeed: 5300,
      twenty: 20, // 10 wagons, NAGAD → MOJO leg only
      scheduledDate: BOOKING_DAY,
    });
    acceptOperation("MXSUB");
    bookContainers({
      suffix: "MXMOVE",
      runStamp: stamp,
      isoSeed: 5500,
      forty: 4, // 4 wagons — later moved onto the sibling train
      scheduledDate: BOOKING_DAY,
    });
    acceptOperation("MXMOVE");

    withSchedule(DEPARTURE, (s) => {
      closeBookingWindow(s.id);
      completeDocReview(s.id);
    });
    ["MX1", "MXSUB", "MXMOVE"].forEach((suffix) =>
      pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
    );
    markPaid("MX1");
    pollAllocations("MX1", 20);
    markPaid("MXSUB");
    pollAllocations("MXSUB", 10);

    // Both ride the same schedule even though MXSUB's endpoints are interior stops.
    withSchedule(DEPARTURE, (s) => {
      withBooking("MX1", (b) => expect(b.train_schedule_id).to.eq(s.id));
      withBooking("MXSUB", (b) => expect(b.train_schedule_id).to.eq(s.id));
    });
  });

  it("intercity ride-along: dateless DOMESTIC booking accepted onto the import train's free leg", () => {
    bookContainers({
      suffix: "MXIC",
      runStamp: stamp,
      isoSeed: 5400,
      twenty: 4, // 2 wagons MOJO → KALITY — plenty of leg capacity left
      // no scheduledDate: intercity bookings are dateless
    });
    acceptOperation("MXIC");

    withSchedule(DEPARTURE, (s) => {
      withBooking("MXIC", (b) => {
        apiPost(opsStaff, `/api/train-scheduling/schedules/${s.id}/intercity/accept`, {
          bookingIds: [b.id],
        })
          .its("status")
          .should("be.oneOf", [200, 201]);
      });
    });
    pollBookingStatus("MXIC", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
    withBooking("MXIC", (b) => {
      expect(b.payment_deadline, "ride-along pay window opened").to.be.a("string");
    });
    markPaid("MXIC");
    withSchedule(DEPARTURE, (s) => {
      withBooking("MXIC", (b) => {
        expect(b.train_schedule_id, "linked to the import train").to.eq(s.id);
        db<{ n: string }>(
          `SELECT count(*) AS n FROM freight.train_schedule_bookings
           WHERE booking_id = $1 AND train_schedule_id = $2 AND deleted_at IS NULL`,
          [b.id, s.id],
        ).then(({ rows }) => expect(Number(rows[0].n), "link row").to.eq(1));
      });
    });
  });

  it("a same-route same-day sibling schedule joins the shared group window", () => {
    createImportSchedule({
      departure: new Date(DEPARTURE.getTime() + 90 * 60_000), // same EAT day, later
      locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"],
    });
    withSchedule(DEPARTURE, (anchor) => {
      pollDb<ScheduleRow>(
        "sibling adopts the group timeline",
        `SELECT ${["ts.id", "ts.window_phase", "ts.window_opens_at"].join(", ")}
         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 ts.id <> $1
           AND abs(extract(epoch FROM (ts.scheduled_departure_date - $2::timestamptz))) < 7200
         ORDER BY ts.created_at DESC LIMIT 1`,
        [anchor.id, DEPARTURE.toISOString()],
        // The anchor is already past OPEN (we closed it) — a mid-cycle joiner
        // mirrors the group's live phase instead of restarting its own clock.
        (row) => !!row && row.window_phase === anchor.window_phase,
      );
    });
  });

  it("staff move a reserved booking onto the sibling train, then it pays there", () => {
    withSchedule(DEPARTURE, (anchor) => {
      db<{ id: string }>(
        `SELECT ts.id FROM freight.train_schedules ts
         WHERE ts.deleted_at IS NULL AND ts.id <> $1
           AND abs(extract(epoch FROM (ts.scheduled_departure_date - $2::timestamptz))) < 7200
         ORDER BY ts.created_at DESC LIMIT 1`,
        [anchor.id, DEPARTURE.toISOString()],
      ).then(({ rows: siblings }) => {
        expect(siblings, "sibling schedule").to.have.length(1);
        // move-schedule only accepts an OPEN target — the sibling joined the
        // group mid-cycle (already past OPEN), so arrange its window state.
        db(
          `UPDATE freight.train_schedules SET booking_window_status = 'OPEN'
           WHERE id = $1`,
          [siblings[0].id],
        );
        withBooking("MXMOVE", (b) => {
          apiPost(opsStaff, `/api/train-scheduling/bookings/${b.id}/move-schedule`, {
            trainScheduleId: siblings[0].id,
          })
            .its("status")
            .should("be.oneOf", [200, 201]);
          pollDb<{ train_schedule_id: string }>(
            "MXMOVE pinned to the sibling",
            `SELECT train_schedule_id FROM freight.bookings WHERE id = $1`,
            [b.id],
            (row) => row?.train_schedule_id === siblings[0].id,
            10,
          );
        });
        markPaid("MXMOVE");
        pollAllocations("MXMOVE", 4);
        withBooking("MXMOVE", (b) => {
          expect(b.train_schedule_id, "paid on the sibling").to.eq(siblings[0].id);
        });
      });
    });
  });
});

export {};
