// // ItinerarySectionBuilderTests.swift // SportsTimeTests // // Tests for ItinerarySectionBuilder pure static function. // import Testing import CoreLocation @testable import SportsTime @Suite("ItinerarySectionBuilder") struct ItinerarySectionBuilderTests { // MARK: - Helpers private func makeTripDays(count: Int, startDate: Date = TestClock.now) -> [Date] { (0.. (Trip, [Date]) { let stops = TestFixtures.tripStops(cities: cities, startDate: startDate, daysPerStop: daysPerStop) var stopsWithGames = stops for (i, ids) in gameIds.enumerated() where i < stopsWithGames.count { stopsWithGames[i] = TripStop( stopNumber: stopsWithGames[i].stopNumber, city: stopsWithGames[i].city, state: stopsWithGames[i].state, coordinate: stopsWithGames[i].coordinate, arrivalDate: stopsWithGames[i].arrivalDate, departureDate: stopsWithGames[i].departureDate, games: ids, isRestDay: stopsWithGames[i].isRestDay ) } let trip = TestFixtures.trip(stops: stopsWithGames) let totalDays = cities.count * daysPerStop let days = makeTripDays(count: totalDays, startDate: startDate) return (trip, days) } // MARK: - Tests @Test("builds one section per day") func buildsSectionsForEachDay() { let startDate = TestClock.calendar.startOfDay(for: TestClock.now) let (trip, days) = makeTrip(cities: ["New York", "Boston", "Philadelphia"], startDate: startDate) let sections = ItinerarySectionBuilder.build( trip: trip, tripDays: days, games: [:], travelOverrides: [:], itineraryItems: [], allowCustomItems: false ) // Should have travel + day sections let daySections = sections.filter { if case .day = $0 { return true } return false } #expect(daySections.count == days.count) } @Test("games filtered correctly by date") func gamesOnFiltersCorrectly() { let startDate = TestClock.calendar.startOfDay(for: TestClock.now) let gameDate = startDate let game = TestFixtures.game(sport: .mlb, city: "New York", dateTime: gameDate) let richGame = TestFixtures.richGame(game: game, homeCity: "New York") let (trip, days) = makeTrip( cities: ["New York", "Boston"], startDate: startDate, gameIds: [[game.id]] ) let sections = ItinerarySectionBuilder.build( trip: trip, tripDays: days, games: [game.id: richGame], travelOverrides: [:], itineraryItems: [], allowCustomItems: false ) // Find the first day section and verify it has a game let firstDaySection = sections.first { if case .day(1, _, _) = $0 { return true } return false } if case .day(_, _, let gamesOnDay) = firstDaySection { #expect(gamesOnDay.count == 1) #expect(gamesOnDay.first?.game.id == game.id) } else { Issue.record("Expected day section with games") } } @Test("travel segments appear in sections") func travelSegmentsAppear() { let startDate = TestClock.calendar.startOfDay(for: TestClock.now) let travel = TestFixtures.travelSegment(from: "New York", to: "Boston") let (baseTrip, days) = makeTrip( cities: ["New York", "Boston"], startDate: startDate, daysPerStop: 1 ) // Create trip with travel segment var trip = baseTrip trip.travelSegments = [travel] // Build without overrides - travel appears at default position let sectionsDefault = ItinerarySectionBuilder.build( trip: trip, tripDays: days, games: [:], travelOverrides: [:], itineraryItems: [], allowCustomItems: false ) let travelSections = sectionsDefault.filter { if case .travel = $0 { return true } return false } // TravelPlacement should place the travel segment #expect(travelSections.count == 1) // Verify travel appears before day 2 (arrival day) if let travelIdx = sectionsDefault.firstIndex(where: { if case .travel = $0 { return true } return false }) { let nextIdx = sectionsDefault.index(after: travelIdx) if nextIdx < sectionsDefault.count, case .day(let dayNum, _, _) = sectionsDefault[nextIdx] { #expect(dayNum == 2) } } } @Test("custom items included when allowCustomItems is true") func customItemsIncluded() { let startDate = TestClock.calendar.startOfDay(for: TestClock.now) let (trip, days) = makeTrip(cities: ["New York"], startDate: startDate) let customItem = ItineraryItem( tripId: trip.id, day: 1, sortOrder: 1.0, kind: .custom(CustomInfo(title: "Lunch at Joe's", icon: "fork.knife")) ) let sectionsWithItems = ItinerarySectionBuilder.build( trip: trip, tripDays: days, games: [:], travelOverrides: [:], itineraryItems: [customItem], allowCustomItems: true ) let sectionsWithoutItems = ItinerarySectionBuilder.build( trip: trip, tripDays: days, games: [:], travelOverrides: [:], itineraryItems: [customItem], allowCustomItems: false ) let customSectionsWithItems = sectionsWithItems.filter { $0.isCustomItem } let addButtonSections = sectionsWithItems.filter { if case .addButton = $0 { return true } return false } let customSectionsWithoutItems = sectionsWithoutItems.filter { $0.isCustomItem } #expect(customSectionsWithItems.count == 1) #expect(addButtonSections.count >= 1) #expect(customSectionsWithoutItems.count == 0) } @Test("stableTravelAnchorId format is consistent") func stableTravelAnchorIdFormat() { let segment = TestFixtures.travelSegment(from: "New York", to: "Boston") let id = ItinerarySectionBuilder.stableTravelAnchorId(segment, at: 0) #expect(id.hasPrefix("travel:0:")) #expect(id.contains("->")) } @Test("empty trip produces no sections") func emptyTripProducesNoSections() { let trip = TestFixtures.trip(stops: []) let sections = ItinerarySectionBuilder.build( trip: trip, tripDays: [], games: [:], travelOverrides: [:], itineraryItems: [], allowCustomItems: false ) #expect(sections.isEmpty) } }