/**
 * TRAIN-BUILDER — adjust-consist headroom guard:
 *
 *   `POST /train-scheduling/schedules/:id/adjust-consist` permanently couples
 *   or trims a BUILT train's consist from a live schedule. This spec drives it
 *   against a dedicated small-capacity train (TRN-ADJ-1, see
 *   seed-adjust-consist.sql): 120T-pull locomotives with a 20T overage
 *   tolerance ⇒ a 140T pull cap, starting consist 4 × CW4 (99.2T tare).
 *
 *     – no wagons passed at all → rejected ("nothing to adjust")
 *     – the same wagon in both add and remove → rejected
 *     – removing a wagon that isn't part of this train → rejected
 *     – adding ONE free spare (→ 124.0T) is within the 140T cap → SUCCEEDS,
 *       schedule.max_wagons follows the new consist size
 *     – adding the SECOND spare too (→ 148.8T) breaches the 140T cap →
 *       REJECTED with the exact gross-weight-over-limit message
 *     – removing a free (unallocated) wagon succeeds, consist shrinks back
 *
 * Sequential steps against one schedule — retries off.
 */

import {
  apiPost,
  createImportSchedule,
  db,
  departureAt,
  ensureExportRoute,
  EXP_DEST,
  EXP_ORIGIN,
  resetCorridorDay,
  superAdmin,
} from "./import-utils";

const DEPARTURE = departureAt(9);

interface WagonRow {
  id: string;
  wagon_number: string;
  train_id: string | null;
  status: string;
}

function wagonByNumber(num: string) {
  return db<WagonRow>(
    `SELECT id, wagon_number, train_id, status FROM freight.wagons WHERE wagon_number = $1`,
    [num],
  ).then(({ rows }) => {
    expect(rows, `wagon ${num}`).to.have.length(1);
    return rows[0];
  });
}

interface ScheduleRow {
  id: string;
  max_wagons: number;
}

function adjTrainSchedule() {
  return db<ScheduleRow>(
    `SELECT ts.id, ts.max_wagons
       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 AND t.code = 'TRN-ADJ-1'
      WHERE ts.deleted_at IS NULL
      ORDER BY ts.created_at DESC LIMIT 1`,
  ).then(({ rows }) => {
    expect(rows, "TRN-ADJ-1 schedule").to.have.length(1);
    return rows[0];
  });
}

function adjustConsist(
  scheduleId: string,
  body: { addWagonIds?: string[]; removeWagonIds?: string[] },
  failOnStatusCode = true,
) {
  return apiPost(
    superAdmin,
    `/api/train-scheduling/schedules/${scheduleId}/adjust-consist`,
    body,
    failOnStatusCode,
  );
}

