import XCTest #if SWIFT_PACKAGE @testable import FuelBoardShared #endif final class FuelPriceFreshnessTests: XCTestCase { private let now = Date(timeIntervalSince1970: 1_792_022_400) private func station( priceUpdated: TimeInterval? = nil, priceUpdatedByFuel: [FuelType: TimeInterval]? = nil ) -> FuelStation { FuelStation( id: "freshness-station", name: "Freshness Station", brand: "Test", address: "1 High Street", postcode: "BD1 1AA", lat: 53.79, lng: -1.75, prices: [.diesel: 129.9, .e10: 169.9], priceUpdated: priceUpdated, priceUpdatedByFuel: priceUpdatedByFuel ) } func testPerGradeTimestampIsDecodedAndEligibleWhenFresh() throws { let json = """ { "stations": [{ "id": "s1", "name": "LISTER ARMS", "brand": "Shell", "address": "MANCHESTER ROAD, BRADFORD", "postcode": "BD5 7JX", "lat": 53.78, "lng": -1.75, "prices": {"E10": 169.9, "DIESEL": 129.9}, "price_updated": "2026-09-25T07:45:52Z", "price_updated_by_grade": { "E10": "2026-10-14T07:45:52Z", "DIESEL": "2026-10-14T07:45:52Z" } }] } """ let stations = try FuelPriceProvider.decodeStations(from: Data(json.utf8)) let decoded = try XCTUnwrap(stations.first) XCTAssertEqual(decoded.priceUpdatedByFuel?[.diesel], ISO8601DateFormatter.fuelBoardDate("2026-10-14T07:45:52Z")?.timeIntervalSince1970) let assessment = FuelPriceFreshness.assessment(for: decoded, fuel: .diesel, now: now) XCTAssertEqual(assessment.source, .gradeSpecific) XCTAssertEqual(assessment.status, .fresh) XCTAssertTrue(assessment.isEligibleForBestValue) } func testStationWideTimestampIsNotPresentedAsGradeSpecificOrEligible() { let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge / 2).timeIntervalSince1970 let assessment = FuelPriceFreshness.assessment( for: station(priceUpdated: updated), fuel: .diesel, now: now ) XCTAssertEqual(assessment.source, .stationWide) XCTAssertEqual(assessment.status, .fresh) XCTAssertEqual(assessment.confidence, .provisional) XCTAssertTrue(assessment.recommendationEligibility == .eligible) XCTAssertTrue(assessment.isEligibleForBestValue) XCTAssertTrue(assessment.warning) XCTAssertTrue(assessment.displayLabel.contains("station-wide")) } func testRecentStationReportExplainsSelectedFuelTimeIsMissing() { let updated = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970 let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .e10, now: now) XCTAssertEqual(assessment.stationReportAgeLabel, "28h ago") XCTAssertEqual(assessment.selectedFuelUpdateLabel, "Not provided") XCTAssertEqual(assessment.source, .stationWide) XCTAssertEqual(assessment.status, .fresh) XCTAssertEqual(FuelPriceFreshness.comparisonWarning( station: station(priceUpdated: updated), fuel: .e10, baseline: station(priceUpdated: updated), now: now), "Estimated saving · Unleaded price not individually dated") } func testOlderFuelSpecificTimestampDoesNotClaimUndatedPrice() { let updated = now.addingTimeInterval(-10 * 86_400).timeIntervalSince1970 let priced = station(priceUpdatedByFuel: [.diesel: updated]) let assessment = FuelPriceFreshness.assessment(for: priced, fuel: .diesel, now: now) XCTAssertNil(assessment.stationReportAgeLabel) XCTAssertEqual(assessment.selectedFuelUpdateLabel, "10d ago") XCTAssertEqual(FuelPriceFreshness.comparisonWarning( station: priced, fuel: .diesel, baseline: priced, now: now), "Estimated saving · Diesel price over 7 days old") } func testProvisionalWarningNamesBaselineWhenCandidateHasFuelSpecificTimestamp() { let recent = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970 let candidate = station(priceUpdatedByFuel: [.e10: recent]) let baseline = station(priceUpdated: recent) XCTAssertEqual(FuelPriceFreshness.comparisonWarning( station: candidate, fuel: .e10, baseline: baseline, now: now), "Estimated saving · closest eligible station's Unleaded price not individually dated") } func testStationWideTimestampAtWarningBoundaryChangesRecommendationEligibility() { let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge).timeIntervalSince1970 let onBoundary = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .diesel, now: now) XCTAssertEqual(onBoundary.status, .fresh) XCTAssertTrue(onBoundary.isEligibleForBestValue) let pastBoundary = FuelPriceFreshness.assessment( for: station(priceUpdated: updated - 1), fuel: .diesel, now: now) XCTAssertEqual(pastBoundary.status, .warning) XCTAssertFalse(pastBoundary.isEligibleForBestValue) } func testStationWideTimestampOlderThanWarningThresholdCannotRecommendSuspiciousDiesel() { let now = Date(timeIntervalSince1970: 1_800_000_000) let shell = station(priceUpdated: now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970) let age = FuelPriceFreshness.assessment(for: shell, fuel: .diesel, now: now) XCTAssertEqual(age.status, .warning) XCTAssertFalse(age.isEligibleForBestValue) XCTAssertEqual(shell.prices[.diesel], 129.9) } func testComparisonConfidenceRejectsMissingSelectedFuelPriceEvenWithFreshTimestamp() { let now = Date(timeIntervalSince1970: 1_800_000_000) let updated = now.addingTimeInterval(-86_400).timeIntervalSince1970 let dieselOnly = FuelStation(id: "diesel", name: "Diesel", brand: "", address: "", postcode: "", lat: 53, lng: -1, prices: [.diesel: 129.9], priceUpdated: nil, priceUpdatedByFuel: [.e10: updated]) let e10 = FuelStation(id: "petrol", name: "Petrol", brand: "", address: "", postcode: "", lat: 53, lng: -1, prices: [.e10: 169.9], priceUpdated: nil, priceUpdatedByFuel: [.e10: updated]) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: dieselOnly, fuel: .e10, baseline: e10, now: now), .ineligible) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: e10, fuel: .e10, baseline: dieselOnly, now: now), .ineligible) } func testComparisonConfidenceRequiresBothSelectedGradeTimestamps() { let now = Date(timeIntervalSince1970: 1_800_000_000) let recent = now.addingTimeInterval(-86_400).timeIntervalSince1970 let old = now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970 let verified = station(priceUpdatedByFuel: [.diesel: recent]) let provisional = station(priceUpdated: recent) let oldStationWide = station(priceUpdated: old) let oldGrade = station(priceUpdatedByFuel: [.diesel: old]) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel, baseline: verified, now: now), .verified) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel, baseline: provisional, now: now), .provisional) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: oldGrade, fuel: .diesel, baseline: verified, now: now), .provisional) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel, baseline: oldStationWide, now: now), .ineligible) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: provisional, fuel: .diesel, baseline: verified, now: now), .provisional) XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .e10, baseline: verified, now: now), .ineligible) } func testNonFiniteAndExtremeEpochsCannotBecomeTrusted() { let now = Date(timeIntervalSince1970: 1_800_000_000) for value in [Double.nan, .infinity, -.infinity, Double.greatestFiniteMagnitude, -Double.greatestFiniteMagnitude] { let assessment = FuelPriceFreshness.assessment( priceUpdated: value, priceUpdatedByFuel: nil, fuel: .diesel, now: now) XCTAssertFalse(assessment.isEligibleForBestValue) XCTAssertTrue(assessment.warning) } } func testMissingAndMalformedTimestampsAreUnknownAndIneligible() { let missing = FuelPriceFreshness.assessment(for: station(), fuel: .diesel, now: now) XCTAssertEqual(missing.status, .unknown) XCTAssertEqual(missing.source, .unknown) XCTAssertFalse(missing.isEligibleForBestValue) let malformed = FuelPriceFreshness.assessment( priceUpdated: "not-a-date", priceUpdatedByGrade: nil, fuel: .diesel, now: now ) XCTAssertEqual(malformed.status, .invalid) XCTAssertFalse(malformed.isEligibleForBestValue) } func testStaleDisplayLabelDoesNotCallIneligiblePriceProvisional() { let stale = station(priceUpdatedByFuel: [ .diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970 ]) let assessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now) XCTAssertFalse(assessment.isEligibleForBestValue) XCTAssertFalse(assessment.displayLabel.contains("provisional")) XCTAssertTrue(assessment.displayLabel.contains("outdated")) } func testStaleAndFutureGradeTimestampsAreRejected() { let stale = station(priceUpdatedByFuel: [ .diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970 ]) let staleAssessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now) XCTAssertEqual(staleAssessment.status, .stale) XCTAssertFalse(staleAssessment.isEligibleForBestValue) XCTAssertTrue(staleAssessment.warning) let future = station(priceUpdatedByFuel: [ .diesel: now.addingTimeInterval(FuelPriceFreshness.futureTolerance + 1).timeIntervalSince1970 ]) let futureAssessment = FuelPriceFreshness.assessment(for: future, fuel: .diesel, now: now) XCTAssertEqual(futureAssessment.status, .invalid) XCTAssertFalse(futureAssessment.isEligibleForBestValue) } func testMalformedAndFutureSourceValuesDoNotCrashOrProduceNegativeAge() { let malformed = FuelPriceFreshness.assessment( priceUpdated: "2026-99-99T99:99:99Z", priceUpdatedByGrade: ["DIESEL": "2026-99-99T99:99:99Z"], fuel: .diesel, now: now ) XCTAssertNil(malformed.age) XCTAssertFalse(malformed.isEligibleForBestValue) let future = FuelPriceFreshness.assessment( priceUpdated: now.addingTimeInterval(86_400).timeIntervalSince1970, priceUpdatedByFuel: nil, fuel: .diesel, now: now ) XCTAssertNil(future.age) XCTAssertFalse(future.isEligibleForBestValue) } } private extension ISO8601DateFormatter { static func fuelBoardDate(_ string: String) -> Date? { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.date(from: string) ?? { formatter.formatOptions = [.withInternetDateTime] return formatter.date(from: string) }() } }