import { strict as assert } from "node:assert"; import { test } from "node:test"; import { adaptLegacyStationNumber, formatServiceMinute, lookupLegacyStations, parseServiceMinute, parseWallClockTime, physicalStationId, serviceDate, serviceDateFromCalendarDate, serviceMinuteFromWallClock, stationStopId, trainIdentityKey, type StationIdentity, type TrainIdentity, } from "../lib/domain/railway"; const stationFixture = (): StationIdentity[] => [ { physicalStationId: physicalStationId("physical-takamatsu"), stationStopId: stationStopId("stop-yosan-takamatsu"), stationNumber: "Y00", lineId: "yosan", }, { physicalStationId: physicalStationId("physical-takamatsu"), stationStopId: stationStopId("stop-kotoku-takamatsu"), stationNumber: "T28", lineId: "kotoku", }, { physicalStationId: physicalStationId("physical-muya"), stationStopId: stationStopId("stop-tokushima-m12"), stationNumber: "M12", lineId: "tokushima", }, { physicalStationId: physicalStationId("physical-komatsushima"), stationStopId: stationStopId("stop-komatsushima-m12"), stationNumber: "M12", lineId: "komatsushima", }, ]; test("station identity keeps physical station, stop, and number distinct", () => { const stations = stationFixture(); const takamatsu = lookupLegacyStations(stations, { stationNumber: "Y00" }); assert.equal(takamatsu.length, 1); assert.equal(takamatsu[0].physicalStationId, stations[0].physicalStationId); const samePhysicalStation = stations.filter( (station) => station.physicalStationId === stations[0].physicalStationId, ); assert.equal(samePhysicalStation.length, 2); assert.notEqual(samePhysicalStation[0].stationNumber, samePhysicalStation[1].stationNumber); assert.equal(lookupLegacyStations(stations, { stationNumber: "M12" }).length, 2); assert.equal( lookupLegacyStations(stations, { stationNumber: "M12", lineId: "tokushima" }).length, 1, ); }); test("legacy adapter does not invent a physical canonical ID", () => { assert.deepEqual(adaptLegacyStationNumber("Y00", "yosan"), { physicalStationId: null, stationNumber: "Y00", lineId: "yosan", }); }); test("service minute represents extended railway times without Date", () => { assert.equal(formatServiceMinute(parseServiceMinute("23:59")), "23:59"); assert.equal(formatServiceMinute(parseServiceMinute("24:00")), "24:00"); assert.equal(formatServiceMinute(parseServiceMinute("24:30")), "24:30"); assert.equal(formatServiceMinute(parseServiceMinute("25:15")), "25:15"); assert.equal(parseServiceMinute("00:00"), 0); assert.equal(parseServiceMinute("24:30"), 1470); assert.equal(parseServiceMinute("25:15"), 1515); assert.throws(() => parseWallClockTime("24:00"), /between 00:00 and 23:59/); }); test("service day maps after-midnight wall clock times to the previous service date", () => { assert.equal(serviceMinuteFromWallClock("00:30"), 1470); assert.equal(serviceMinuteFromWallClock("03:59"), 1679); assert.equal(serviceMinuteFromWallClock("04:00"), 240); assert.equal(serviceDateFromCalendarDate("2026-08-30", "00:30"), "2026-08-29"); assert.equal(serviceDateFromCalendarDate("2026-08-30", "03:59"), "2026-08-29"); assert.equal(serviceDateFromCalendarDate("2026-08-30", "04:00"), "2026-08-30"); }); test("train identity key includes service date, line, and source", () => { const base: TrainIdentity = { trainNumber: "特急1", serviceDate: serviceDate("2026-08-30"), lineId: "yosan", source: "r2", }; const nextDay = { ...base, serviceDate: serviceDate("2026-08-31") }; const otherLine = { ...base, lineId: "dosan" }; const otherSource = { ...base, source: "gas" }; assert.notEqual(trainIdentityKey(base), trainIdentityKey(nextDay)); assert.notEqual(trainIdentityKey(base), trainIdentityKey(otherLine)); assert.notEqual(trainIdentityKey(base), trainIdentityKey(otherSource)); assert.equal(trainIdentityKey(base), trainIdentityKey({ ...base })); });