import pytest from trip_planner import combine_trips, find_unreachable_morning_eurostars, _fmt_duration DATE = '2026-03-30' # --------------------------------------------------------------------------- # _fmt_duration # --------------------------------------------------------------------------- def test_fmt_duration_hours_and_minutes(): assert _fmt_duration(95) == '1h 35m' def test_fmt_duration_exact_hours(): assert _fmt_duration(120) == '2h' def test_fmt_duration_minutes_only(): assert _fmt_duration(45) == '45m' # --------------------------------------------------------------------------- # combine_trips — basic pairing # --------------------------------------------------------------------------- GWR_FAST = {'depart_bristol': '07:00', 'arrive_paddington': '08:45'} # 1h 45m GWR_SLOW = {'depart_bristol': '07:00', 'arrive_paddington': '09:26'} # 2h 26m — connection too short for ES_PARIS ES_PARIS = {'depart_st_pancras': '10:01', 'arrive_destination': '13:34', 'destination': 'Paris Gare du Nord'} ES_EARLY = {'depart_st_pancras': '09:00', 'arrive_destination': '12:00', 'destination': 'Paris Gare du Nord'} def test_valid_trip_is_returned(): trips = combine_trips([GWR_FAST], [ES_PARIS], DATE) assert len(trips) == 1 t = trips[0] assert t['depart_bristol'] == '07:00' assert t['arrive_paddington'] == '08:45' assert t['depart_st_pancras'] == '10:01' assert t['arrive_destination'] == '13:34' assert t['destination'] == 'Paris Gare du Nord' def test_gwr_too_slow_excluded(): # arrive 09:26, Eurostar 10:01 → 35 min connection < 50 min minimum trips = combine_trips([GWR_SLOW], [ES_PARIS], DATE) assert trips == [] def test_eurostar_too_early_excluded(): # Eurostar departs before min connection time has elapsed trips = combine_trips([GWR_FAST], [ES_EARLY], DATE) assert trips == [] def test_no_trains_returns_empty(): assert combine_trips([], [], DATE) == [] def test_no_gwr_returns_empty(): assert combine_trips([], [ES_PARIS], DATE) == [] def test_no_eurostar_returns_empty(): assert combine_trips([GWR_FAST], [], DATE) == [] # --------------------------------------------------------------------------- # Connection window constraints # --------------------------------------------------------------------------- def test_min_connection_enforced(): # Arrive Paddington 08:45, need 75 min → earliest St Pancras 10:00 # ES at 09:59 should be excluded, 10:00 should be included es_too_close = {'depart_st_pancras': '09:59', 'arrive_destination': '13:00', 'destination': 'Paris Gare du Nord'} es_ok = {'depart_st_pancras': '10:00', 'arrive_destination': '13:00', 'destination': 'Paris Gare du Nord'} assert combine_trips([GWR_FAST], [es_too_close], DATE, min_connection_minutes=75) == [] trips = combine_trips([GWR_FAST], [es_ok], DATE, min_connection_minutes=75) assert len(trips) == 1 def test_max_connection_enforced(): # Arrive Paddington 08:45, max 140 min → latest St Pancras 11:05 es_ok = {'depart_st_pancras': '11:05', 'arrive_destination': '14:00', 'destination': 'Paris Gare du Nord'} es_too_late = {'depart_st_pancras': '11:06', 'arrive_destination': '14:00', 'destination': 'Paris Gare du Nord'} trips = combine_trips([GWR_FAST], [es_ok], DATE, max_connection_minutes=140) assert len(trips) == 1 assert combine_trips([GWR_FAST], [es_too_late], DATE, max_connection_minutes=140) == [] # --------------------------------------------------------------------------- # Only earliest valid Eurostar per GWR departure # --------------------------------------------------------------------------- def test_only_earliest_eurostar_per_gwr(): es1 = {'depart_st_pancras': '10:01', 'arrive_destination': '13:34', 'destination': 'Paris Gare du Nord'} es2 = {'depart_st_pancras': '11:01', 'arrive_destination': '14:34', 'destination': 'Paris Gare du Nord'} trips = combine_trips([GWR_FAST], [es1, es2], DATE) assert len(trips) == 1 assert trips[0]['depart_st_pancras'] == '10:01' # --------------------------------------------------------------------------- # Multiple GWR trains → multiple trips # --------------------------------------------------------------------------- def test_multiple_gwr_trains(): gwr2 = {'depart_bristol': '08:00', 'arrive_paddington': '09:45'} es = {'depart_st_pancras': '11:01', 'arrive_destination': '14:34', 'destination': 'Paris Gare du Nord'} trips = combine_trips([GWR_FAST, gwr2], [es], DATE, max_connection_minutes=140) assert len(trips) == 2 assert trips[0]['depart_bristol'] == '07:00' assert trips[1]['depart_bristol'] == '08:00' # --------------------------------------------------------------------------- # Duration fields # --------------------------------------------------------------------------- def test_gwr_duration_in_trip(): trips = combine_trips([GWR_FAST], [ES_PARIS], DATE) assert trips[0]['gwr_duration'] == '1h 45m' def test_total_duration_in_trip(): # depart 07:00, arrive 13:34 → 6h 34m trips = combine_trips([GWR_FAST], [ES_PARIS], DATE) assert trips[0]['total_duration'] == '6h 34m' def test_connection_duration_in_trip(): # arrive Paddington 08:45, depart St Pancras 10:01 → 1h 16m trips = combine_trips([GWR_FAST], [ES_PARIS], DATE) assert trips[0]['connection_duration'] == '1h 16m' def test_find_unreachable_eurostars_excludes_connectable_services(): # GWR arrives 08:45; default min=50/max=110 → viable window 09:35–10:35. # 09:30 too early, 10:15 connectable, 12:30 beyond max connection. gwr = [ {'depart_bristol': '07:00', 'arrive_paddington': '08:45'}, ] eurostar = [ {'depart_st_pancras': '09:30', 'arrive_destination': '12:00', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9001'}, {'depart_st_pancras': '10:15', 'arrive_destination': '13:40', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9002'}, {'depart_st_pancras': '12:30', 'arrive_destination': '15:55', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9003'}, ] unreachable = find_unreachable_morning_eurostars(gwr, eurostar, DATE) assert [s['depart_st_pancras'] for s in unreachable] == ['09:30', '12:30'] def test_combine_trips_includes_ticket_fields(): trips = combine_trips([GWR_FAST], [ES_PARIS], DATE) assert len(trips) == 1 t = trips[0] assert 'ticket_name' in t assert 'ticket_price' in t assert 'ticket_code' in t def test_combine_trips_uses_gwr_fares_when_provided(): fares = {'07:00': {'ticket': 'Super Off-Peak Single', 'price': 49.30, 'code': 'SSS'}} trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares=fares) assert len(trips) == 1 assert trips[0]['ticket_price'] == 49.30 assert trips[0]['ticket_code'] == 'SSS' def test_combine_trips_ticket_price_none_when_no_fares(): trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares={}) assert len(trips) == 1 assert trips[0]['ticket_price'] is None def test_find_unreachable_eurostars_returns_empty_when_all_connectable(): gwr = [ {'depart_bristol': '07:00', 'arrive_paddington': '08:45'}, ] eurostar = [ {'depart_st_pancras': '10:15', 'arrive_destination': '13:40', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9002'}, ] assert find_unreachable_morning_eurostars(gwr, eurostar, DATE) == []