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

import { encodePickUpDropOffTypes } from '../../gtfs/trips.js';
import { PickUpDropOffTypes, Route } from '../route.js';
import { TIME_ORIGIN, timeFromHMS } from '../time.js';

describe('Route', () => {
  const stopTimes = new Uint16Array([
    // Trip 0: Stop 1 -> Stop 2
    timeFromHMS(8, 0, 0),
    timeFromHMS(8, 1, 0),
    timeFromHMS(8, 30, 0),
    timeFromHMS(8, 31, 0),
    // Trip 1: Stop 1 -> Stop 2
    timeFromHMS(9, 0, 0),
    timeFromHMS(9, 1, 0),
    timeFromHMS(9, 30, 0),
    timeFromHMS(9, 31, 0),
    // Trip 2: Stop 1 -> Stop 2
    timeFromHMS(10, 0, 0),
    timeFromHMS(10, 1, 0),
    timeFromHMS(10, 30, 0),
    timeFromHMS(10, 31, 0),
  ]);

  const pickupDropOffTypes = encodePickUpDropOffTypes(
    [
      // Trip 0
      PickUpDropOffTypes.REGULAR,
      PickUpDropOffTypes.NOT_AVAILABLE,
      // Trip 1
      PickUpDropOffTypes.REGULAR,
      PickUpDropOffTypes.REGULAR,
      // Trip 2
      PickUpDropOffTypes.MUST_PHONE_AGENCY,
      PickUpDropOffTypes.MUST_COORDINATE_WITH_DRIVER,
    ],
    [
      // Trip 0
      PickUpDropOffTypes.REGULAR,
      PickUpDropOffTypes.REGULAR,
      // Trip 1
      PickUpDropOffTypes.REGULAR,
      PickUpDropOffTypes.REGULAR,
      // Trip 2
      PickUpDropOffTypes.REGULAR,
      PickUpDropOffTypes.REGULAR,
    ],
  );

  const stops = new Uint32Array([1001, 1002]);
  const serviceRouteId = 0;

  const route = new Route(
    0,
    stopTimes,
    pickupDropOffTypes,
    stops,
    serviceRouteId,
  );

  describe('constructor', () => {
    it('should create a route with correct properties', () => {
      assert.strictEqual(route.getNbStops(), 2);
      assert.strictEqual(route.serviceRoute(), serviceRouteId);
    });

    it('should handle empty route', () => {
      const emptyRoute = new Route(
        0,
        new Uint16Array([]),
        new Uint8Array([]),
        new Uint32Array([]),
        1,
      );
      assert.strictEqual(emptyRoute.getNbStops(), 0);
      assert.strictEqual(emptyRoute.serviceRoute(), 1);
    });
  });

  describe('serialize', () => {
    it('should serialize route data correctly', () => {
      const serialized = route.serialize();
      assert.deepStrictEqual(serialized.stopTimes, stopTimes);
      assert.deepStrictEqual(serialized.pickupDropOffTypes, pickupDropOffTypes);
      assert.deepStrictEqual(serialized.stops, stops);
      assert.strictEqual(serialized.serviceRouteId, serviceRouteId);
    });
  });

  describe('getNbStops', () => {
    it('should return correct number of stops', () => {
      assert.strictEqual(route.getNbStops(), 2);
    });
  });

  describe('serviceRoute', () => {
    it('should return correct service route ID', () => {
      assert.strictEqual(route.serviceRoute(), serviceRouteId);
    });
  });

  describe('arrivalAt', () => {
    it('should return correct arrival time for trip 0 at stop index 0', () => {
      const arrival = route.arrivalAt(0, 0);
      assert.strictEqual(arrival, timeFromHMS(8, 0, 0));
    });

    it('should return correct arrival time for trip 1 at stop index 1', () => {
      const arrival = route.arrivalAt(1, 1);
      assert.strictEqual(arrival, timeFromHMS(9, 30, 0));
    });

    it('should throw error for invalid stop index', () => {
      assert.throws(
        () => route.arrivalAt(999, 0),
        /StopId for stop at index 999 not found/,
      );
    });

    it('should throw error for invalid trip index', () => {
      assert.throws(() => route.arrivalAt(0, 999), /Arrival time not found/);
    });
  });

  describe('departureFrom', () => {
    it('should return correct departure time for trip 0 at stop index 0', () => {
      const departure = route.departureFrom(0, 0);
      assert.strictEqual(departure, timeFromHMS(8, 1, 0));
    });

    it('should return correct departure time for trip 2 at stop index 1', () => {
      const departure = route.departureFrom(1, 2);
      assert.strictEqual(departure, timeFromHMS(10, 31, 0));
    });

    it('should throw error for invalid stop index', () => {
      assert.throws(
        () => route.departureFrom(999, 0),
        /StopId for stop at index 999 not found/,
      );
    });

    it('should throw error for invalid trip index', () => {
      assert.throws(
        () => route.departureFrom(0, 999),
        /Departure time not found/,
      );
    });
  });

  describe('pickUpTypeFrom', () => {
    it('should return REGULAR pickup type for trip 0 at stop index 0', () => {
      const pickUpType = route.pickUpTypeFrom(0, 0);
      assert.strictEqual(pickUpType, PickUpDropOffTypes.REGULAR);
    });

    it('should return NOT_AVAILABLE pickup type for trip 0 at stop index 1', () => {
      const pickUpType = route.pickUpTypeFrom(1, 0);
      assert.strictEqual(pickUpType, PickUpDropOffTypes.NOT_AVAILABLE);
    });

    it('should return MUST_PHONE_AGENCY pickup type for trip 2 at stop index 0', () => {
      const pickUpType = route.pickUpTypeFrom(0, 2);
      assert.strictEqual(pickUpType, PickUpDropOffTypes.MUST_PHONE_AGENCY);
    });

    it('should throw error for invalid stop index', () => {
      assert.throws(
        () => route.pickUpTypeFrom(999, 0),
        /StopId for stop at index 999 not found/,
      );
    });

    it('should throw error for invalid trip index', () => {
      assert.throws(
        () => route.pickUpTypeFrom(0, 999),
        /Pick up type not found/,
      );
    });
  });

  describe('dropOffTypeAt', () => {
    it('should return REGULAR drop off type for trip 0 at stop index 0', () => {
      const dropOffType = route.dropOffTypeAt(0, 0);
      assert.strictEqual(dropOffType, PickUpDropOffTypes.REGULAR);
    });

    it('should return REGULAR drop off type for trip 1 at stop index 1', () => {
      const dropOffType = route.dropOffTypeAt(1, 1);
      assert.strictEqual(dropOffType, PickUpDropOffTypes.REGULAR);
    });

    it('should throw error for invalid stop index', () => {
      assert.throws(
        () => route.dropOffTypeAt(999, 0),
        /StopId for stop at index 999 not found/,
      );
    });

    it('should throw error for invalid trip index', () => {
      assert.throws(
        () => route.dropOffTypeAt(0, 999),
        /Drop off type not found/,
      );
    });
  });

  describe('findEarliestTrip', () => {
    it('should find earliest trip without time constraint', () => {
      const tripIndex = route.findEarliestTrip(0);
      assert.strictEqual(tripIndex, 0);
    });

    it('should find earliest trip after specified time', () => {
      const afterTime = timeFromHMS(8, 30, 0);
      const tripIndex = route.findEarliestTrip(0, afterTime);
      assert.strictEqual(tripIndex, 1);
    });

    it('should find earliest trip with exact match time', () => {
      const afterTime = timeFromHMS(9, 1, 0);
      const tripIndex = route.findEarliestTrip(0, afterTime);
      assert.strictEqual(tripIndex, 1);
    });

    it('should return undefined when no trip is available after specified time', () => {
      const afterTime = timeFromHMS(23, 0, 0);
      const tripIndex = route.findEarliestTrip(0, afterTime);
      assert.strictEqual(tripIndex, undefined);
    });

    it('should find earliest trip', () => {
      const tripIndex = route.findEarliestTrip(1);
      // findEarliestTrip only filters by time, not pickup type
      assert.strictEqual(tripIndex, 0);
    });

    it('should respect beforeTrip constraint', () => {
      const tripIndex = route.findEarliestTrip(0, timeFromHMS(8, 2, 0), 1);
      assert.strictEqual(tripIndex, undefined);
    });

    it('should return undefined when beforeTrip is 0', () => {
      const tripIndex = route.findEarliestTrip(0, TIME_ORIGIN, 0);
      assert.strictEqual(tripIndex, undefined);
    });

    it('should handle MUST_PHONE_AGENCY pickup type', () => {
      const afterTime = timeFromHMS(9, 30, 0);
      const tripIndex = route.findEarliestTrip(0, afterTime);
      // Should find trip 2 even though it requires phone agency
      assert.strictEqual(tripIndex, 2);
    });

    it('should throw error for invalid stop index', () => {
      assert.throws(
        () => route.findEarliestTrip(999),
        /StopId for stop at index 999 not found/,
      );
    });
  });

  describe('stopRouteIndices', () => {
    it('should return correct stop route indices for existing stop', () => {
      const indices = route.stopRouteIndices(1001);
      assert.deepStrictEqual(indices, [0]);
    });

    it('should return correct stop route indices for second stop', () => {
      const indices = route.stopRouteIndices(1002);
      assert.deepStrictEqual(indices, [1]);
    });

    it('should return empty array for non-existent stop', () => {
      const indices = route.stopRouteIndices(9999);
      assert.deepStrictEqual(indices, []);
    });
  });
});
