// // ScoreResolutionCacheTests.swift // SportsTimeTests // // TDD specification tests for ScoreResolutionCache types. // import Testing import Foundation @testable import SportsTime // MARK: - CacheStats Tests @Suite("CacheStats") struct CacheStatsTests { // MARK: - Test Data private func makeStats( totalEntries: Int = 100, entriesWithScores: Int = 80, entriesWithoutScores: Int = 20, expiredEntries: Int = 5, entriesBySport: [Sport: Int] = [.mlb: 50, .nba: 30, .nhl: 20] ) -> CacheStats { CacheStats( totalEntries: totalEntries, entriesWithScores: entriesWithScores, entriesWithoutScores: entriesWithoutScores, expiredEntries: expiredEntries, entriesBySport: entriesBySport ) } // MARK: - Specification Tests: Properties @Test("CacheStats: stores totalEntries") func cacheStats_totalEntries() { let stats = makeStats(totalEntries: 150) #expect(stats.totalEntries == 150) } @Test("CacheStats: stores entriesWithScores") func cacheStats_entriesWithScores() { let stats = makeStats(entriesWithScores: 75) #expect(stats.entriesWithScores == 75) } @Test("CacheStats: stores entriesWithoutScores") func cacheStats_entriesWithoutScores() { let stats = makeStats(entriesWithoutScores: 25) #expect(stats.entriesWithoutScores == 25) } @Test("CacheStats: stores expiredEntries") func cacheStats_expiredEntries() { let stats = makeStats(expiredEntries: 10) #expect(stats.expiredEntries == 10) } @Test("CacheStats: stores entriesBySport") func cacheStats_entriesBySport() { let bySport: [Sport: Int] = [.mlb: 40, .nba: 60] let stats = makeStats(entriesBySport: bySport) #expect(stats.entriesBySport[.mlb] == 40) #expect(stats.entriesBySport[.nba] == 60) } // MARK: - Edge Cases @Test("CacheStats: handles empty cache") func cacheStats_emptyCache() { let stats = makeStats( totalEntries: 0, entriesWithScores: 0, entriesWithoutScores: 0, expiredEntries: 0, entriesBySport: [:] ) #expect(stats.totalEntries == 0) #expect(stats.entriesBySport.isEmpty) } @Test("CacheStats: handles all expired") func cacheStats_allExpired() { let stats = makeStats(totalEntries: 100, expiredEntries: 100) #expect(stats.expiredEntries == stats.totalEntries) } @Test("CacheStats: handles all with scores") func cacheStats_allWithScores() { let stats = makeStats(totalEntries: 100, entriesWithScores: 100, entriesWithoutScores: 0) #expect(stats.entriesWithScores == stats.totalEntries) #expect(stats.entriesWithoutScores == 0) } @Test("CacheStats: handles single sport") func cacheStats_singleSport() { let stats = makeStats(entriesBySport: [.mlb: 100]) #expect(stats.entriesBySport.count == 1) #expect(stats.entriesBySport[.mlb] == 100) } // MARK: - Invariant Tests /// - Invariant: entriesWithScores + entriesWithoutScores == totalEntries @Test("Invariant: scores split equals total") func invariant_scoresSplitEqualsTotal() { let stats = makeStats(totalEntries: 100, entriesWithScores: 80, entriesWithoutScores: 20) #expect(stats.entriesWithScores + stats.entriesWithoutScores == stats.totalEntries) } /// - Invariant: expiredEntries <= totalEntries @Test("Invariant: expired entries cannot exceed total") func invariant_expiredCannotExceedTotal() { let stats = makeStats(totalEntries: 100, expiredEntries: 50) #expect(stats.expiredEntries <= stats.totalEntries) } /// - Invariant: sum of entriesBySport <= totalEntries @Test("Invariant: sport entries sum does not exceed total") func invariant_sportEntriesSumDoesNotExceedTotal() { let bySport: [Sport: Int] = [.mlb: 30, .nba: 40, .nhl: 30] let stats = makeStats(totalEntries: 100, entriesBySport: bySport) let sportSum = bySport.values.reduce(0, +) #expect(sportSum <= stats.totalEntries) } } // MARK: - Cache Expiration Behavior Tests @Suite("Cache Expiration Behavior") struct CacheExpirationBehaviorTests { // These tests document the expected cache expiration behavior // based on ScoreResolutionCache.calculateExpiration // MARK: - Specification Tests: Cache Durations /// - Expected Behavior: Recent games (< 30 days old) expire after 24 hours @Test("Expiration: recent games expire after 24 hours") func expiration_recentGames() { // Games less than 30 days old should expire after 24 hours let recentGameCacheDuration: TimeInterval = 24 * 60 * 60 #expect(recentGameCacheDuration == 86400) // 24 hours in seconds } /// - Expected Behavior: Historical games (> 30 days old) never expire (nil) @Test("Expiration: historical games never expire") func expiration_historicalGames() { // Games older than 30 days should have nil expiration (never expire) let historicalAgeThreshold: TimeInterval = 30 * 24 * 60 * 60 #expect(historicalAgeThreshold == 2592000) // 30 days in seconds } /// - Expected Behavior: Failed lookups expire after 7 days @Test("Expiration: failed lookups expire after 7 days") func expiration_failedLookups() { let failedLookupCacheDuration: TimeInterval = 7 * 24 * 60 * 60 #expect(failedLookupCacheDuration == 604800) // 7 days in seconds } // MARK: - Invariant Tests /// - Invariant: Historical threshold > recent cache duration @Test("Invariant: historical threshold exceeds recent cache duration") func invariant_historicalExceedsRecent() { let recentCacheDuration: TimeInterval = 24 * 60 * 60 let historicalThreshold: TimeInterval = 30 * 24 * 60 * 60 #expect(historicalThreshold > recentCacheDuration) } /// - Invariant: Failed lookup duration > recent cache duration @Test("Invariant: failed lookup duration exceeds recent cache duration") func invariant_failedLookupExceedsRecent() { let recentCacheDuration: TimeInterval = 24 * 60 * 60 let failedLookupDuration: TimeInterval = 7 * 24 * 60 * 60 #expect(failedLookupDuration > recentCacheDuration) } }