// ── Cascade Tests ────────────────────────────────────────────── // XCTest unit tests mirroring web cascade.test.ts import XCTest @testable import ChronoMind final class CascadeTests: XCTestCase { // MARK: - Preset Intervals func testAggressivePreset() { let intervals = CascadePreset.aggressive.defaultIntervals XCTAssertEqual(intervals, [240, 180, 120, 90, 60, 30, 15, 5, 1]) } func testStandardPreset() { let intervals = CascadePreset.standard.defaultIntervals XCTAssertEqual(intervals, [120, 60, 30, 15, 5]) } func testLightPreset() { let intervals = CascadePreset.light.defaultIntervals XCTAssertEqual(intervals, [60, 15, 5]) } func testMinimalPreset() { let intervals = CascadePreset.minimal.defaultIntervals XCTAssertEqual(intervals, [15]) } func testNonePreset() { let intervals = CascadePreset.none.defaultIntervals XCTAssertTrue(intervals.isEmpty) } // MARK: - Calculate Cascade Warnings func testCalculateWarnings() { let now = Date() let targetTime = now.addingTimeInterval(3600) // 1h from now let intervals = [60, 30, 15, 5] let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) XCTAssertEqual(warnings.count, 4) // Should be sorted largest first XCTAssertEqual(warnings[0].minutesBefore, 60) XCTAssertEqual(warnings[1].minutesBefore, 30) XCTAssertEqual(warnings[2].minutesBefore, 15) XCTAssertEqual(warnings[3].minutesBefore, 5) } func testWarningsInPastAreFired() { let now = Date() let targetTime = now.addingTimeInterval(1800) // 30 min from now let intervals = [120, 60, 15, 5] // 2h and 1h are in the past let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) // 120min and 60min warnings should be marked as fired (they're in the past) let fired = warnings.filter(\.fired) XCTAssertEqual(fired.count, 2) XCTAssertTrue(warnings[0].fired) // 120m — in the past XCTAssertTrue(warnings[1].fired) // 60m — in the past XCTAssertFalse(warnings[2].fired) // 15m — still in the future XCTAssertFalse(warnings[3].fired) // 5m — still in the future } func testWarningScheduledTimes() { let now = Date() let targetTime = now.addingTimeInterval(7200) // 2h from now let intervals = [60, 30] let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) // 60min warning should be at targetTime - 60min let expected60 = targetTime.addingTimeInterval(-3600) XCTAssertEqual(warnings[0].scheduledTime.timeIntervalSince1970, expected60.timeIntervalSince1970, accuracy: 1.0) // 30min warning should be at targetTime - 30min let expected30 = targetTime.addingTimeInterval(-1800) XCTAssertEqual(warnings[1].scheduledTime.timeIntervalSince1970, expected30.timeIntervalSince1970, accuracy: 1.0) } func testEmptyIntervals() { let now = Date() let targetTime = now.addingTimeInterval(3600) let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: [], now: now) XCTAssertTrue(warnings.isEmpty) } // MARK: - Get Next Warning func testGetNextWarning() { let now = Date() let targetTime = now.addingTimeInterval(7200) // 2h from now let warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [60, 30, 15, 5], now: now ) let next = getNextWarning(warnings) XCTAssertNotNil(next) // All warnings are in the future (earliest at targetTime - 60m = 1h from now) XCTAssertEqual(next?.minutesBefore, 60) } func testGetNextWarningAllFired() { let now = Date() let targetTime = now.addingTimeInterval(60) // 1 min from now var warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [120, 60, 30], now: now ) // Mark all as fired for i in warnings.indices { warnings[i].fired = true } let next = getNextWarning(warnings) XCTAssertNil(next) } // MARK: - Check Warnings func testCheckWarnings() { let now = Date() let targetTime = now.addingTimeInterval(7200) // 2h from now var warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [60, 30, 15, 5], now: now ) // All warnings should be unfired initially XCTAssertTrue(warnings.allSatisfy { !$0.fired }) // Check at a time when 60m warning should fire (targetTime - 60min) let checkTime = targetTime.addingTimeInterval(-3599) // 1 second after 60m mark let fired = checkWarnings(&warnings, now: checkTime) // The 60m warning should fire XCTAssertEqual(fired.count, 1) XCTAssertTrue(warnings[0].fired) // 60m warning fired XCTAssertFalse(warnings[1].fired) // 30m warning not yet } func testCheckWarningsNoneFire() { let now = Date() let targetTime = now.addingTimeInterval(7200) // 2h from now var warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [60, 30, 15], now: now ) // Check at current time — no warnings should fire yet (earliest is in 1h) let fired = checkWarnings(&warnings, now: now) XCTAssertTrue(fired.isEmpty) } // MARK: - Get Cascade Intervals func testGetCascadeIntervalsPreset() { let config = CascadeConfig(preset: .standard, intervals: []) let intervals = getCascadeIntervals(config) XCTAssertEqual(intervals, [120, 60, 30, 15, 5]) } func testGetCascadeIntervalsCustom() { let config = CascadeConfig(preset: .custom, intervals: [10, 45, 5, 30]) let intervals = getCascadeIntervals(config) XCTAssertEqual(intervals, [45, 30, 10, 5]) // sorted descending } // MARK: - Format Minutes Before func testFormatMinutesBefore() { XCTAssertEqual(formatMinutesBefore(5), "5m") XCTAssertEqual(formatMinutesBefore(15), "15m") XCTAssertEqual(formatMinutesBefore(60), "1h") XCTAssertEqual(formatMinutesBefore(90), "1h 30m") XCTAssertEqual(formatMinutesBefore(120), "2h") XCTAssertEqual(formatMinutesBefore(240), "4h") } }