import XCTest #if SWIFT_PACKAGE @testable import FuelBoardShared #endif final class FuelSavingsCalculatorTests: XCTestCase { func testUKMPGConversionUsesImperialGallons() { XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 10, mpg: 45), 1.0102, accuracy: 0.0001) } func testPoundsBudgetConvertsToLitresUsingBaselinePrice() throws { let amount = FuelSavingsFillAmount(value: 40, unit: .pounds) let litres = try FuelSavingsCalculator.fillLitres(amount: amount, baselinePricePencePerLitre: 160) XCTAssertEqual(litres, 25.0, accuracy: 0.0001) } func testLitresAmountPassesThrough() throws { let amount = FuelSavingsFillAmount(value: 35, unit: .litres) let litres = try FuelSavingsCalculator.fillLitres(amount: amount, baselinePricePencePerLitre: 160) XCTAssertEqual(litres, 35, accuracy: 0.0001) } func testNetSavingSubtractsAdditionalDrivingFuelCost() throws { let result = try FuelSavingsCalculator.calculate( mpg: 45, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 2, candidateJourneyMiles: 8 ) XCTAssertEqual(result.fillLitres, 25, accuracy: 0.0001) XCTAssertEqual(result.grossSavingGBP, 2.50, accuracy: 0.0001) XCTAssertEqual(result.extraDistanceMiles, 6, accuracy: 0.0001) XCTAssertEqual(result.extraDrivingCostGBP, 0.9092, accuracy: 0.0001) XCTAssertEqual(result.netSavingGBP, 1.5908, accuracy: 0.0001) XCTAssertEqual(result.status, .worthTheDrive) } func testNegativeAndBreakEvenOutcomes() throws { let negative = try FuelSavingsCalculator.calculate( mpg: 45, amount: FuelSavingsFillAmount(value: 10, unit: .litres), baselinePricePencePerLitre: 150, candidatePricePencePerLitre: 149, baselineJourneyMiles: 1, candidateJourneyMiles: 20 ) XCTAssertEqual(negative.status, .notWorthTheDrive) XCTAssertLessThan(negative.netSavingGBP, 0) let even = try FuelSavingsCalculator.calculate( mpg: 45, amount: FuelSavingsFillAmount(value: 10, unit: .litres), baselinePricePencePerLitre: 150, candidatePricePencePerLitre: 150, baselineJourneyMiles: 2, candidateJourneyMiles: 2 ) XCTAssertEqual(even.status, .breakEven) } func testInvalidInputsRejected() { XCTAssertThrowsError(try FuelSavingsCalculator.fillLitres(amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 0)) XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 0, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2)) XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 500, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2)) } func testSwitchingPoundsToLitresPreservesPhysicalQuantity() throws { let pounds = FuelSavingsFillAmount(value: 40, unit: .pounds) let litres = try FuelSavingsCalculator.convert(amount: pounds, to: .litres, referencePricePencePerLitre: 160) XCTAssertEqual(litres.unit, .litres) XCTAssertEqual(litres.value, 25, accuracy: 0.0001) let back = try FuelSavingsCalculator.convert(amount: litres, to: .pounds, referencePricePencePerLitre: 160) XCTAssertEqual(back.value, 40, accuracy: 0.0001) } func testSortingPlacesUnavailableAfterSavingsAndBreaksTies() { let a = FuelSavingsRankedStation(stationID: "a", result: .available(.init(fillLitres: 25, grossSavingGBP: 2, extraDrivingCostGBP: 1, extraDistanceMiles: 3, netSavingGBP: 1, status: .worthTheDrive)), distanceMiles: 5, pricePencePerLitre: 150) let b = FuelSavingsRankedStation(stationID: "b", result: .available(.init(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)), distanceMiles: 8, pricePencePerLitre: 149) let c = FuelSavingsRankedStation(stationID: "c", result: .unavailable("No route"), distanceMiles: 1, pricePencePerLitre: 140) XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([a, c, b]).map(\.stationID), ["b", "a", "c"]) } func testBestValueAlertCandidateRequiresConfiguredSavingsAndThreshold() { let stations = [ station("near", lat: 0, lng: 0, price: 160), station("cheap", lat: 0, lng: 0, price: 150), ] let journeys = ["near": 2.0, "cheap": 4.0] let off = FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)) XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate( stations: stations, fuel: .e10, settings: off, amount: off.preferredAmount, minimumNetSavingGBP: 1.0, journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } } )) let on = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)) XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate( stations: stations, fuel: .e10, settings: on, amount: on.preferredAmount, minimumNetSavingGBP: 5.0, journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } } )) } func testBestValueAlertCandidatePicksHighestNetSavingAboveThreshold() throws { let stations = [ station("baseline", lat: 0, lng: 0, price: 160), station("small", lat: 0, lng: 0, price: 154), station("best", lat: 0, lng: 0, price: 148), ] let journeys = ["baseline": 2.0, "small": 4.0, "best": 6.0] let settings = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)) let candidate = FuelSavingsCalculator.bestValueAlertCandidate( stations: stations, fuel: .e10, settings: settings, amount: settings.preferredAmount, minimumNetSavingGBP: 1.0, journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } } ) XCTAssertEqual(candidate?.station.id, "best") XCTAssertEqual(candidate?.result.status, .worthTheDrive) XCTAssertGreaterThan(candidate?.result.netSavingGBP ?? 0, 1.0) } func testBestValueAlertCandidateBreaksTiesByDistanceThenPrice() throws { let first = FuelSavingsResult(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive) let nearer = FuelSavingsRankedStation(stationID: "nearer", result: .available(first), distanceMiles: 4, pricePencePerLitre: 149) let farther = FuelSavingsRankedStation(stationID: "farther", result: .available(first), distanceMiles: 8, pricePencePerLitre: 145) XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([farther, nearer]).first?.stationID, "nearer") } private func station(_ id: String, lat: Double, lng: Double, price: Double) -> FuelStation { FuelStation(id: id, name: id, brand: id, address: "", postcode: "", lat: lat, lng: lng, prices: [.e10: price], priceUpdated: nil) } } final class FuelSavingsSettingsTests: XCTestCase { func testSettingsValidationRequiresEnabledMPGAndFillAmount() { XCTAssertFalse(FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured) XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 0, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured) XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 0, unit: .pounds)).isConfigured) XCTAssertTrue(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured) } }