// // 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: Date(), departureDate: Date().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: Date(), 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 } }