describe("train-builder: adjust-consist headroom guard", { retries: 0 }, () => {
  before(() => {
    cy.task("db:seedFile", "seed-import-corridor.sql");
    cy.task("db:seedFile", "seed-adjust-consist.sql");
    // Every run leaves its TRN-ADJ-1 schedule behind as a live DRAFT, and its
    // slots keep the ADJ wagons pinned. The trim guard counts pins on any
    // non-deleted DRAFT/SCHEDULED/DISPATCHED schedule, so the next run cannot
    // remove WGN-ADJ-C4 (409 "loaded/pinned on a schedule"). Retire the older
    // ones — TRN-ADJ-1 is this spec's dedicated train, nothing else uses it.
    // Once per run: before() re-fires on cross-origin visits and would
    // otherwise delete the schedule this run just created.
    cy.task("db:queryOnce", {
      key: "train_builder_adjust_consist:reset-adj-schedules",
      sql: `UPDATE freight.train_schedules ts
               SET deleted_at = now()
              FROM freight.train_sets se
              JOIN freight.trains t ON t.id = se.train_id
             WHERE se.id = ts.train_set_id
               AND t.code = 'TRN-ADJ-1'
               AND ts.deleted_at IS NULL`,
    });
    // The ADJ wagons are this spec's dedicated fixture, but once a run detaches
    // one it goes back to the loose pool and another spec's schedule can
    // auto-pin it. Those pins outlive the run and then block the trim here, so
    // release any held by a schedule that is not TRN-ADJ-1's.
    cy.task("db:queryOnce", {
      key: "train_builder_adjust_consist:release-foreign-pins",
      sql: `UPDATE freight.train_set_wagons tsw
               SET deleted_at = now()
              FROM freight.wagons w
             WHERE w.id = tsw.physical_wagon_id
               AND w.wagon_number LIKE 'WGN-ADJ-%'
               AND tsw.deleted_at IS NULL
               AND NOT EXISTS (
                 SELECT 1
                   FROM freight.train_sets se
                   JOIN freight.trains t ON t.id = se.train_id
                  WHERE se.id = tsw.train_set_id
                    AND t.code = 'TRN-ADJ-1'
               )`,
    });
  });

  it("operations schedules the dedicated 4-wagon built train (TRN-ADJ-1)", () => {
    ensureExportRoute();
    resetCorridorDay(DEPARTURE, EXP_DEST, EXP_ORIGIN);
    createImportSchedule({
      departure: DEPARTURE,
      trainCode: "TRN-ADJ-1",
      maxWagons: 4,
      kind: "bulk",
      originCode: EXP_ORIGIN,
      destCode: EXP_DEST,
    });
  });

  it("rejects an empty adjustment and a wagon listed in both add and remove", () => {
    adjTrainSchedule().then((s) => {
      adjustConsist(s.id, {}, false).then((res) => {
        expect(res.status, "nothing-to-adjust rejected").to.eq(400);
        expect(JSON.stringify(res.body)).to.include("Nothing to adjust");
      });

      wagonByNumber("WGN-ADJ-C1").then((w) => {
        adjustConsist(s.id, { addWagonIds: [w.id], removeWagonIds: [w.id] }, false).then(
          (res) => {
            expect(res.status, "add+remove same wagon rejected").to.eq(400);
            expect(JSON.stringify(res.body)).to.include(
              "cannot be added and removed in the same adjustment",
            );
          },
        );
      });
    });
  });

  it("rejects removing a wagon that isn't coupled to this train", () => {
    adjTrainSchedule().then((s) => {
      wagonByNumber("WGN-ADJ-S1").then((spare) => {
        adjustConsist(s.id, { removeWagonIds: [spare.id] }, false).then((res) => {
          expect(res.status, "not-part-of-train rejected").to.eq(404);
          expect(JSON.stringify(res.body)).to.include("not coupled to train");
        });
      });
    });
  });

  it("adds one spare within the 140T pull cap (99.2T + 24.8T = 124.0T) — succeeds", () => {
    adjTrainSchedule().then((s) => {
      wagonByNumber("WGN-ADJ-S1").then((spare) => {
        adjustConsist(s.id, { addWagonIds: [spare.id] }).then((res) => {
          expect(res.status, "add within headroom").to.be.oneOf([200, 201]);
        });
      });
    });

    // Consist grew to 5; the coupled spare now belongs to the train.
    adjTrainSchedule().then((s) => expect(s.max_wagons, "max_wagons = 5").to.eq(5));
    wagonByNumber("WGN-ADJ-S1").then((w) => {
      db<{ code: string }>(
        `SELECT t.code FROM freight.trains t WHERE t.id = $1`,
        [w.train_id],
      ).then(({ rows }) => expect(rows[0]?.code, "S1 now on TRN-ADJ-1").to.eq("TRN-ADJ-1"));
    });
  });

  it("adding the second spare breaches the 140T cap (124.0T + 24.8T = 148.8T) — rejected", () => {
    adjTrainSchedule().then((s) => {
      wagonByNumber("WGN-ADJ-S2").then((spare) => {
        adjustConsist(s.id, { addWagonIds: [spare.id] }, false).then((res) => {
          expect(res.status, "add over headroom rejected").to.eq(400);
          const body = JSON.stringify(res.body);
          expect(body).to.include("puts gross weight at 148.8T");
          expect(body).to.include("over the locomotives' 140T limit incl. tolerance");
        });
      });
    });

    // Rejected add must not have moved anything: still 5 wagons, S2 still free.
    adjTrainSchedule().then((s) => expect(s.max_wagons, "still 5").to.eq(5));
    wagonByNumber("WGN-ADJ-S2").then((w) => {
      expect(w.train_id, "S2 still uncoupled").to.be.null;
      expect(w.status, "S2 still AVAILABLE").to.eq("AVAILABLE");
    });
  });

  it("removes a free coupled wagon — consist shrinks and the wagon returns to the yard", () => {
    adjTrainSchedule().then((s) => {
      wagonByNumber("WGN-ADJ-C4").then((w) => {
        adjustConsist(s.id, { removeWagonIds: [w.id] }).then((res) => {
          expect(res.status, "remove free wagon").to.be.oneOf([200, 201]);
        });
      });
    });

    adjTrainSchedule().then((s) => expect(s.max_wagons, "max_wagons = 4").to.eq(4));
    wagonByNumber("WGN-ADJ-C4").then((w) => {
      expect(w.train_id, "C4 detached").to.be.null;
      expect(w.status, "C4 back to AVAILABLE").to.eq("AVAILABLE");
    });
  });
});

export {};
