import assert from 'node:assert';
import { Readable } from 'node:stream';
import { describe, it } from 'node:test';

import { Route } from '../../timetable/route.js';
import { durationFromSeconds, timeFromHM } from '../../timetable/time.js';
import { Timetable, TransferTypes } from '../../timetable/timetable.js';
import { encode } from '../../timetable/tripStopId.js';
import { GtfsStopsMap } from '../stops.js';
import {
  addGeneratedTransfers,
  addMissingSiblingTransfers,
  buildTripTransfers,
  ForbiddenTransfersMap,
  GtfsTripTransfer,
  parseTransfers,
  TransfersMap,
} from '../transfers.js';
import { TripsMapping } from '../trips.js';

describe('GTFS transfers parser', () => {
  it('should correctly parse valid transfers', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","2","180"\n');
    mockedStream.push('"1100097","8014447","0","240"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedTransfers = new Map([
      [
        0, // Internal ID for stop '1100084'
        [
          {
            destination: 1, // Internal ID for stop '8014440:0:1'
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
            minTransferTime: durationFromSeconds(180),
          },
        ],
      ],
      [
        2, // Internal ID for stop '1100097'
        [
          {
            destination: 3, // Internal ID for stop '8014447'
            type: TransferTypes.RECOMMENDED,
            minTransferTime: durationFromSeconds(240),
          },
        ],
      ],
    ]);

    assert.deepEqual(result.transfers, expectedTransfers);
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should only retain unscoped impossible transfers as forbidden', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,from_route_id,to_route_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","","","","","3","180"\n');
    mockedStream.push('"8014440:0:1","1100084","trip-a","","","","3","180"\n');
    mockedStream.push('"1100097","8014447","","","","route-b","3","240"\n');
    mockedStream.push('"1100097","8014447","","","","","5","240"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.forbiddenTransfers, new Map([[0, new Set([1])]]));
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should ignore transfers with missing stop IDs', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push(',"8014440:0:1","2","180"\n');
    mockedStream.push('"1100097",,"0","240"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should correctly parse in-seat transfers (type 4)', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","trip1","trip2","4","0"\n');
    mockedStream.push('"1100097","8014447","trip3","trip4","4","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedTripContinuations = [
      {
        fromStop: 0,
        fromTrip: 'trip1',
        toStop: 1,
        toTrip: 'trip2',
      },
      {
        fromStop: 2,
        fromTrip: 'trip3',
        toStop: 3,
        toTrip: 'trip4',
      },
    ];

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, expectedTripContinuations);
    assert.deepEqual(result.guaranteedTripTransfers, []);
  });

  it('should correctly parse guaranteed transfers (type 1) with trip IDs', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","tripA","tripB","1","0"\n');
    mockedStream.push('"1100097","8014447","tripC","tripD","1","60"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedGuaranteedTripTransfers = [
      {
        fromStop: 0,
        fromTrip: 'tripA',
        toStop: 1,
        toTrip: 'tripB',
      },
      {
        fromStop: 2,
        fromTrip: 'tripC',
        toStop: 3,
        toTrip: 'tripD',
      },
    ];

    // Guaranteed transfers with trip IDs should go to guaranteedTripTransfers, not transfers
    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
    assert.deepEqual(
      result.guaranteedTripTransfers,
      expectedGuaranteedTripTransfers,
    );
  });

  it('should differentiate guaranteed transfers with and without trip IDs', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    // Type 1 with trip IDs -> goes to guaranteedTripTransfers
    mockedStream.push('"1100084","8014440:0:1","tripA","tripB","1","0"\n');
    // Type 1 without trip IDs -> goes to transfers as stop-to-stop
    mockedStream.push('"1100097","8014447","","","1","120"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    // Type 1 with trip IDs -> guaranteedTripTransfers
    const expectedGuaranteedTripTransfers = [
      {
        fromStop: 0,
        fromTrip: 'tripA',
        toStop: 1,
        toTrip: 'tripB',
      },
    ];

    // Type 1 without trip IDs -> transfers (stop-to-stop)
    const expectedTransfers = new Map([
      [
        2,
        [
          {
            destination: 3,
            type: TransferTypes.GUARANTEED,
            minTransferTime: durationFromSeconds(120),
          },
        ],
      ],
    ]);

    assert.deepEqual(result.transfers, expectedTransfers);
    assert.deepEqual(result.tripContinuations, []);
    assert.deepEqual(
      result.guaranteedTripTransfers,
      expectedGuaranteedTripTransfers,
    );
  });

  it('should ignore in-seat transfers with missing trip IDs', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1",,"trip2","4","0"\n');
    mockedStream.push('"1100097","8014447","trip3",,"4","0"\n');
    mockedStream.push('"1100098","8014448","","trip5","4","0"\n');
    mockedStream.push('"1100099","8014449","trip6","","4","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should ignore unsupported transfer types between trips', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","trip1","trip2","1","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should ignore unsupported transfer types between routes', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_route_id,to_route_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","route1","route2","1","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should handle transfers without minimum transfer time', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","2"\n');
    mockedStream.push('"1100097","8014447","1","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedTransfers = new Map([
      [
        0,
        [
          {
            destination: 1,
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
          },
        ],
      ],
      [
        2,
        [
          {
            destination: 3,
            type: TransferTypes.GUARANTEED,
            minTransferTime: durationFromSeconds(0),
          },
        ],
      ],
    ]);

    assert.deepEqual(result.transfers, expectedTransfers);
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should handle mixed transfers and trip continuations', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"1100084","8014440:0:1","","","1","120"\n');
    mockedStream.push('"1100097","8014447","trip1","trip2","4","0"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '1100097',
        {
          id: 2,
          sourceStopId: '1100097',
          name: 'Test Stop 3',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014447',
        {
          id: 3,
          sourceStopId: '8014447',
          name: 'Test Stop 4',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedTransfers = new Map([
      [
        0,
        [
          {
            destination: 1,
            type: TransferTypes.GUARANTEED,
            minTransferTime: durationFromSeconds(120),
          },
        ],
      ],
    ]);

    const expectedTripContinuations = [
      {
        fromStop: 2,
        fromTrip: 'trip1',
        toStop: 3,
        toTrip: 'trip2',
      },
    ];

    assert.deepEqual(result.transfers, expectedTransfers);
    assert.deepEqual(result.tripContinuations, expectedTripContinuations);
  });

  it('should filter service-scoped transfers by active service regardless of type', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,from_trip_id,to_trip_id,service_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"from","to","","","","0",""\n');
    mockedStream.push('"from","to","","","","2","120"\n');
    mockedStream.push('"from","to","","","active","1",""\n');
    mockedStream.push('"from","to","","","inactive","2","180"\n');
    mockedStream.push('"from","to","tripA","tripB","active","1",""\n');
    mockedStream.push('"from","to","tripC","tripD","inactive","1",""\n');
    mockedStream.push('"from","to","tripE","tripF","active","4",""\n');
    mockedStream.push('"from","to","tripG","tripH","inactive","4",""\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        'from',
        {
          id: 0,
          sourceStopId: 'from',
          name: 'From',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        'to',
        {
          id: 1,
          sourceStopId: 'to',
          name: 'To',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(
      mockedStream,
      stopsMap,
      new Set(['active']),
    );

    assert.deepEqual(
      result.transfers,
      new Map([
        [
          0,
          [
            {
              destination: 1,
              type: TransferTypes.RECOMMENDED,
              minTransferTime: durationFromSeconds(0),
            },
            {
              destination: 1,
              type: TransferTypes.REQUIRES_MINIMAL_TIME,
              minTransferTime: durationFromSeconds(120),
            },
            {
              destination: 1,
              type: TransferTypes.GUARANTEED,
              minTransferTime: durationFromSeconds(0),
            },
          ],
        ],
      ]),
    );
    assert.deepEqual(result.guaranteedTripTransfers, [
      {
        fromStop: 0,
        fromTrip: 'tripA',
        toStop: 1,
        toTrip: 'tripB',
      },
    ]);
    assert.deepEqual(result.tripContinuations, [
      {
        fromStop: 0,
        fromTrip: 'tripE',
        toStop: 1,
        toTrip: 'tripF',
      },
    ]);
  });

  it('should handle empty transfers file', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map();

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    assert.deepEqual(result.transfers, new Map());
    assert.deepEqual(result.tripContinuations, []);
  });

  it('should ignore transfers with non-existent stops', async () => {
    const mockedStream = new Readable();
    mockedStream.push(
      'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
    );
    mockedStream.push('"unknown_stop","8014440:0:1","0","120"\n');
    mockedStream.push('"1100084","unknown_stop","1","60"\n');
    mockedStream.push('"1100084","8014440:0:1","2","180"\n');
    mockedStream.push(null);

    const stopsMap: GtfsStopsMap = new Map([
      [
        '1100084',
        {
          id: 0,
          sourceStopId: '1100084',
          name: 'Test Stop 1',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
      [
        '8014440:0:1',
        {
          id: 1,
          sourceStopId: '8014440:0:1',
          name: 'Test Stop 2',
          children: [],
          locationType: 'SIMPLE_STOP_OR_PLATFORM',
        },
      ],
    ]);

    const result = await parseTransfers(mockedStream, stopsMap, new Set());

    const expectedTransfers = new Map([
      [
        0,
        [
          {
            destination: 1,
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
            minTransferTime: durationFromSeconds(180),
          },
        ],
      ],
    ]);

    assert.deepEqual(result.transfers, expectedTransfers);
    assert.deepEqual(result.tripContinuations, []);
  });
});

describe('generated transfers', () => {
  const stopsMap: GtfsStopsMap = new Map([
    [
      'station',
      {
        id: 0,
        sourceStopId: 'station',
        name: 'Interchange',
        children: [1, 2],
        locationType: 'STATION',
      },
    ],
    [
      'platform-a',
      {
        id: 1,
        sourceStopId: 'platform-a',
        name: 'Interchange',
        parent: 0,
        children: [],
        locationType: 'SIMPLE_STOP_OR_PLATFORM',
      },
    ],
    [
      'platform-b',
      {
        id: 2,
        sourceStopId: 'platform-b',
        name: 'Interchange',
        parent: 0,
        children: [],
        locationType: 'SIMPLE_STOP_OR_PLATFORM',
      },
    ],
  ]);

  it('adds directed fallback transfers between active sibling platforms', () => {
    const transfers: TransfersMap = new Map();

    const added = addMissingSiblingTransfers(
      stopsMap,
      new Set([1, 2]),
      transfers,
    );

    assert.strictEqual(added, 2);
    assert.deepStrictEqual(
      transfers,
      new Map([
        [
          1,
          [
            {
              destination: 2,
              type: TransferTypes.REQUIRES_MINIMAL_TIME,
            },
          ],
        ],
        [
          2,
          [
            {
              destination: 1,
              type: TransferTypes.REQUIRES_MINIMAL_TIME,
            },
          ],
        ],
      ]),
    );
  });

  it('preserves explicit transfers and excludes forbidden sibling directions', () => {
    const transfers: TransfersMap = new Map([
      [
        1,
        [
          {
            destination: 2,
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
            minTransferTime: 7,
          },
        ],
      ],
    ]);
    const forbiddenTransfers: ForbiddenTransfersMap = new Map([
      [2, new Set([1])],
    ]);

    const added = addMissingSiblingTransfers(
      stopsMap,
      new Set([1, 2]),
      transfers,
      forbiddenTransfers,
    );

    assert.strictEqual(added, 0);
    assert.deepStrictEqual(transfers.get(1), [
      {
        destination: 2,
        type: TransferTypes.REQUIRES_MINIMAL_TIME,
        minTransferTime: 7,
      },
    ]);
    assert.strictEqual(transfers.has(2), false);
  });

  it('excludes forbidden directions from generated transfers', () => {
    const transfers: TransfersMap = new Map();
    const generatedTransfers: TransfersMap = new Map([
      [
        1,
        [
          {
            destination: 2,
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
            minTransferTime: 3,
          },
        ],
      ],
      [
        2,
        [
          {
            destination: 1,
            type: TransferTypes.REQUIRES_MINIMAL_TIME,
            minTransferTime: 3,
          },
        ],
      ],
    ]);
    const forbiddenTransfers: ForbiddenTransfersMap = new Map([
      [1, new Set([2])],
    ]);

    const added = addGeneratedTransfers(
      generatedTransfers,
      new Set([1, 2]),
      transfers,
      forbiddenTransfers,
    );

    assert.strictEqual(added, 1);
    assert.strictEqual(transfers.has(1), false);
    assert.deepStrictEqual(transfers.get(2), [
      {
        destination: 1,
        type: TransferTypes.REQUIRES_MINIMAL_TIME,
        minTransferTime: 3,
      },
    ]);
  });
});

describe('buildTripTransfers', () => {
  it('should build trip transfers for valid data', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
    ];

    const mockFromRoute = {
      stopRouteIndices: () => [0],
      arrivalAt: () => timeFromHM(1, 0), // 1:00
    } as unknown as Route;

    const mockToRoute = {
      stopRouteIndices: () => [1],
      departureFrom: () => timeFromHM(1, 15), // 1:15
    } as unknown as Route;

    const mockTimetable = {
      getRoute: (routeId: number) =>
        routeId === 0 ? mockFromRoute : mockToRoute,
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    const expectedTripBoardingId = encode(0, 0, 0);
    const transfers = result.get(expectedTripBoardingId);

    assert(transfers);
    assert.strictEqual(transfers.length, 1);
    assert.deepEqual(transfers[0], {
      stopIndex: 1,
      routeId: 1,
      tripIndex: 0,
    });
  });

  it('should ignore transfers with inactive stops', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100, // inactive stop
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
    ];

    const mockTimetable = {} as unknown as Timetable;
    const activeStopIds = new Set([200]); // only toStop is active

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    assert.strictEqual(result.size, 0);
  });

  it('should ignore transfers with unknown trip IDs', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'unknown_trip', // not in tripsMapping
        toStop: 200,
        toTrip: 'trip1',
      },
    ];

    const mockTimetable = {} as unknown as Timetable;
    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    assert.strictEqual(result.size, 0);
  });

  it('should ignore transfers with unknown routes', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
    ];

    const mockTimetable = {
      getRoute: () => undefined, // no routes found
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    assert.strictEqual(result.size, 0);
  });

  it('should ignore transfers with no valid timing', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
    ];

    const mockFromRoute = {
      stopRouteIndices: () => [0],
      arrivalAt: () => timeFromHM(1, 15), // 1:15 - arrives AFTER departure
    } as unknown as Route;

    const mockToRoute = {
      stopRouteIndices: () => [1],
      departureFrom: () => timeFromHM(1, 0), // 1:00 - departs BEFORE arrival
    } as unknown as Route;

    const mockTimetable = {
      getRoute: (routeId: number) =>
        routeId === 0 ? mockFromRoute : mockToRoute,
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    assert.strictEqual(result.size, 0);
  });

  it('should handle multiple transfers from same trip boarding', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
      ['trip3', { routeId: 2, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 300,
        toTrip: 'trip3',
      },
    ];

    const mockFromRoute = {
      stopRouteIndices: () => [0],
      arrivalAt: () => timeFromHM(1, 0),
    } as unknown as Route;

    const mockToRoute1 = {
      stopRouteIndices: () => [1],
      departureFrom: () => timeFromHM(1, 10),
    } as unknown as Route;

    const mockToRoute2 = {
      stopRouteIndices: () => [2],
      departureFrom: () => timeFromHM(1, 20),
    } as unknown as Route;

    const mockTimetable = {
      getRoute: (routeId: number) => {
        if (routeId === 0) return mockFromRoute;
        if (routeId === 1) return mockToRoute1;
        if (routeId === 2) return mockToRoute2;
        return undefined;
      },
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200, 300]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    const expectedTripBoardingId = encode(0, 0, 0);
    const transfers = result.get(expectedTripBoardingId);

    assert(transfers);
    assert.strictEqual(transfers.length, 2);
    assert.deepEqual(transfers[0], {
      stopIndex: 1,
      routeId: 1,
      tripIndex: 0,
    });
    assert.deepEqual(transfers[1], {
      stopIndex: 2,
      routeId: 2,
      tripIndex: 0,
    });
  });

  it('should handle empty input gracefully', () => {
    const tripsMapping: TripsMapping = new Map();
    const tripTransfers: GtfsTripTransfer[] = [];
    const mockTimetable = {} as unknown as Timetable;
    const activeStopIds = new Set<number>();

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    assert.strictEqual(result.size, 0);
  });

  it('should disambiguate transfers when routes visit same stop multiple times', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100, // This stop appears multiple times in the route
        fromTrip: 'trip1',
        toStop: 200, // This stop also appears multiple times
        toTrip: 'trip2',
      },
    ];

    // Mock route that visits stop 100 at indices 0 and 3 (circular route)
    const mockFromRoute = {
      stopRouteIndices: () => [0, 3], // Stop 100 appears twice
      arrivalAt: (stopIndex: number) => {
        // First visit at 1:00, second visit at 2:00
        return stopIndex === 0 ? timeFromHM(1, 0) : timeFromHM(2, 0);
      },
    } as unknown as Route;

    // Mock route that visits stop 200 at indices 1 and 4
    const mockToRoute = {
      stopRouteIndices: () => [1, 4], // Stop 200 appears twice
      departureFrom: (stopIndex: number) => {
        // First departure at 1:10, second departure at 2:30
        return stopIndex === 1 ? timeFromHM(1, 10) : timeFromHM(2, 30);
      },
    } as unknown as Route;

    const mockTimetable = {
      getRoute: (routeId: number) =>
        routeId === 0 ? mockFromRoute : mockToRoute,
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    // Should pick the best timing: arrive at stop 0 (1:00) -> depart from stop 1 (1:10)
    // This is better than arrive at stop 3 (2:00) -> depart from stop 4 (2:30)
    const expectedTripBoardingId = encode(0, 0, 0); // stopIndex=0, routeId=0, tripIndex=0
    const transfers = result.get(expectedTripBoardingId);

    assert(transfers);
    assert.strictEqual(transfers.length, 1);
    assert.deepEqual(transfers[0], {
      stopIndex: 1, // Best to-stop index
      routeId: 1,
      tripIndex: 0,
    });
  });

  it('should handle case where no valid transfer timing exists between duplicate stops', () => {
    const tripsMapping: TripsMapping = new Map([
      ['trip1', { routeId: 0, tripRouteIndex: 0 }],
      ['trip2', { routeId: 1, tripRouteIndex: 0 }],
    ]);

    const tripTransfers: GtfsTripTransfer[] = [
      {
        fromStop: 100,
        fromTrip: 'trip1',
        toStop: 200,
        toTrip: 'trip2',
      },
    ];

    // Mock route where all arrivals are AFTER all departures (impossible transfer)
    const mockFromRoute = {
      stopRouteIndices: () => [0, 3], // Stop 100 appears twice
      arrivalAt: (stopIndex: number) => {
        // Both arrivals are late: 2:00 and 3:00
        return stopIndex === 0 ? timeFromHM(2, 0) : timeFromHM(3, 0);
      },
    } as unknown as Route;

    const mockToRoute = {
      stopRouteIndices: () => [1, 4], // Stop 200 appears twice
      departureFrom: (stopIndex: number) => {
        // Both departures are early: 1:00 and 1:30
        return stopIndex === 1 ? timeFromHM(1, 0) : timeFromHM(1, 30);
      },
    } as unknown as Route;

    const mockTimetable = {
      getRoute: (routeId: number) =>
        routeId === 0 ? mockFromRoute : mockToRoute,
    } as unknown as Timetable;

    const activeStopIds = new Set([100, 200]);

    const result = buildTripTransfers(
      tripsMapping,
      tripTransfers,
      mockTimetable,
      activeStopIds,
    );

    // Should find no valid transfers since all departures are before arrivals
    assert.strictEqual(result.size, 0);
  });
});
