Files
Sportstime/SportsTimeTests/Services/RouteDescriptionGeneratorTests.swift
2026-02-18 13:00:15 -06:00

257 lines
8.1 KiB
Swift

//
// RouteDescriptionGeneratorTests.swift
// SportsTimeTests
//
// TDD specification tests for RouteDescriptionGenerator types.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
// MARK: - RouteDescriptionInput Tests
@Suite("RouteDescriptionInput")
struct RouteDescriptionInputTests {
// MARK: - Test Data
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
private func makeOption(
stops: [ItineraryStop] = [],
totalDrivingHours: Double = 8.5,
totalDistanceMiles: Double = 500
) -> ItineraryOption {
ItineraryOption(
rank: 1,
stops: stops,
travelSegments: [],
totalDrivingHours: totalDrivingHours,
totalDistanceMiles: totalDistanceMiles,
geographicRationale: "Test rationale"
)
}
private func makeStop(city: String, games: [String] = []) -> ItineraryStop {
ItineraryStop(
city: city,
state: "XX",
coordinate: nycCoord,
games: games,
arrivalDate: TestClock.now,
departureDate: TestClock.now.addingTimeInterval(86400),
location: LocationInput(name: city, coordinate: nycCoord),
firstGameStart: nil
)
}
private func makeRichGame(id: String, sport: Sport = .mlb) -> RichGame {
let game = Game(
id: id,
homeTeamId: "team1",
awayTeamId: "team2",
stadiumId: "stadium1",
dateTime: TestClock.now,
sport: sport,
season: "2026",
isPlayoff: false
)
let team = Team(
id: "team1",
name: "Test Team",
abbreviation: "TST",
sport: sport,
city: "Test City",
stadiumId: "stadium1"
)
let stadium = Stadium(
id: "stadium1",
name: "Test Stadium",
city: "Test City",
state: "XX",
latitude: nycCoord.latitude,
longitude: nycCoord.longitude,
capacity: 40000,
sport: sport
)
return RichGame(game: game, homeTeam: team, awayTeam: team, stadium: stadium)
}
// MARK: - Specification Tests: init(from:games:)
@Test("init: extracts cities from option stops")
func init_extractsCities() {
let stops = [
makeStop(city: "New York"),
makeStop(city: "Boston"),
makeStop(city: "Philadelphia")
]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.cities == ["New York", "Boston", "Philadelphia"])
}
@Test("init: deduplicates cities preserving order")
func init_deduplicatesCities() {
let stops = [
makeStop(city: "New York"),
makeStop(city: "Boston"),
makeStop(city: "New York") // Duplicate
]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
// NSOrderedSet preserves first occurrence order and removes duplicates
#expect(input.cities == ["New York", "Boston"])
}
@Test("init: extracts sports from games")
func init_extractsSports() {
let stops = [makeStop(city: "New York", games: ["game1", "game2"])]
let option = makeOption(stops: stops)
let games = [
"game1": makeRichGame(id: "game1", sport: .mlb),
"game2": makeRichGame(id: "game2", sport: .nba)
]
let input = RouteDescriptionInput(from: option, games: games)
#expect(input.sports.count == 2)
#expect(input.sports.contains("MLB")) // rawValue is uppercase
#expect(input.sports.contains("NBA")) // rawValue is uppercase
}
@Test("init: computes totalGames from option")
func init_computesTotalGames() {
let stops = [
makeStop(city: "New York", games: ["g1", "g2"]),
makeStop(city: "Boston", games: ["g3"])
]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.totalGames == 3)
}
@Test("init: copies totalMiles from option")
func init_copiesTotalMiles() {
let option = makeOption(totalDistanceMiles: 1234.5)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.totalMiles == 1234.5)
}
@Test("init: copies totalDrivingHours from option")
func init_copiesTotalDrivingHours() {
let option = makeOption(totalDrivingHours: 15.75)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.totalDrivingHours == 15.75)
}
@Test("init: copies id from option")
func init_copiesId() {
let option = makeOption()
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.id == option.id)
}
// MARK: - Edge Cases
@Test("init: handles empty stops")
func init_emptyStops() {
let option = makeOption(stops: [])
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.cities.isEmpty)
#expect(input.totalGames == 0)
}
@Test("init: handles empty games dictionary")
func init_emptyGames() {
let stops = [makeStop(city: "NYC", games: ["g1"])]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.sports.isEmpty)
}
@Test("init: handles zero distance and hours")
func init_zeroValues() {
let option = makeOption(totalDrivingHours: 0, totalDistanceMiles: 0)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.totalMiles == 0)
#expect(input.totalDrivingHours == 0)
}
@Test("init: handles single city")
func init_singleCity() {
let stops = [makeStop(city: "Only City")]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.cities.count == 1)
#expect(input.cities.first == "Only City")
}
// MARK: - Invariant Tests
/// - Invariant: cities preserves stop order
@Test("Invariant: cities preserves stop order")
func invariant_citiesPreservesOrder() {
let stops = [
makeStop(city: "First"),
makeStop(city: "Second"),
makeStop(city: "Third")
]
let option = makeOption(stops: stops)
let input = RouteDescriptionInput(from: option, games: [:])
#expect(input.cities == ["First", "Second", "Third"])
}
}
// MARK: - RouteDescription Tests
@Suite("RouteDescription")
struct RouteDescriptionTests {
// MARK: - Specification Tests
/// - Expected Behavior: RouteDescription stores description string
@Test("RouteDescription: stores description")
func routeDescription_storesDescription() {
let desc = RouteDescription(description: "An exciting road trip!")
#expect(desc.description == "An exciting road trip!")
}
@Test("RouteDescription: handles empty description")
func routeDescription_emptyDescription() {
let desc = RouteDescription(description: "")
#expect(desc.description == "")
}
@Test("RouteDescription: handles long description")
func routeDescription_longDescription() {
let longText = String(repeating: "A", count: 1000)
let desc = RouteDescription(description: longText)
#expect(desc.description == longText)
}
// MARK: - Invariant Tests
/// - Invariant: description is never nil (non-optional)
@Test("Invariant: description is non-optional")
func invariant_descriptionNonOptional() {
let desc = RouteDescription(description: "Test")
// Just accessing .description should always work
let _ = desc.description
#expect(Bool(true)) // If we got here, description is non-optional
}
}