diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index 6777228..d240d3d 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -1,13 +1,23 @@ #!/usr/bin/env python # coding: utf-8 """Functions to calculate co2 emissions""" - -import warnings from pathlib import Path from typing import Tuple import pandas as pd +from co2calculator.mobility.calculate_mobility import ( + calc_co2_bicycle, + calc_co2_bus, + calc_co2_car, + calc_co2_ferry, + calc_co2_motorbike, + calc_co2_pedelec, + calc_co2_plane, + calc_co2_train, + calc_co2_tram, +) + from ._types import Kilogram, Kilometer from .constants import ( KWH_TO_TJ, @@ -17,23 +27,15 @@ TrainFuel, BusTrainRange, FlightClass, - FlightRange, FerryClass, ElectricityFuel, HeatingFuel, Unit, TransportationMode, - EmissionCategory, ) from .data_handlers import EmissionFactors, ConversionFactors from .distances import create_distance_request, get_distance, range_categories from .parameters import ( - CarEmissionParameters, - MotorbikeEmissionParameters, - BusEmissionParameters, - TrainEmissionParameters, - PlaneEmissionParameters, - FerryEmissionParameters, ElectricityEmissionParameters, HeatingEmissionParameters, ) @@ -44,206 +46,6 @@ conversion_factors = ConversionFactors() -def calc_co2_car( - distance: Kilometer, - passengers: int = None, - size: Size = None, - fuel_type: CarFuel = None, -) -> Kilogram: - """ - Function to compute the emissions of a car trip. - :param distance: Distance travelled by car; - :param passengers: Number of passengers in the car (including the person answering the questionnaire), - [1, 2, 3, 4, 5, 6, 7, 8, 9] default: 1 - :param size: size of car - ["small", "medium", "large", "average"] default: "average" - :param fuel_type: type of fuel the car is using - ["diesel", "gasoline", "cng", "electric", "hybrid", "plug-in_hybrid", "average"] - default: "average" - :type distance: Kilometer - :type passengers: int - :type size: str - :type fuel_type: str - :return: Total emissions of trip in co2 equivalents - :rtype: Kilogram - """ - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = CarEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - # Calculate emissions - return distance * co2e / params.passengers - - -def calc_co2_motorbike(distance: Kilometer = None, size: Size = None) -> Kilogram: - """ - Function to compute the emissions of a motorbike trip. - :param distance: Distance travelled by motorbike; - alternatively param can be provided - :param size: size of motorbike - ["small", "medium", "large", "average"] - :type distance: Kilometer - :type size: str - :return: Total emissions of trip in co2 equivalents - :rtype: Kilogram - """ - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = MotorbikeEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - # Calculate emissions - return distance * co2e - - -def calc_co2_bus( - distance: Kilometer, - size: Size = None, - fuel_type: BusFuel = None, - occupancy: int = None, - vehicle_range: BusTrainRange = None, -) -> Kilogram: - """ - Function to compute the emissions of a bus trip. - :param distance: Distance travelled by bus; - :param size: size class of the bus; ["medium", "large", "average"] - :param fuel_type: type of fuel the bus is using; ["diesel", "cng", "hydrogen"] - :param occupancy: number of people on the bus [20, 50, 80, 100] - :param vehicle_range: range/haul of the vehicle ["local", "long-distance"] - :type distance: Kilometer - :type size: str - :type fuel_type: str - :type occupancy: int - :type vehicle_range: str - :return: Total emissions of trip in co2 equivalents - :rtype: Kilogram - """ - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = BusEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - return distance * co2e - - -def calc_co2_train( - distance: Kilometer, - fuel_type: TrainFuel = None, - vehicle_range: BusTrainRange = None, -) -> Kilogram: - """ - Function to compute the emissions of a train trip. - :param distance: Distance travelled by train; - :param fuel_type: type of fuel the train is using; ["diesel", "electric", "average"] - :param vehicle_range: range/haul of the vehicle ["local", "long-distance"] - :type distance: Kilometer - :type fuel_type: float - :type vehicle_range: str - :return: Total emissions of trip in co2 equivalents - :rtype: Kilogram - """ - - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = TrainEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - return distance * co2e - - -def calc_co2_plane(distance: Kilometer, seating: FlightClass = None) -> Kilogram: - """ - Function to compute emissions of a plane trip - :param distance: Distance of plane flight - :param seating: Seating class in the airplane; Emission factors differ between seating classes because - business class or first class seats take up more space. An airplane with more such therefore - needs to have higher capacity to transport less people -> more co2 - ["average", "economy_class", "business_class", "premium_economy_class", "first_class"] - :type distance: Kilometer - :type seating: str - :return: Total emissions of flight in co2 equivalents - :rtype: Kilogram - """ - - # Retrieve whether distance is <= 700, > 700 and <= 3700 or above 3700 km - # todo: move to PlaneEmissionParameters - if distance <= 700: - range = FlightRange.DOMESTIC - elif 700 < distance <= 3700: - range = FlightRange.SHORT_HAUL - elif distance > 3700: - range = FlightRange.LONG_HAUL - - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = PlaneEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - return distance * co2e - - -def calc_co2_ferry(distance: Kilometer, seating: FerryClass = None) -> Kilogram: - """ - Function to compute emissions of a ferry trip - :param distance: Distance of ferry trip - :param seating: ["average", "Foot passenger", "Car passenger"] - :type distance: Kilometer - :type seating: str - :return: Total emissions of sea travel in co2 equivalents - :rtype: Kilogram - """ - - # Validate parameters - params_extracted = {k: v for k, v in locals().items() if v is not None} - params = FerryEmissionParameters(**params_extracted) - # Get the co2 factor - co2e = emission_factors.get(params.dict()) - return distance * co2e - - -def calc_co2_bicycle(weekly_distance): - """Calculate co2 emissions for commuting by bicycle - - :param weekly_distance: distance in km per week - """ - co2e = emission_factors.get( - { - "category": EmissionCategory.TRANSPORT, - "subcategory": TransportationMode.BICYCLE, - } - ) - return co2e * weekly_distance - - -def calc_co2_pedelec(weekly_distance): - """Calculate co2 emissions for commuting by pedelec - - :param weekly_distance: distance in km per week - """ - co2e = emission_factors.get( - { - "category": EmissionCategory.TRANSPORT, - "subcategory": TransportationMode.PEDELEC, - } - ) - return co2e * weekly_distance - - -def calc_co2_tram(weekly_distance): - """Calculate co2 emissions for commuting by pedelec - - :param weekly_distance: distance in km per week - """ - co2e = emission_factors.get( - { - "category": EmissionCategory.TRANSPORT, - "subcategory": TransportationMode.TRAM, - } - ) - return co2e * weekly_distance - - def calc_co2_electricity( consumption: float, fuel_type: ElectricityFuel = None, energy_share: float = 1 ) -> Kilogram: @@ -371,32 +173,25 @@ def calc_co2_businesstrip( if transportation_mode == TransportationMode.CAR: emissions = calc_co2_car( distance=distance, - passengers=passengers, - size=size, - fuel_type=fuel_type, + options={}, ) - elif transportation_mode == TransportationMode.BUS: emissions = calc_co2_bus( distance=distance, - size=size, - fuel_type=fuel_type, - occupancy=occupancy, - vehicle_range=BusTrainRange.LONG_DISTANCE, + options={}, ) elif transportation_mode == TransportationMode.TRAIN: emissions = calc_co2_train( distance=distance, - fuel_type=fuel_type, - vehicle_range=BusTrainRange.LONG_DISTANCE, + options={}, ) elif transportation_mode == TransportationMode.PLANE: - emissions = calc_co2_plane(distance, seating=seating) + emissions = calc_co2_plane(distance, options={}) elif transportation_mode == TransportationMode.FERRY: - emissions = calc_co2_ferry(distance, seating=seating) + emissions = calc_co2_ferry(distance, options={}) else: raise ValueError( @@ -440,28 +235,22 @@ def calc_co2_commuting( # get weekly co2e for respective mode of transport if transportation_mode == TransportationMode.CAR: weekly_co2e = calc_co2_car( - passengers=passengers, - size=size, - fuel_type=fuel_type, distance=weekly_distance, + options={}, ) elif transportation_mode == TransportationMode.MOTORBIKE: - weekly_co2e = calc_co2_motorbike(size=size, distance=weekly_distance) + weekly_co2e = calc_co2_motorbike(options={}, distance=weekly_distance) elif transportation_mode == TransportationMode.BUS: weekly_co2e = calc_co2_bus( - size=size, - fuel_type=fuel_type, - occupancy=occupancy, - vehicle_range=BusTrainRange.LOCAL, distance=weekly_distance, + options={}, ) elif transportation_mode == TransportationMode.TRAIN: weekly_co2e = calc_co2_train( - fuel_type=fuel_type, - vehicle_range=BusTrainRange.LOCAL, distance=weekly_distance, + options={}, ) elif transportation_mode == TransportationMode.PEDELEC: diff --git a/co2calculator/mobility/__init__.py b/co2calculator/mobility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/co2calculator/mobility/calculate_mobility.py b/co2calculator/mobility/calculate_mobility.py new file mode 100644 index 0000000..ebededc --- /dev/null +++ b/co2calculator/mobility/calculate_mobility.py @@ -0,0 +1,199 @@ +"""Function colleciton to calculate mobility type co2 emissions""" + +from typing import Union + +from co2calculator.constants import EmissionCategory, FlightRange, TransportationMode +from .._types import Kilogram, Kilometer +from ..parameters import ( + BusEmissionParameters, + CarEmissionParameters, + FerryEmissionParameters, + MotorbikeEmissionParameters, + PlaneEmissionParameters, + TrainEmissionParameters, +) +from ..data_handlers import ConversionFactors, EmissionFactors + +emission_factors = EmissionFactors() +conversion_factors = ConversionFactors() + + +def calc_co2_car( + distance: Kilometer, options: Union[CarEmissionParameters, dict] +) -> Kilogram: + """ + Function to compute the emissions of a car trip. + :param distance: Distance travelled by car; + :param options: Options for the car trip; + :return: Total emissions of trip in co2 equivalents + :rtype: Kilogram + """ + if options is None: + options = {} + # Validate parameters + params = CarEmissionParameters.parse_obj(options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + # Calculate emissions + return distance * co2e / params.passengers + + +def calc_co2_motorbike( + distance: Kilometer, options: Union[MotorbikeEmissionParameters, dict] +) -> Kilogram: + """ + Function to compute the emissions of a motorbike trip. + :param distance: Distance travelled by motorbike; + alternatively param can be provided + :param options: Options for the motorbike trip; + :type distance: Kilometer + :type size: str + :return: Total emissions of trip in co2 equivalents + :rtype: Kilogram + """ + # Validate parameters + if options is None: + options = {} + params = MotorbikeEmissionParameters(**options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + # Calculate emissions + return distance * co2e + + +def calc_co2_bus( + distance: Kilometer, options: Union[BusEmissionParameters, dict] +) -> Kilogram: + """ + Function to compute the emissions of a bus trip. + :param distance: Distance travelled by motorbike; + alternatively param can be provided + :param options: Options for the bus trip; + :type distance: Kilometer + :type size: str + :type fuel_type: str + :type occupancy: int + :type vehicle_range: str + :return: Total emissions of trip in co2 equivalents + :rtype: Kilogram + """ + # Validate parameters + if options is None: + options = {} + params = BusEmissionParameters(**options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + return distance * co2e + + +def calc_co2_train( + distance: Kilometer, options: Union[TrainEmissionParameters, dict] +) -> Kilogram: + """ + Function to compute the emissions of a train trip. + :param distance: Distance travelled by train; + :param options: Options for the train trip; + :type distance: Kilometer + :type fuel_type: float + :type vehicle_range: str + :return: Total emissions of trip in co2 equivalents + :rtype: Kilogram + """ + + if options is None: + options = {} + params = TrainEmissionParameters(**options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + return distance * co2e + + +def calc_co2_plane(distance: Kilometer, options: PlaneEmissionParameters) -> Kilogram: + """ + Function to compute emissions of a plane trip + :param distance: Distance of plane flight + :param options: Options for the plane trip + :type distance: Kilometer + :type seating: str + :return: Total emissions of flight in co2 equivalents + :rtype: Kilogram + """ + + if options is None: + options = {} + # Retrieve whether distance is <= 700, > 700 and <= 3700 or above 3700 km + # todo: move to PlaneEmissionParameters + if distance <= 700: + options["range"] = FlightRange.DOMESTIC + elif 700 < distance <= 3700: + options["range"] = FlightRange.SHORT_HAUL + elif distance > 3700: + options["range"] = FlightRange.LONG_HAUL + + params = PlaneEmissionParameters(**options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + return distance * co2e + + +def calc_co2_ferry( + distance: Kilometer, options: Union[FerryEmissionParameters, dict] +) -> Kilogram: + """ + Function to compute emissions of a ferry trip + :param distance: Distance of ferry trip + :param options: Options for the ferry trip + :type distance: Kilometer + :type seating: str + :return: Total emissions of sea travel in co2 equivalents + :rtype: Kilogram + """ + + if options is None: + options = {} + params = FerryEmissionParameters(**options) + # Get the co2 factor + co2e = emission_factors.get(params.dict()) + return distance * co2e + + +def calc_co2_bicycle(distance: Kilometer) -> Kilogram: + """Calculate co2 emissions for commuting by bicycle + + :param distance: distance in km + """ + co2e = emission_factors.get( + { + "category": EmissionCategory.TRANSPORT, + "subcategory": TransportationMode.BICYCLE, + } + ) + return co2e * distance + + +def calc_co2_pedelec(distance: Kilometer) -> Kilogram: + """Calculate co2 emissions for commuting by pedelec + + :param distance: distance in km + """ + co2e = emission_factors.get( + { + "category": EmissionCategory.TRANSPORT, + "subcategory": TransportationMode.PEDELEC, + } + ) + return co2e * distance + + +def calc_co2_tram(distance) -> Kilogram: + """Calculate co2 emissions for commuting by pedelec + + :param distance: distance in km + """ + co2e = emission_factors.get( + { + "category": EmissionCategory.TRANSPORT, + "subcategory": TransportationMode.TRAM, + } + ) + return co2e * distance diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index ffbbdbf..1e9dd9e 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -9,238 +9,6 @@ import co2calculator.calculate as candidate from co2calculator.constants import RangeCategory -from pydantic import ValidationError - -from co2calculator.exceptions import ConversionFactorNotFound, EmissionFactorNotFound - - -@pytest.mark.parametrize( - "distance,passengers,size,fuel_type,expected_emissions", - [ - pytest.param(100, None, None, None, 21.5, id="defaults"), - pytest.param(444, 3, "medium", "gasoline", 34.19, id="all optional arguments"), - pytest.param(10, 1, "small", None, 1.79, id="size: 'small'"), - pytest.param(10, 1, "medium", None, 2.09, id="size: 'medium'"), - pytest.param(10, 1, "large", None, 2.74, id="size: 'large'"), - pytest.param(10, 1, "average", None, 2.15, id="size: 'average'"), - pytest.param(10, 1, None, "diesel", 2.01, id="fuel_type: 'diesel'"), - pytest.param(10, 1, None, "gasoline", 2.24, id="fuel_type: 'gasoline'"), - pytest.param(10, 1, None, "cng", 2.37, id="fuel_type: 'cng'"), - pytest.param(10, 1, None, "electric", 0.51, id="fuel_type: 'electric'"), - pytest.param(10, 1, None, "hybrid", 1.2, id="fuel_type: 'hybrid'"), - pytest.param( - 10, 1, None, "plug-in_hybrid", 0.93, id="fuel_type: 'plug-in_hybrid'" - ), - pytest.param(10, 1, None, "average", 2.15, id="fuel_type: 'average'"), - ], -) -def test_calc_co2_car( - distance: float, - passengers: Optional[int], - size: Optional[str], - fuel_type: Optional[str], - expected_emissions: float, -): - """Test: Calculate car-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - actual_emissions = candidate.calc_co2_car( - distance=distance, - passengers=passengers, - size=size, - fuel_type=fuel_type, - ) - - assert round(actual_emissions, 2) == expected_emissions - - -@pytest.mark.parametrize( - "distance,size,expected_emissions", - [ - pytest.param(100, None, 11.36, id="defaults"), - pytest.param(100, "small", 8.31, id="size: 'small'"), - pytest.param(100, "medium", 10.09, id="size: 'medium'"), - pytest.param(100, "large", 13.24, id="size: 'large'"), - pytest.param(100, "average", 11.36, id="size: 'average'"), - ], -) -def test_calc_co2_motorbike( - distance: float, size: Optional[str], expected_emissions: float -): - """Test: Calculate motorbike-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - actual_emissions = candidate.calc_co2_motorbike(distance=distance, size=size) - - assert round(actual_emissions, 2) == expected_emissions - - -@pytest.mark.parametrize( - "distance,size,fuel_type,occupancy,vehicle_range,expected_emissions", - [ - pytest.param(549, None, None, None, None, 21.63, id="defaults"), - pytest.param( - 549, "large", "diesel", 80, "long-distance", 12.3, id="optional arguments" - ), - pytest.param(10, "medium", None, None, None, 0.42, id="size: 'medium'"), - pytest.param(10, "large", None, None, None, 0.33, id="size: 'large'"), - pytest.param(10, "average", None, None, None, 0.39, id="size: 'average'"), - pytest.param(10, None, None, 20, None, 0.92, id="occupancy: 20"), - pytest.param(10, None, None, 50, None, 0.39, id="occupancy: 50"), - pytest.param(10, None, None, 80, None, 0.26, id="occupancy: 80"), - pytest.param(10, None, None, 100, None, 0.22, id="occupancy: 100"), - pytest.param(10, None, None, None, "local", 0.39, id="vehicle_range: 'local'"), - pytest.param( - 10, - None, - None, - None, - "long-distance", - 0.39, - id="vehicle_range: 'long-distance'", - ), - # pytest.param( - # 10, - # "small", - # "diesel", - # None, - # "long-distance", - # 0.39, - # id="size: 'small', fuel_type: `diesel`, vehicle_range: 'long-distance'", - # ), - # pytest.param( - # 10, - # "medium", - # "cng", - # None, - # "long-distance", - # 0.62, - # id="fuel_type: `cng` and size", - # ), - # pytest.param( - # 10, - # "small", - # "hydrogen", - # None, - # "local", - # 0.25, - # id="fuel_type: `hydrogen` and size", - # ), - ], -) -def test_calc_co2_bus( - distance: float, - size: Optional[str], - fuel_type: Optional[str], - occupancy: Optional[int], - vehicle_range: Optional[str], - expected_emissions: float, -): - """Test: Calculate bus-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - - # Calculate co2e - actual_emissions = candidate.calc_co2_bus( - distance=distance, - size=size, - fuel_type=fuel_type, - occupancy=occupancy, - vehicle_range=vehicle_range, - ) - - assert round(actual_emissions, 2) == expected_emissions - - -@pytest.mark.parametrize( - "distance,fuel_type,vehicle_range,expected_emissions", - [ - pytest.param(1162, None, None, 38.23, id="defaults"), - pytest.param( - 1162, "electric", "long-distance", 37.18, id="all optional arguments" - ), - pytest.param(10, "electric", None, 0.32, id="fuel_type: 'electric'"), - pytest.param(10, "diesel", None, 0.7, id="fuel_type: 'diesel'"), - pytest.param(10, "average", None, 0.33, id="fuel_type: 'average'"), - # pytest.param(10, None, "local", 0.6, id="vehicle_range: 'local'"), - pytest.param( - 10, None, "long-distance", 0.33, id="vehicle_range: 'long-distance'" - ), - ], -) -def test_calc_co2_train( - distance: float, - fuel_type: Optional[str], - vehicle_range: Optional[str], - expected_emissions: float, -): - """Test: Calculate train-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - - actual_emissions = candidate.calc_co2_train( - distance=distance, fuel_type=fuel_type, vehicle_range=vehicle_range - ) - - assert round(actual_emissions, 2) == expected_emissions - - -@pytest.mark.parametrize( - "distance,seating_class,expected_emissions", - [ - pytest.param(1000, None, 153.53, id="defaults, short-haul"), - pytest.param(2000, None, 307.06, id="defaults, long-haul"), - pytest.param(1000, "economy_class", 151.52, id="seating_class"), - ], -) -def test_calc_co2_plane( - distance: float, - seating_class: Optional[str], - expected_emissions: float, -): - """Test: Calculate plane-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - - actual_emissions = candidate.calc_co2_plane( - distance=distance, seating=seating_class - ) - - assert round(actual_emissions, 2) == expected_emissions - - -def test_calc_co2_plane__failed() -> None: - """Test: Calculation on plane-trip emissions fails due to false input. - Expect: Raises ValidationError. - """ - with pytest.raises(ValidationError): - candidate.calc_co2_plane(distance=5000, seating="NON-EXISTENT") - - -def test_calc_co2_plane__invalid_distance_seating_combo() -> None: - """Test: Calculation on plane-trip emissions fails due to false input. - Expect: Raises ValueError. - """ - # Check if raises warning (premium economy class is not available for short-haul flights) - with pytest.raises(EmissionFactorNotFound): - candidate.calc_co2_plane(distance=800, seating="premium_economy_class") - - -@pytest.mark.parametrize( - "seating_class,expected_emissions", - [ - pytest.param(None, 11.29, id="defaults"), - pytest.param("average", 11.29, id="seating_class: 'average'"), - pytest.param("foot_passenger", 1.87, id="seating_class: 'Foot passenger'"), - pytest.param("car_passenger", 12.95, id="seating_class: 'Car passenger"), - ], -) -def test_calc_ferry(seating_class: Optional[str], expected_emissions: float) -> None: - """Test: Calculate ferry-trip emissions based on given distance. - Expect: Returns emissions and distance. - """ - actual_emissions = candidate.calc_co2_ferry(distance=100, seating=seating_class) - assert round(actual_emissions, 2) == expected_emissions # @pytest.mark.skip( @@ -283,10 +51,8 @@ def test_electricity(): @pytest.mark.parametrize( "transportation_mode,weekly_distance,size,fuel_type,occupancy,passengers,expected", [ - pytest.param( - "car", 30, "medium", "gasoline", None, None, 6.93, id="car commute" - ), - pytest.param("bicycle", 60, None, None, None, None, 0.54, id="bicycle commute"), + pytest.param("car", 30, "medium", "gasoline", None, 1, 6.45, id="car commute"), + pytest.param("bicycle", 60, None, None, None, 1, 0.54, id="bicycle commute"), ], ) def test_commuting( diff --git a/tests/unit/test_calculate_mobility.py b/tests/unit/test_calculate_mobility.py new file mode 100644 index 0000000..d3fb08c --- /dev/null +++ b/tests/unit/test_calculate_mobility.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for co2calculator.calculate module""" + +from typing import Optional + +from pydantic import ValidationError +import pytest +from pytest_mock import MockerFixture + +import co2calculator.mobility.calculate_mobility as mobility + +from co2calculator.exceptions import ConversionFactorNotFound, EmissionFactorNotFound + + +@pytest.mark.parametrize( + "distance,options,expected_emissions", + [ + pytest.param(100, None, 21.5, id="defaults"), + pytest.param( + 444, + {"passengers": 3, "size": "medium", "fuel_type": "gasoline"}, + 34.19, + id="all optional arguments", + ), + pytest.param( + 10, {"passengers": 1, "size": "small"}, 1.79, id="only 2 arguments'" + ), + pytest.param(10, {"passengers": 1}, 2.15, id="only passengers'"), + pytest.param(10, {}, 2.15, id="empty options'"), + ], +) +def test_calc_co2_car(distance: float, options: dict, expected_emissions: float): + """Test: Calculate car-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + actual_emissions = mobility.calc_co2_car(distance=distance, options=options) + + assert round(actual_emissions, 2) == expected_emissions + + +@pytest.mark.parametrize( + "distance,options,expected_emissions", + [ + pytest.param(100, None, 11.36, id="defaults"), + pytest.param(100, {"size": "small"}, 8.31, id="size: 'small'"), + pytest.param(100, {}, 11.36, id="size: 'medium'"), + ], +) +def test_calc_co2_motorbike(distance: float, options: dict, expected_emissions: float): + """Test: Calculate motorbike-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + actual_emissions = mobility.calc_co2_motorbike(distance=distance, options=options) + + assert round(actual_emissions, 2) == expected_emissions + + +@pytest.mark.parametrize( + "distance,options,expected_emissions", + [ + pytest.param(549, None, 21.63, id="defaults"), + pytest.param( + 549, + { + "size": "large", + "fuel_type": "diesel", + "occupancy": 80, + "vehicle_range": "long-distance", + }, + 12.3, + id="all options", + ), + pytest.param(10, {"size": "medium"}, 0.42, id="size: 'medium'"), + pytest.param(10, {"occupancy": 20}, 0.92, id="occupancy: 20"), + pytest.param(10, {"vehicle_range": "local"}, 0.39, id="local range"), + pytest.param( + 549, + {}, + 21.63, + id="empty options", + ), + ], +) +def test_calc_co2_bus( + distance: float, + options: dict, + expected_emissions: float, +): + """Test: Calculate bus-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + + # Calculate co2e + actual_emissions = mobility.calc_co2_bus( + distance=distance, + options=options, + ) + + assert round(actual_emissions, 2) == expected_emissions + + +@pytest.mark.parametrize( + "distance,options,expected_emissions", + [ + pytest.param(1162, None, 38.23, id="defaults"), + pytest.param(1162, {}, 38.23, id="defaults on empty"), + pytest.param( + 1162, + {"fuel_type": "electric", "vehicle_range": "long-distance"}, + 37.18, + id="all optional arguments", + ), + pytest.param(10, {"fuel_type": "diesel"}, 0.7, id="fuel_type: 'electric'"), + ], +) +def test_calc_co2_train( + distance: float, + options: dict, + expected_emissions: float, +): + """Test: Calculate train-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + + actual_emissions = mobility.calc_co2_train(distance=distance, options=options) + + assert round(actual_emissions, 2) == expected_emissions + + +@pytest.mark.parametrize( + "distance,options,expected_emissions", + [ + pytest.param(1000, None, 153.53, id="defaults on none, short-haul"), + pytest.param(2000, {}, 307.06, id="defaults on empty, long-haul"), + pytest.param(1000, {"seating": "economy_class"}, 151.52, id="seating_class"), + ], +) +def test_calc_co2_plane( + distance: float, + options: dict, + expected_emissions: float, +): + """Test: Calculate plane-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + + actual_emissions = mobility.calc_co2_plane(distance=distance, options=options) + + assert round(actual_emissions, 2) == expected_emissions + + +def test_calc_co2_plane__failed() -> None: + """Test: Calculation on plane-trip emissions fails due to false input. + Expect: Raises ValidationError. + """ + with pytest.raises(ValidationError): + mobility.calc_co2_plane(distance=5000, options={"seating": "NON-EXISTENT"}) + + +def test_calc_co2_plane__invalid_distance_seating_combo() -> None: + """Test: Calculation on plane-trip emissions fails due to false input. + Expect: Raises ValueError. + """ + # Check if raises warning (premium economy class is not available for short-haul flights) + with pytest.raises(EmissionFactorNotFound): + mobility.calc_co2_plane( + distance=800, options={"seating": "premium_economy_class"} + ) + + +@pytest.mark.parametrize( + "options,expected_emissions", + [ + pytest.param(None, 11.29, id="defaults"), + pytest.param({}, 11.29, id="defaults on empty"), + pytest.param({"seating": "average"}, 11.29, id="seating_class: 'average'"), + pytest.param( + {"seating": "foot_passenger"}, 1.87, id="seating_class: 'Foot passenger'" + ), + ], +) +def test_calc_ferry(options: dict, expected_emissions: float) -> None: + """Test: Calculate ferry-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + actual_emissions = mobility.calc_co2_ferry(distance=100, options=options) + assert round(actual_emissions, 2) == expected_emissions