// // GameDAGRouterTests.swift // SportsTimeTests // // Phase 2: GameDAGRouter Tests // The "scary to touch" component — extensive edge case coverage. // import Testing import CoreLocation @testable import SportsTime @Suite("GameDAGRouter Tests") struct GameDAGRouterTests { // MARK: - Test Fixtures private let calendar = Calendar.current // Standard game times (7pm local) private func gameDate(daysFromNow: Int, hour: Int = 19) -> Date { let baseDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 1))! var components = calendar.dateComponents([.year, .month, .day], from: baseDate) components.day! += daysFromNow components.hour = hour components.minute = 0 return calendar.date(from: components)! } // Create a stadium at a known location private func makeStadium( id: UUID = UUID(), city: String, lat: Double, lon: Double ) -> Stadium { Stadium( id: id, name: "\(city) Stadium", city: city, state: "ST", latitude: lat, longitude: lon, capacity: 40000, sport: .mlb ) } // Create a game at a stadium private func makeGame( id: UUID = UUID(), stadiumId: UUID, dateTime: Date ) -> Game { Game( id: id, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: dateTime, sport: .mlb, season: "2026" ) } // MARK: - 2A: Empty & Single-Element Cases @Test("2.1 - Empty games returns empty array") func test_findRoutes_EmptyGames_ReturnsEmptyArray() { let routes = GameDAGRouter.findRoutes( games: [], stadiums: [:], constraints: .default ) #expect(routes.isEmpty, "Expected empty array for empty games input") } @Test("2.2 - Single game returns single route") func test_findRoutes_SingleGame_ReturnsSingleRoute() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let routes = GameDAGRouter.findRoutes( games: [game], stadiums: [stadiumId: stadium], constraints: .default ) #expect(routes.count == 1, "Expected exactly 1 route for single game") #expect(routes.first?.count == 1, "Route should contain exactly 1 game") #expect(routes.first?.first?.id == game.id, "Route should contain the input game") } @Test("2.3 - Single game with matching anchor returns single route") func test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let routes = GameDAGRouter.findRoutes( games: [game], stadiums: [stadiumId: stadium], constraints: .default, anchorGameIds: [game.id] ) #expect(routes.count == 1, "Expected 1 route when anchor matches the only game") #expect(routes.first?.contains(where: { $0.id == game.id }) == true) } @Test("2.4 - Single game with non-matching anchor returns empty") func test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let nonExistentAnchor = UUID() let routes = GameDAGRouter.findRoutes( games: [game], stadiums: [stadiumId: stadium], constraints: .default, anchorGameIds: [nonExistentAnchor] ) #expect(routes.isEmpty, "Expected empty when anchor doesn't match any game") } // MARK: - 2B: Two-Game Cases @Test("2.5 - Two games with feasible transition returns both in order") func test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder() { // Chicago to Milwaukee is ~90 miles - easily feasible let chicagoStadiumId = UUID() let milwaukeeStadiumId = UUID() let chicagoStadium = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukeeStadium = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let game1 = makeGame(stadiumId: chicagoStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 14)) // Day 1, 2pm let game2 = makeGame(stadiumId: milwaukeeStadiumId, dateTime: gameDate(daysFromNow: 2, hour: 19)) // Day 2, 7pm let stadiums = [chicagoStadiumId: chicagoStadium, milwaukeeStadiumId: milwaukeeStadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should have at least one route with both games let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Expected a route containing both games") if let route = routeWithBoth { #expect(route[0].id == game1.id, "First game should be Chicago (earlier)") #expect(route[1].id == game2.id, "Second game should be Milwaukee (later)") } } @Test("2.6 - Two games with infeasible transition returns separate routes") func test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes() { // NYC to LA on same day is infeasible let nycStadiumId = UUID() let laStadiumId = UUID() let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) // Games on same day, 5 hours apart (can't drive 2500 miles in 5 hours) let game1 = makeGame(stadiumId: nycStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 13)) // 1pm let game2 = makeGame(stadiumId: laStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 21)) // 9pm let stadiums = [nycStadiumId: nycStadium, laStadiumId: laStadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should NOT have a route with both games (infeasible) let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth == nil, "Should not have a combined route for infeasible transition") // Should have separate single-game routes let singleGameRoutes = routes.filter { $0.count == 1 } #expect(singleGameRoutes.count >= 2, "Should have separate routes for each game") } @Test("2.7 - Two games same stadium same day (doubleheader) succeeds") func test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Doubleheader: 1pm and 7pm same day, same stadium let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 13)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19)) let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should have a route with both games let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Doubleheader at same stadium should be feasible") if let route = routeWithBoth { #expect(route[0].startTime < route[1].startTime, "Games should be in chronological order") } } // MARK: - 2C: Anchor Game Constraints @Test("2.8 - With anchors only returns routes containing all anchors") func test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2)) let game3 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 3)) let stadiums = [stadiumId: stadium] let anchor = game2.id let routes = GameDAGRouter.findRoutes( games: [game1, game2, game3], stadiums: stadiums, constraints: .default, anchorGameIds: [anchor] ) // All routes must contain the anchor game for route in routes { let containsAnchor = route.contains { $0.id == anchor } #expect(containsAnchor, "Every route must contain the anchor game") } } @Test("2.9 - Impossible anchors returns empty") func test_findRoutes_ImpossibleAnchors_ReturnsEmpty() { // Two anchors at opposite ends of country on same day - impossible to attend both let nycStadiumId = UUID() let laStadiumId = UUID() let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) // Same day, same time - physically impossible let game1 = makeGame(stadiumId: nycStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19)) let game2 = makeGame(stadiumId: laStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19)) let stadiums = [nycStadiumId: nycStadium, laStadiumId: laStadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default, anchorGameIds: [game1.id, game2.id] // Both are anchors ) #expect(routes.isEmpty, "Should return empty for impossible anchor combination") } @Test("2.10 - Multiple anchors route must contain all") func test_findRoutes_MultipleAnchors_RouteMustContainAll() { // Three games in nearby cities over 3 days - all feasible let chicagoId = UUID() let milwaukeeId = UUID() let detroitId = UUID() let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458) let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)) let game3 = makeGame(stadiumId: detroitId, dateTime: gameDate(daysFromNow: 3)) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit] // Make game1 and game3 anchors let routes = GameDAGRouter.findRoutes( games: [game1, game2, game3], stadiums: stadiums, constraints: .default, anchorGameIds: [game1.id, game3.id] ) #expect(!routes.isEmpty, "Should find routes with both anchors") for route in routes { let hasGame1 = route.contains { $0.id == game1.id } let hasGame3 = route.contains { $0.id == game3.id } #expect(hasGame1 && hasGame3, "Every route must contain both anchor games") } } // MARK: - 2D: Repeat Cities Toggle @Test("2.11 - Allow repeat cities same city multiple days allowed") func test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Three games in Chicago over 3 days let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2)) let game3 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 3)) let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2, game3], stadiums: stadiums, constraints: .default, allowRepeatCities: true ) // Should have routes with all 3 games (same city allowed) let routeWithAll = routes.first { $0.count == 3 } #expect(routeWithAll != nil, "Should allow visiting same city multiple days when repeat cities enabled") } @Test("2.12 - Disallow repeat cities skips second visit") func test_findRoutes_DisallowRepeatCities_SkipsSecondVisit() { let chicagoId = UUID() let milwaukeeId = UUID() let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) // Day 1: Chicago, Day 2: Milwaukee, Day 3: Back to Chicago let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)) let game3 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 3)) // Return to Chicago let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let routes = GameDAGRouter.findRoutes( games: [game1, game2, game3], stadiums: stadiums, constraints: .default, allowRepeatCities: false ) // Should NOT have a route with both Chicago games for route in routes { let chicagoGames = route.filter { stadiums[$0.stadiumId]?.city == "Chicago" } #expect(chicagoGames.count <= 1, "Should not repeat Chicago when repeat cities disabled") } } @Test("2.13 - Disallow repeat cities only option is repeat overrides with warning") func test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning() { // When only games available are in the same city, we still need to produce routes let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Only Chicago games available let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2)) let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default, allowRepeatCities: false ) // Should still return single-game routes even with repeat cities disabled #expect(!routes.isEmpty, "Should return routes even when only option is repeat city") // Note: TDD defines Trip.warnings property (test 2.13 in plan) // For now, we verify routes exist; warning system will be added when implementing } // MARK: - 2E: Driving Constraints @Test("2.14 - Exceeds max daily driving transition rejected") func test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected() { // NYC to Denver is ~1,800 miles, way over 8 hours of driving (480 miles at 60mph) let nycId = UUID() let denverId = UUID() let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let denver = makeStadium(id: denverId, city: "Denver", lat: 39.7392, lon: -104.9903) // Games on consecutive days - can't drive 1800 miles in one day let game1 = makeGame(stadiumId: nycId, dateTime: gameDate(daysFromNow: 1, hour: 14)) let game2 = makeGame(stadiumId: denverId, dateTime: gameDate(daysFromNow: 2, hour: 19)) let stadiums = [nycId: nyc, denverId: denver] // Use strict constraints (8 hours max) let strictConstraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0) let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: strictConstraints ) // Should not have a combined route (distance too far for 1 day) let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth == nil, "Should reject transition exceeding max daily driving") } @Test("2.15 - Multi-day drive allowed if within daily limits") func test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits() { // NYC to Chicago is ~790 miles - doable over multiple days let nycId = UUID() let chicagoId = UUID() let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Games 3 days apart - enough time to drive 790 miles let game1 = makeGame(stadiumId: nycId, dateTime: gameDate(daysFromNow: 1, hour: 14)) let game2 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 4, hour: 19)) let stadiums = [nycId: nyc, chicagoId: chicago] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should have a route with both (multi-day driving allowed) let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Should allow multi-day drive when time permits") } @Test("2.16 - Same day different stadiums checks available time") func test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime() { // Chicago to Milwaukee is ~90 miles (~1.5 hours driving) let chicagoId = UUID() let milwaukeeId = UUID() let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) // Same day: Chicago at 1pm, Milwaukee at 7pm (6 hours apart - feasible) let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1, hour: 13)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 1, hour: 19)) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should be feasible (1pm game + 3hr duration + 1.5hr drive = arrives ~5:30pm for 7pm game) let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Should allow same-day travel when time permits") // Now test too tight timing let game3 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 2, hour: 16)) // 4pm let game4 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2, hour: 17)) // 5pm (only 1 hr apart) let routes2 = GameDAGRouter.findRoutes( games: [game3, game4], stadiums: stadiums, constraints: .default ) let tooTightRoute = routes2.first { $0.count == 2 } #expect(tooTightRoute == nil, "Should reject same-day travel when not enough time") } // MARK: - 2F: Calendar Day Logic @Test("2.17 - Max day lookahead respects limit") func test_findRoutes_MaxDayLookahead_RespectsLimit() { // Games more than 5 days apart should not connect directly let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 8)) // 7 days later let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // With max lookahead of 5, these shouldn't directly connect // (Though they might still appear in separate routes) let routeWithBoth = routes.first { $0.count == 2 } // Note: Implementation uses maxDayLookahead = 5 // Games 7 days apart may not connect directly // This test verifies the behavior if routeWithBoth != nil { // If they do connect, verify they're in order #expect(routeWithBoth![0].startTime < routeWithBoth![1].startTime) } } @Test("2.18 - DST transition handles correctly") func test_findRoutes_DSTTransition_HandlesCorrectly() { // Test around DST transition (March 9, 2026 - spring forward) let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Create dates around DST transition var components1 = DateComponents() components1.year = 2026 components1.month = 3 components1.day = 8 // Day before spring forward components1.hour = 19 let preDST = calendar.date(from: components1)! var components2 = DateComponents() components2.year = 2026 components2.month = 3 components2.day = 9 // Spring forward day components2.hour = 19 let postDST = calendar.date(from: components2)! let game1 = makeGame(stadiumId: stadiumId, dateTime: preDST) let game2 = makeGame(stadiumId: stadiumId, dateTime: postDST) let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Should handle DST correctly - both games should be connectable let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Should handle DST transition correctly") } @Test("2.19 - Midnight game assigns to correct day") func test_findRoutes_MidnightGame_AssignsToCorrectDay() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Game at 12:05 AM belongs to the new day var components = DateComponents() components.year = 2026 components.month = 6 components.day = 2 components.hour = 0 components.minute = 5 let midnightGame = calendar.date(from: components)! let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19)) // Day 1, 7pm let game2 = makeGame(stadiumId: stadiumId, dateTime: midnightGame) // Day 2, 12:05am let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default ) // Midnight game should be on day 2, making transition feasible let routeWithBoth = routes.first { $0.count == 2 } #expect(routeWithBoth != nil, "Midnight game should be assigned to correct calendar day") } // MARK: - 2G: Diversity Selection @Test("2.20 - Select diverse routes includes short and long trips") func test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented() { // Create a mix of games over a week let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) var games: [Game] = [] for day in 1...7 { games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: day))) } let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, constraints: .default, allowRepeatCities: true ) // Should have both short (2-3 game) and long (5+ game) routes let shortRoutes = routes.filter { $0.count <= 3 } let longRoutes = routes.filter { $0.count >= 5 } #expect(!shortRoutes.isEmpty, "Should include short trip options") #expect(!longRoutes.isEmpty, "Should include long trip options") } @Test("2.21 - Select diverse routes includes high and low mileage") func test_selectDiverseRoutes_HighAndLowMileage_BothRepresented() { // Create games in both nearby and distant cities let chicagoId = UUID() let milwaukeeId = UUID() let laId = UUID() let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) let games = [ makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)), makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)), makeGame(stadiumId: laId, dateTime: gameDate(daysFromNow: 8)), // Far away, needs time ] let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, laId: la] let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, constraints: .default ) // Should have routes with varying mileage #expect(!routes.isEmpty, "Should produce diverse mileage routes") } @Test("2.22 - Select diverse routes includes few and many cities") func test_selectDiverseRoutes_FewAndManyCities_BothRepresented() { // Create games across multiple cities let cities = [ ("Chicago", 41.8781, -87.6298), ("Milwaukee", 43.0389, -87.9065), ("Detroit", 42.3314, -83.0458), ("Cleveland", 41.4993, -81.6944), ] var stadiums: [UUID: Stadium] = [:] var games: [Game] = [] for (index, city) in cities.enumerated() { let stadiumId = UUID() stadiums[stadiumId] = makeStadium(id: stadiumId, city: city.0, lat: city.1, lon: city.2) games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: index + 1))) } let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, constraints: .default ) // Should have routes with varying city counts let cityCounts = routes.map { route in Set(route.compactMap { stadiums[$0.stadiumId]?.city }).count } let minCities = cityCounts.min() ?? 0 let maxCities = cityCounts.max() ?? 0 #expect(minCities < maxCities || routes.count <= 1, "Should have routes with varying city counts") } @Test("2.23 - Select diverse routes deduplicates") func test_selectDiverseRoutes_DuplicateRoutes_Deduplicated() { let stadiumId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2)) let stadiums = [stadiumId: stadium] let routes = GameDAGRouter.findRoutes( games: [game1, game2], stadiums: stadiums, constraints: .default, allowRepeatCities: true ) // Check for duplicates var seen = Set() for route in routes { let key = route.map { $0.id.uuidString }.joined(separator: "-") #expect(!seen.contains(key), "Routes should be deduplicated") seen.insert(key) } } // MARK: - 2H: Cycle Handling @Test("2.24 - Graph with potential cycle handles silently") func test_findRoutes_GraphWithPotentialCycle_HandlesSilently() { // Create a scenario where a naive algorithm might get stuck in a loop let chicagoId = UUID() let milwaukeeId = UUID() let detroitId = UUID() let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458) // Multiple games at each city over several days (potential for cycles) let games = [ makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)), makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)), makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 3)), // Back to Chicago makeGame(stadiumId: detroitId, dateTime: gameDate(daysFromNow: 4)), makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 5)), // Back to Milwaukee ] let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit] // Should complete without hanging or infinite loop let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, constraints: .default, allowRepeatCities: true ) // Just verify it completes and returns valid routes #expect(routes.allSatisfy { !$0.isEmpty }, "All routes should be non-empty") // Verify chronological order in each route for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime < route[i + 1].startTime, "Games should be in chronological order") } } } // MARK: - 2I: Beam Search Behavior @Test("2.25 - Large dataset scales beam width") func test_findRoutes_LargeDataset_ScalesBeamWidth() { // Generate a large dataset (use fixture generator) let data = FixtureGenerator.generate(with: .medium) // 500 games let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) // Should complete and return routes #expect(!routes.isEmpty, "Should produce routes for large dataset") // Verify routes are valid for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } } @Test("2.26 - Early termination triggers when beam full") func test_findRoutes_EarlyTermination_TriggersWhenBeamFull() { // Generate a dataset that would take very long without early termination let config = FixtureGenerator.Configuration( seed: 42, gameCount: 100, stadiumCount: 20, teamCount: 20, geographicSpread: .regional ) let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default, beamWidth: 50 // Moderate beam width ) let elapsed = Date().timeIntervalSince(startTime) // Should complete in reasonable time (< 30 seconds indicates early termination is working) #expect(elapsed < TestConstants.hangTimeout, "Should complete before hang timeout (early termination)") #expect(!routes.isEmpty, "Should produce routes") } }