// // MockCloudKitService.swift // SportsTimeTests // // Mock implementation of CloudKitService for testing without network dependencies. // import Foundation @testable import SportsTime // MARK: - Mock CloudKit Service actor MockCloudKitService { // MARK: - Configuration struct Configuration { var isAvailable: Bool = true var simulatedLatency: TimeInterval = 0 var shouldFailWithError: CloudKitError? = nil var errorAfterNCalls: Int? = nil static var `default`: Configuration { Configuration() } static var offline: Configuration { Configuration(isAvailable: false) } static var slow: Configuration { Configuration(simulatedLatency: 2.0) } } // MARK: - Stored Data private var stadiums: [Stadium] = [] private var teams: [Team] = [] private var games: [Game] = [] private var leagueStructure: [LeagueStructureModel] = [] private var teamAliases: [TeamAlias] = [] private var stadiumAliases: [StadiumAlias] = [] // MARK: - Call Tracking private(set) var fetchStadiumsCallCount = 0 private(set) var fetchTeamsCallCount = 0 private(set) var fetchGamesCallCount = 0 private(set) var isAvailableCallCount = 0 // MARK: - Configuration private var config: Configuration // MARK: - Initialization init(config: Configuration = .default) { self.config = config } // MARK: - Configuration Methods func configure(_ newConfig: Configuration) { self.config = newConfig } func setStadiums(_ stadiums: [Stadium]) { self.stadiums = stadiums } func setTeams(_ teams: [Team]) { self.teams = teams } func setGames(_ games: [Game]) { self.games = games } func setLeagueStructure(_ structure: [LeagueStructureModel]) { self.leagueStructure = structure } func reset() { stadiums = [] teams = [] games = [] leagueStructure = [] teamAliases = [] stadiumAliases = [] fetchStadiumsCallCount = 0 fetchTeamsCallCount = 0 fetchGamesCallCount = 0 isAvailableCallCount = 0 config = .default } // MARK: - Simulated Network private func simulateNetwork() async throws { // Simulate latency if config.simulatedLatency > 0 { try await Task.sleep(nanoseconds: UInt64(config.simulatedLatency * 1_000_000_000)) } // Check for configured error if let error = config.shouldFailWithError { throw error } } private func checkErrorAfterNCalls(_ callCount: Int) throws { if let errorAfterN = config.errorAfterNCalls, callCount >= errorAfterN { throw config.shouldFailWithError ?? CloudKitError.networkUnavailable } } // MARK: - Availability func isAvailable() async -> Bool { isAvailableCallCount += 1 return config.isAvailable } func checkAvailabilityWithError() async throws { if !config.isAvailable { throw CloudKitError.networkUnavailable } if let error = config.shouldFailWithError { throw error } } // MARK: - Fetch Operations func fetchStadiums() async throws -> [Stadium] { fetchStadiumsCallCount += 1 try checkErrorAfterNCalls(fetchStadiumsCallCount) try await simulateNetwork() return stadiums } func fetchTeams(for sport: Sport) async throws -> [Team] { fetchTeamsCallCount += 1 try checkErrorAfterNCalls(fetchTeamsCallCount) try await simulateNetwork() return teams.filter { $0.sport == sport } } func fetchGames( sports: Set, startDate: Date, endDate: Date ) async throws -> [Game] { fetchGamesCallCount += 1 try checkErrorAfterNCalls(fetchGamesCallCount) try await simulateNetwork() return games.filter { game in sports.contains(game.sport) && game.dateTime >= startDate && game.dateTime <= endDate }.sorted { $0.dateTime < $1.dateTime } } func fetchGame(by id: String) async throws -> Game? { try await simulateNetwork() return games.first { $0.id == id } } // MARK: - Sync Fetch Methods (Delta Sync Pattern) /// Fetch stadiums for sync - returns all if lastSync is nil, otherwise filters by modification date func fetchStadiumsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncStadium] { try await simulateNetwork() // Mock doesn't track modification dates, so return all stadiums // (In production, CloudKit filters by modificationDate) return stadiums.map { stadium in CloudKitService.SyncStadium( stadium: stadium, canonicalId: stadium.id ) } } /// Fetch teams for sync - returns all if lastSync is nil, otherwise filters by modification date func fetchTeamsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncTeam] { try await simulateNetwork() // Mock doesn't track modification dates, so return all teams // (In production, CloudKit filters by modificationDate) return teams.map { team in CloudKitService.SyncTeam( team: team, canonicalId: team.id, stadiumCanonicalId: team.stadiumId ) } } /// Fetch games for sync - returns all if lastSync is nil, otherwise filters by modification date func fetchGamesForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncGame] { try await simulateNetwork() // Mock doesn't track modification dates, so return all games // (In production, CloudKit filters by modificationDate) return games.map { game in CloudKitService.SyncGame( game: game, canonicalId: game.id, homeTeamCanonicalId: game.homeTeamId, awayTeamCanonicalId: game.awayTeamId, stadiumCanonicalId: game.stadiumId ) } } // MARK: - League Structure & Aliases func fetchLeagueStructure(for sport: Sport? = nil) async throws -> [LeagueStructureModel] { try await simulateNetwork() if let sport = sport { return leagueStructure.filter { $0.sport == sport.rawValue } } return leagueStructure } func fetchTeamAliases(for teamCanonicalId: String? = nil) async throws -> [TeamAlias] { try await simulateNetwork() if let teamId = teamCanonicalId { return teamAliases.filter { $0.teamCanonicalId == teamId } } return teamAliases } func fetchStadiumAliases(for stadiumCanonicalId: String? = nil) async throws -> [StadiumAlias] { try await simulateNetwork() if let stadiumId = stadiumCanonicalId { return stadiumAliases.filter { $0.stadiumCanonicalId == stadiumId } } return stadiumAliases } // MARK: - Delta Sync func fetchLeagueStructureChanges(since lastSync: Date?) async throws -> [LeagueStructureModel] { try await simulateNetwork() guard let lastSync = lastSync else { return leagueStructure } return leagueStructure.filter { $0.lastModified > lastSync } } func fetchTeamAliasChanges(since lastSync: Date?) async throws -> [TeamAlias] { try await simulateNetwork() guard let lastSync = lastSync else { return teamAliases } return teamAliases.filter { $0.lastModified > lastSync } } func fetchStadiumAliasChanges(since lastSync: Date?) async throws -> [StadiumAlias] { try await simulateNetwork() guard let lastSync = lastSync else { return stadiumAliases } return stadiumAliases.filter { $0.lastModified > lastSync } } // MARK: - Subscriptions (No-ops for testing) func subscribeToScheduleUpdates() async throws {} func subscribeToLeagueStructureUpdates() async throws {} func subscribeToTeamAliasUpdates() async throws {} func subscribeToStadiumAliasUpdates() async throws {} func subscribeToAllUpdates() async throws {} } // MARK: - Convenience Extensions extension MockCloudKitService { /// Load fixture data from FixtureGenerator func loadFixtures(_ data: FixtureGenerator.GeneratedData) { Task { await setStadiums(data.stadiums) await setTeams(data.teams) await setGames(data.games) } } /// Configure to simulate specific error scenarios static func withError(_ error: CloudKitError) -> MockCloudKitService { let mock = MockCloudKitService() Task { await mock.configure(Configuration(shouldFailWithError: error)) } return mock } /// Configure to be offline static var offline: MockCloudKitService { MockCloudKitService(config: .offline) } }