import pytest from trip_planner import combine_trips, find_unreachable_morning_eurostars, _fmt_duration, cheapest_gwr_ticket 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 — over limit 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(): # 2h 26m GWR journey exceeds MAX_GWR_MINUTES (110) 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_unreachable_morning_eurostars_lists_only_unreachable_morning_services(): 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 [service['depart_st_pancras'] for service in unreachable] == ['09:30'] # --------------------------------------------------------------------------- # cheapest_gwr_ticket — Bristol Temple Meads → Paddington # --------------------------------------------------------------------------- # 2026-03-30 is a Monday; 2026-03-28 is a Saturday def test_cheapest_ticket_weekday_super_off_peak_morning(): # 05:00 on Monday: dep ≤ 05:04 → Super Off-Peak t = cheapest_gwr_ticket('05:00', '2026-03-30') assert t['ticket'] == 'Super Off-Peak' assert t['price'] == 45.00 def test_cheapest_ticket_weekday_anytime_window(): # 07:00 on Monday: 05:05–08:25 → Anytime only t = cheapest_gwr_ticket('07:00', '2026-03-30') assert t['ticket'] == 'Anytime' assert t['price'] == 138.70 def test_cheapest_ticket_weekday_off_peak(): # 08:30 on Monday: dep ≥ 08:26 but < 09:58 → Off-Peak t = cheapest_gwr_ticket('08:30', '2026-03-30') assert t['ticket'] == 'Off-Peak' assert t['price'] == 63.60 def test_cheapest_ticket_weekday_super_off_peak_late(): # 10:00 on Monday: dep ≥ 09:58 → Super Off-Peak t = cheapest_gwr_ticket('10:00', '2026-03-30') assert t['ticket'] == 'Super Off-Peak' assert t['price'] == 45.00 def test_cheapest_ticket_boundary_super_off_peak_cutoff(): # 05:04 is last valid minute for early Super Off-Peak assert cheapest_gwr_ticket('05:04', '2026-03-30')['ticket'] == 'Super Off-Peak' # 05:05 falls into the Anytime window (off-peak starts at 08:26) assert cheapest_gwr_ticket('05:05', '2026-03-30')['ticket'] == 'Anytime' def test_cheapest_ticket_boundary_off_peak_start(): assert cheapest_gwr_ticket('08:25', '2026-03-30')['ticket'] == 'Anytime' assert cheapest_gwr_ticket('08:26', '2026-03-30')['ticket'] == 'Off-Peak' def test_cheapest_ticket_boundary_super_off_peak_resumes(): assert cheapest_gwr_ticket('09:57', '2026-03-30')['ticket'] == 'Off-Peak' assert cheapest_gwr_ticket('09:58', '2026-03-30')['ticket'] == 'Super Off-Peak' def test_cheapest_ticket_weekend_always_super_off_peak(): # Saturday — no restrictions t = cheapest_gwr_ticket('07:00', '2026-03-28') assert t['ticket'] == 'Super Off-Peak' assert t['price'] == 45.00 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_unreachable_morning_eurostars_returns_empty_when_morning_service_is_reachable(): 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) == []