Files
Sportstime/SportsTimeTests/Features/Trip/ItineraryTravelConstraintTests.swift

292 lines
14 KiB
Swift

//
// ItineraryTravelConstraintTests.swift
// SportsTimeTests
//
// Tests for travel segment movement constraints.
//
import XCTest
@testable import SportsTime
private typealias H = ItineraryTestHelpers
@MainActor
final class ItineraryTravelConstraintTests: XCTestCase {
private let testTripId = H.testTripId
private let testDate = H.testDate
// MARK: - Travel Constraint Tests
func test_travel_cannotMoveBeforeLastDepartureGame() {
// Given: Chicago has games on Days 1-2, Detroit has game on Day 4
// Travel Chicago Detroit valid range: Days 2-4
// Travel cannot be on Day 1 (before last Chicago game)
let chicagoGame1 = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let chicagoGame2 = H.makeGameItem(city: "Chicago", day: 2, sortOrder: 100)
let detroitGame = H.makeGameItem(city: "Detroit", day: 4, sortOrder: 100)
let constraints = ItineraryConstraints(
tripDayCount: 5,
items: [chicagoGame1, chicagoGame2, detroitGame]
)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50)
// Travel on Day 1 should be INVALID (Chicago game on Day 2 not finished)
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 50), "Travel on Day 1 should be invalid - must wait for Day 2 Chicago game")
// Travel on Day 2 after the game should be VALID
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 150), "Travel on Day 2 after game should be valid")
}
func test_travel_cannotMoveAfterFirstArrivalGame() {
// Given: Detroit has games on Days 3-4
// Travel to Detroit must arrive by Day 3 (before first game)
let chicagoGame = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let detroitGame1 = H.makeGameItem(city: "Detroit", day: 3, sortOrder: 100)
let detroitGame2 = H.makeGameItem(city: "Detroit", day: 4, sortOrder: 100)
let constraints = ItineraryConstraints(
tripDayCount: 5,
items: [chicagoGame, detroitGame1, detroitGame2]
)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 4, sortOrder: 50)
// Travel on Day 4 should be INVALID (missed Day 3 Detroit game)
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 4, sortOrder: 50), "Travel on Day 4 should be invalid - missed Day 3 game")
// Travel on Day 3 before the game should be VALID
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50), "Travel on Day 3 before game should be valid")
}
func test_travel_onEdgeDay_mustRespectSortOrderConstraints() {
// Given: Chicago game on Day 1 at sortOrder 100
// Travel on Day 1 must have sortOrder > 100 (after the game)
let chicagoGame = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let constraints = ItineraryConstraints(tripDayCount: 3, items: [chicagoGame])
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50)
// SortOrder 50 is BEFORE the game - INVALID
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 50), "Travel before game on same day should be invalid")
// SortOrder 100 is AT the game - INVALID (must be strictly after)
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 100), "Travel at same sortOrder as game should be invalid")
// SortOrder 150 is AFTER the game - VALID
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 1, sortOrder: 150), "Travel after game on same day should be valid")
}
func test_travel_onArrivalDay_mustBeBeforeGame() {
// Given: Detroit game on Day 3 at sortOrder 100
// Travel arriving Day 3 must have sortOrder < 100 (before the game)
let detroitGame = H.makeGameItem(city: "Detroit", day: 3, sortOrder: 100)
let constraints = ItineraryConstraints(tripDayCount: 3, items: [detroitGame])
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 150)
// SortOrder 150 is AFTER the game - INVALID
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 150), "Travel after game on arrival day should be invalid")
// SortOrder 100 is AT the game - INVALID
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 100), "Travel at same sortOrder as arrival game should be invalid")
// SortOrder 50 is BEFORE the game - VALID
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50), "Travel before game on arrival day should be valid")
}
func test_travel_validDayRange_calculatedCorrectly() {
// Given: Chicago games Days 1-2, Detroit games Days 4-5
// Travel valid range should be Days 2-4
let games = [
H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100),
H.makeGameItem(city: "Chicago", day: 2, sortOrder: 100),
H.makeGameItem(city: "Detroit", day: 4, sortOrder: 100),
H.makeGameItem(city: "Detroit", day: 5, sortOrder: 100)
]
let constraints = ItineraryConstraints(tripDayCount: 6, items: games)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 50)
let range = constraints.validDayRange(for: travel)
XCTAssertEqual(range, 2...4, "Valid range should be Days 2-4")
}
func test_travel_impossibleConstraints_returnsNil() {
// Given: Chicago game on Day 3, Detroit game on Day 1
// This is impossible - can't leave after Day 3 and arrive by Day 1
let games = [
H.makeGameItem(city: "Chicago", day: 3, sortOrder: 100),
H.makeGameItem(city: "Detroit", day: 1, sortOrder: 100)
]
let constraints = ItineraryConstraints(tripDayCount: 3, items: games)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 50)
let range = constraints.validDayRange(for: travel)
XCTAssertNil(range, "Impossible constraints should return nil range")
}
// MARK: - Barrier Games Tests
func test_barrierGames_identifiesCorrectGames() {
// Given: Chicago games Days 1-2, Detroit games Days 4-5
// Barriers should be: last Chicago game (Day 2) and first Detroit game (Day 4)
let chicagoGame1 = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let chicagoGame2 = H.makeGameItem(city: "Chicago", day: 2, sortOrder: 100)
let detroitGame1 = H.makeGameItem(city: "Detroit", day: 4, sortOrder: 100)
let detroitGame2 = H.makeGameItem(city: "Detroit", day: 5, sortOrder: 100)
let constraints = ItineraryConstraints(
tripDayCount: 6,
items: [chicagoGame1, chicagoGame2, detroitGame1, detroitGame2]
)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 50)
let barriers = constraints.barrierGames(for: travel)
XCTAssertEqual(barriers.count, 2, "Should identify 2 barrier games")
XCTAssertTrue(barriers.contains { $0.id == chicagoGame2.id }, "Should include last Chicago game")
XCTAssertTrue(barriers.contains { $0.id == detroitGame1.id }, "Should include first Detroit game")
}
// MARK: - Travel with Games on Same Day Tests
func test_travel_departureDay_sortOrderMustBeAfterLastGame() {
// Given: Chicago game at sortOrder 100, travel from Chicago on same day
// Travel sortOrder must be > 100
let chicagoGame = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let travel = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 150)
let dayData = ItineraryDayData(
id: 1,
dayNumber: 1,
date: testDate,
games: [H.makeRichGame(city: "Chicago", hour: 19)],
items: [.travel(H.makeTravelSegment(from: "Chicago", to: "Detroit"), dayNumber: 1)],
travelBefore: nil
)
let controller = ItineraryTableViewController(style: .plain)
controller.reloadData(
days: [dayData],
travelValidRanges: ["travel:0:chicago->detroit": 1...1],
itineraryItems: [chicagoGame, travel]
)
// Rows: 0=header, 1=games, 2=travel
// Travel is already at valid position, just verify it stays after games
XCTAssertTrue(controller.tableView(controller.tableView, canMoveRowAt: IndexPath(row: 2, section: 0)))
}
// MARK: - Travel Movement Tests
func test_travel_moveToValidDay_callsCallback() {
// Given: Travel with valid range 2-3
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelItem = ItineraryRowItem.travel(travel, dayNumber: 2)
// Travel model item for sortOrder lookup
let travelModelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 1.0)
let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [travelItem], travelBefore: nil)
let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [], travelBefore: nil)
var capturedTravelId: String = ""
var capturedDay: Int = 0
let controller = ItineraryTableViewController(style: .plain)
controller.onTravelMoved = { travelId, day, _ in
capturedTravelId = travelId
capturedDay = day
}
controller.reloadData(
days: [day1, day2, day3],
travelValidRanges: ["travel:0:chicago->detroit": 2...3],
itineraryItems: [travelModelItem]
)
// Rows: 0=Day1 header, 1=Day2 header, 2=travel, 3=Day3 header
// Move travel (row 2) to row 3 (after Day3 header = Day 3)
controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 2, section: 0), to: IndexPath(row: 3, section: 0))
XCTAssertEqual(capturedTravelId, "travel:0:chicago->detroit")
XCTAssertEqual(capturedDay, 3, "Travel should now be on Day 3")
}
// MARK: - Move Validation Tests
func test_moveValidation_travel_snapsToValidDayRange() {
// Given: Travel with valid range Days 2-3
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelId = "travel:0:chicago->detroit"
let travelItem = ItineraryRowItem.travel(travel, dayNumber: 2)
let travelModelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 1.0)
let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [travelItem], travelBefore: nil)
let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [], travelBefore: nil)
let controller = ItineraryTableViewController(style: .plain)
let validRanges = [travelId: 2...3]
controller.reloadData(days: [day1, day2, day3], travelValidRanges: validRanges, itineraryItems: [travelModelItem])
// Rows: 0=Day1 header, 1=Day2 header, 2=travel, 3=Day3 header
// Travel is at row 2 (after Day2 header at row 1)
// Try to move it to Day 1 area (row 0) - should snap back to valid range
let source = IndexPath(row: 2, section: 0)
let proposed = IndexPath(row: 0, section: 0)
let result = controller.tableView(controller.tableView, targetIndexPathForMoveFromRowAt: source, toProposedIndexPath: proposed)
// Result should NOT be row 0 (Day 1 is outside valid range)
XCTAssertGreaterThan(result.row, 0, "Travel should snap away from invalid Day 1")
}
// MARK: - Complex Scenario
func test_complexScenario_multiCityTripWithConstraints() {
// Given: A 7-day trip with:
// - Chicago games Days 1-2
// - Travel Chicago Detroit (valid Days 2-4)
// - Detroit games Days 4-5
// - Travel Detroit Milwaukee (valid Days 5-6)
// - Milwaukee game Day 6
let chicagoGame1 = H.makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let chicagoGame2 = H.makeGameItem(city: "Chicago", day: 2, sortOrder: 100)
let detroitGame1 = H.makeGameItem(city: "Detroit", day: 4, sortOrder: 100)
let detroitGame2 = H.makeGameItem(city: "Detroit", day: 5, sortOrder: 100)
let milwaukeeGame = H.makeGameItem(city: "Milwaukee", day: 6, sortOrder: 100)
let constraints = ItineraryConstraints(
tripDayCount: 7,
items: [chicagoGame1, chicagoGame2, detroitGame1, detroitGame2, milwaukeeGame]
)
// Travel 1: Chicago Detroit
let travel1 = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 50)
XCTAssertEqual(constraints.validDayRange(for: travel1), 2...4)
// Travel 2: Detroit Milwaukee
let travel2 = H.makeTravelItem(from: "Detroit", to: "Milwaukee", day: 5, sortOrder: 150)
XCTAssertEqual(constraints.validDayRange(for: travel2), 5...6)
// Invalid positions
XCTAssertFalse(constraints.isValidPosition(for: travel1, day: 1, sortOrder: 50), "Travel1 on Day 1 invalid")
XCTAssertFalse(constraints.isValidPosition(for: travel1, day: 5, sortOrder: 50), "Travel1 on Day 5 invalid")
XCTAssertFalse(constraints.isValidPosition(for: travel2, day: 4, sortOrder: 50), "Travel2 on Day 4 invalid")
XCTAssertFalse(constraints.isValidPosition(for: travel2, day: 7, sortOrder: 50), "Travel2 on Day 7 invalid")
}
}