"""Tests for car journey YAML generation.""" import json from datetime import date, datetime from pathlib import Path import agenda.car_journey_yaml from agenda.types import Trip def write_home_route(data_dir: Path) -> None: """Write a route that identifies PCH as the home coordinate.""" route_dir = data_dir / "car_routes" route_dir.mkdir() (route_dir / "PCH_to_EMF.geojson").write_text( json.dumps( { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[-2.60283, 51.44083], [-2.37789, 52.038]], }, } ) ) def test_car_journeys_for_trip_uses_accommodation_dates_and_locations( tmp_path: Path, ) -> None: """Build outbound and return journeys for the trip accommodation.""" write_home_route(tmp_path) trip = Trip( start=date(2025, 4, 22), accommodation=[ { "location": "Callestick", "from": datetime(2025, 4, 22, 15, 0), "to": datetime(2025, 4, 25, 10, 0), "latitude": 50.307093, "longitude": -5.13466, } ], ) journeys = agenda.car_journey_yaml.car_journeys_for_trip( trip, tmp_path, destination="accommodation" ) assert [journey.route for journey in journeys] == [ "PCH_to_Callestick", "Callestick_to_PCH", ] assert [journey.depart for journey in journeys] == [ date(2025, 4, 22), date(2025, 4, 25), ] assert journeys[0].start == (-2.60283, 51.44083) assert journeys[0].end == (-5.13466, 50.307093) assert journeys[1].start == (-5.13466, 50.307093) assert journeys[1].end == (-2.60283, 51.44083) def test_car_journeys_for_trip_can_route_to_airport(tmp_path: Path) -> None: """Build outbound and return journeys for airport access.""" write_home_route(tmp_path) trip = Trip( start=date(2025, 11, 17), travel=[ { "type": "flight", "depart": datetime(2025, 11, 17, 17, 55), "arrive": datetime(2025, 11, 18, 14, 50), "from_airport": { "iata": "LHR", "latitude": 51.4775, "longitude": -0.461389, }, "to_airport": { "iata": "HKG", "latitude": 22.308889, "longitude": 113.914444, }, }, { "type": "flight", "depart": datetime(2025, 11, 30, 22, 45), "arrive": datetime(2025, 12, 1, 5, 35), "from_airport": { "iata": "HKG", "latitude": 22.308889, "longitude": 113.914444, }, "to_airport": { "iata": "LHR", "latitude": 51.4775, "longitude": -0.461389, }, }, ], ) journeys = agenda.car_journey_yaml.car_journeys_for_trip( trip, tmp_path, destination="airport" ) assert [journey.route for journey in journeys] == ["PCH_to_LHR", "LHR_to_PCH"] assert [journey.depart for journey in journeys] == [ date(2025, 11, 17), date(2025, 12, 1), ] assert journeys[0].start == (-2.60283, 51.44083) assert journeys[0].end == (-0.461389, 51.4775) assert journeys[1].start == (-0.461389, 51.4775) assert journeys[1].end == (-2.60283, 51.44083) def test_car_journeys_for_trip_can_route_around_ferry(tmp_path: Path) -> None: """Build home, ferry terminal and accommodation driving legs.""" write_home_route(tmp_path) trip = Trip( start=date(2025, 7, 4), travel=[ { "type": "ferry", "depart": datetime(2025, 7, 4, 22, 0), "arrive": datetime(2025, 7, 5, 8, 0), "from": "Plymouth", "to": "Roscoff", "from_terminal": { "name": "Plymouth", "latitude": 50.3651254, "longitude": -4.1578164, }, "to_terminal": { "name": "Roscoff", "latitude": 48.721672, "longitude": -3.966925, }, }, { "type": "ferry", "depart": datetime(2025, 7, 21, 15, 0), "arrive": datetime(2025, 7, 21, 20, 10), "from": "Roscoff", "to": "Plymouth", "from_terminal": { "name": "Roscoff", "latitude": 48.721672, "longitude": -3.966925, }, "to_terminal": { "name": "Plymouth", "latitude": 50.3651254, "longitude": -4.1578164, }, }, ], accommodation=[ { "location": "Brest", "from": datetime(2025, 7, 5, 16, 0), "to": datetime(2025, 7, 21, 10, 0), "latitude": 48.3645307, "longitude": -4.5534685, } ], ) journeys = agenda.car_journey_yaml.car_journeys_for_trip( trip, tmp_path, destination="ferry" ) assert [journey.route for journey in journeys] == [ "PCH_to_Plymouth", "Roscoff_to_Brest", "Brest_to_Roscoff", "Plymouth_to_PCH", ] assert [journey.depart for journey in journeys] == [ date(2025, 7, 4), date(2025, 7, 5), date(2025, 7, 21), date(2025, 7, 21), ] assert journeys[0].start == (-2.60283, 51.44083) assert journeys[0].end == (-4.1578164, 50.3651254) assert journeys[1].start == (-3.966925, 48.721672) assert journeys[1].end == (-4.5534685, 48.3645307) assert journeys[2].start == (-4.5534685, 48.3645307) assert journeys[2].end == (-3.966925, 48.721672) assert journeys[3].start == (-4.1578164, 50.3651254) assert journeys[3].end == (-2.60283, 51.44083) def test_car_journeys_for_trip_can_route_fly_drive_chain(tmp_path: Path) -> None: """Build home, destination rental and return airport driving legs.""" write_home_route(tmp_path) trip = Trip( start=date(2025, 9, 13), travel=[ { "type": "flight", "depart": datetime(2025, 9, 14, 6, 50), "arrive": datetime(2025, 9, 14, 8, 55), "from_airport": { "iata": "LTN", "latitude": 51.8747, "longitude": -0.3683, "country": "gb", }, "to_airport": { "iata": "KEF", "latitude": 63.985, "longitude": -22.605556, "country": "is", }, }, { "type": "flight", "depart": datetime(2025, 10, 1, 9, 0), "arrive": datetime(2025, 10, 1, 13, 0), "from_airport": { "iata": "KEF", "latitude": 63.985, "longitude": -22.605556, "country": "is", }, "to_airport": { "iata": "LTN", "latitude": 51.8747, "longitude": -0.3683, "country": "gb", }, }, ], accommodation=[ { "location": "Luton Airport", "country": "gb", "from": datetime(2025, 9, 13, 15, 0), "to": datetime(2025, 9, 14, 12, 0), "latitude": 51.8745678, "longitude": -0.3844256, }, { "location": "Akranes", "country": "is", "from": datetime(2025, 9, 14, 16, 0), "to": datetime(2025, 9, 16, 11, 0), "latitude": 64.4344723, "longitude": -21.5517961, }, { "location": "Ísafjörður", "country": "is", "from": datetime(2025, 9, 16, 15, 0), "to": datetime(2025, 10, 1, 11, 0), "latitude": 66.072445, "longitude": -23.120111, }, ], ) journeys = agenda.car_journey_yaml.car_journeys_for_trip( trip, tmp_path, destination="fly-drive" ) assert [journey.route for journey in journeys] == [ "PCH_to_Luton_Airport", "KEF_to_Akranes", "Akranes_to_Isafjordur", "Isafjordur_to_KEF", "LTN_to_PCH", ] assert [journey.depart for journey in journeys] == [ date(2025, 9, 13), date(2025, 9, 14), date(2025, 9, 16), date(2025, 10, 1), date(2025, 10, 1), ] def test_write_route_files_skips_existing_routes(tmp_path: Path) -> None: """Existing route files should not call the route API.""" write_home_route(tmp_path) journey = agenda.car_journey_yaml.CarJourney( trip=date(2025, 4, 22), depart=date(2025, 4, 22), arrive=date(2025, 4, 22), route="PCH_to_EMF", start=(-2.60283, 51.44083), end=(-2.37789, 52.038), ) def fail_fetch( _start: agenda.car_journey_yaml.LonLat, _end: agenda.car_journey_yaml.LonLat, ) -> agenda.car_journey_yaml.GeoJSON: raise AssertionError("route API should not be called") written = agenda.car_journey_yaml.write_route_files(tmp_path, [journey], fail_fetch) assert written == 0 def test_import_car_journeys_inserts_chronologically_and_is_idempotent( tmp_path: Path, ) -> None: """Generated journeys should be inserted without duplicates.""" (tmp_path / "car_journeys.yaml").write_text("""--- - trip: 2025-05-01 depart: 2025-05-01 arrive: 2025-05-01 route: PCH_to_Later """) journeys = [ agenda.car_journey_yaml.CarJourney( trip=date(2025, 4, 22), depart=date(2025, 4, 22), arrive=date(2025, 4, 22), route="PCH_to_Callestick", start=(-2.60283, 51.44083), end=(-5.13466, 50.307093), ), agenda.car_journey_yaml.CarJourney( trip=date(2025, 4, 22), depart=date(2025, 4, 25), arrive=date(2025, 4, 25), route="Callestick_to_PCH", start=(-5.13466, 50.307093), end=(-2.60283, 51.44083), ), ] added = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys) added_again = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys) assert added == 2 assert added_again == 0 text = (tmp_path / "car_journeys.yaml").read_text() assert text.index("PCH_to_Callestick") < text.index("PCH_to_Later")