292 lines
14 KiB
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")
|
|
}
|
|
}
|