import XCTest #if SWIFT_PACKAGE @testable import FuelBoardShared #endif /// Station-level price age — one timestamp per station, reported in More info /// and nowhere else. /// /// The relay's `price_updated` is the newest `price_last_updated` across every /// grade a station maps, so it is not evidence about the selected grade. These /// tests pin what the app *reports*; `StationPriceAgePolicyTests` pins what the /// app does about it (nothing). final class FuelPriceFreshnessTests: XCTestCase { private let now = Date(timeIntervalSince1970: 1_800_000_000) private func at(_ offset: TimeInterval) -> TimeInterval { now.addingTimeInterval(offset).timeIntervalSince1970 } private func station(priceUpdated: TimeInterval?) -> FuelStation { FuelStation(id: "freshness-station", name: "Freshness Station", brand: "Test", address: "1 High Street", postcode: "BD1 1AA", lat: 53.79, lng: -1.75, prices: [.e10: 169.9], priceUpdated: priceUpdated) } // MARK: - One figure per station, whatever fuel is on screen func testOneAgeIsReportedForEveryFuelAStationSells() { let sold = FuelStation(id: "multi", name: "Multi", brand: "", address: "", postcode: "", lat: 53, lng: -1, prices: [.e5: 165.9, .e10: 169.9, .diesel: 129.9], priceUpdated: at(-28 * 60 * 60)) for fuel in FuelType.allCases { XCTAssertEqual(FuelPriceFreshness.assessment(for: sold, now: now).updateAgeLabel, "28h ago", "\(fuel.rawValue): every grade reads the same station timestamp") } } func testStationTimestampIsDecodedAndNoPerGradeTimestampIsInvented() 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-10-14T07:45:52Z", "price_updated_by_grade": { "E10": "2026-01-01T00:00:00Z", "DIESEL": "2026-01-01T00:00:00Z" } }] } """ let decoded = try XCTUnwrap(try FuelPriceProvider.decodeStations(from: Data(json.utf8)).first) XCTAssertEqual(decoded.priceUpdated, ISO8601DateFormatter.fuelBoardDate("2026-10-14T07:45:52Z")?.timeIntervalSince1970, "the station-level field is what the app records") // A per-grade map in the payload is ignored: the app has one age, and it // is the station's. let assessment = FuelPriceFreshness.assessment(for: decoded, now: now) XCTAssertEqual(assessment.status, .outdated, "the station field is used, not the 2026-01-01 grade entries") } // MARK: - Reading the age func testFreshAgeReadsInMinutesThenHours() { XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-59 * 60), now: now).updateAgeLabel, "59m ago") XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-60 * 60), now: now).updateAgeLabel, "1h ago") XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-47 * 60 * 60), now: now).updateAgeLabel, "47h ago") XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-48 * 60 * 60), now: now).updateAgeLabel, "2d ago") } func testFreshInformationIsNeverFlagged() { let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: at(-28 * 60 * 60)), now: now) XCTAssertEqual(assessment.status, .fresh) XCTAssertFalse(assessment.isOutdated) XCTAssertNil(assessment.outdatedNotice) XCTAssertEqual(assessment.age, 28 * 60 * 60) } // MARK: - The thirty-day display threshold func testInformationExactlyThirtyDaysOldIsNotYetFlagged() { let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-FuelPriceFreshness.outdatedAge), now: now) XCTAssertEqual(assessment.status, .fresh) XCTAssertNil(assessment.outdatedNotice) } func testInformationJustPastThirtyDaysIsFlaggedButStillReportsItsAge() { let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-FuelPriceFreshness.outdatedAge - 1), now: now) XCTAssertEqual(assessment.status, .outdated) XCTAssertTrue(assessment.isOutdated) XCTAssertEqual(assessment.outdatedNotice, "May be outdated") XCTAssertEqual(assessment.updateAgeLabel, "30d ago") } func testLongOldInformationIsFlaggedWithItsDayCount() { // The oldest figure the mirror carries for a real station is ~250 days. let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-250 * 86_400), now: now) XCTAssertEqual(assessment.status, .outdated) XCTAssertEqual(assessment.updateAgeLabel, "250d ago") XCTAssertEqual(assessment.outdatedNotice, "May be outdated") } // MARK: - Missing, malformed, future and absurd timestamps func testMissingTimestampReadsNotProvidedAndIsNeverFlagged() { let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: nil), now: now) XCTAssertEqual(assessment.status, .unknown) XCTAssertNil(assessment.age) XCTAssertEqual(assessment.updateAgeLabel, "Not provided") XCTAssertFalse(assessment.isOutdated) XCTAssertNil(assessment.outdatedNotice) } func testUnparseableSourceTextIsDroppedRatherThanGuessed() throws { XCTAssertNil(FuelPriceTimestamp.parse("not-a-date")) XCTAssertNil(FuelPriceTimestamp.parse("")) XCTAssertNil(FuelPriceTimestamp.parse(" ")) let json = """ {"stations": [{"id": "bad", "name": "Bad", "brand": "", "address": "", "postcode": "", "lat": 53, "lng": -1, "prices": {"E10": 169.9}, "price_updated": "2026-99-99T99:99:99Z"}]} """ let decoded = try XCTUnwrap(try FuelPriceProvider.decodeStations(from: Data(json.utf8)).first) XCTAssertNil(decoded.priceUpdated) XCTAssertEqual(FuelPriceFreshness.assessment(for: decoded, now: now).updateAgeLabel, "Not provided") } func testSmallClockSkewReadsAsJustNowNotAsAFutureDate() { let assessment = FuelPriceFreshness.assessment(priceUpdated: at(60), now: now) XCTAssertEqual(assessment.status, .fresh) XCTAssertEqual(assessment.age, 0) XCTAssertEqual(assessment.updateAgeLabel, "0m ago") } func testTimestampBeyondTheSkewToleranceIsUnusableNotOptimistic() { let assessment = FuelPriceFreshness.assessment(priceUpdated: at(86_400), now: now) XCTAssertEqual(assessment.status, .invalid) XCTAssertNil(assessment.age) XCTAssertEqual(assessment.updateAgeLabel, "Not provided") XCTAssertNil(assessment.outdatedNotice) } func testNonFiniteAndAbsurdEpochsAreUnusableAndCannotTrap() { let values: [TimeInterval] = [.nan, .infinity, -.infinity, .greatestFiniteMagnitude, -.greatestFiniteMagnitude] for value in values { let assessment = FuelPriceFreshness.assessment(priceUpdated: value, now: now) XCTAssertEqual(assessment.status, .invalid, "\(value)") XCTAssertNil(assessment.age, "\(value)") XCTAssertEqual(assessment.updateAgeLabel, "Not provided", "\(value)") XCTAssertFalse(assessment.isOutdated, "\(value)") } } } 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) }() } }