// ── Timer Engine Tests ───────────────────────────────────────── // XCTest unit tests mirroring web Vitest tests import XCTest @testable import ChronoMind final class TimerEngineTests: XCTestCase { // MARK: - Create Alarm func testCreateAlarm() { let target = Date().addingTimeInterval(3600) // 1h from now let timer = createAlarm(CreateAlarmParams(label: "Test Alarm", targetTime: target)) XCTAssertEqual(timer.type, .alarm) XCTAssertEqual(timer.label, "Test Alarm") XCTAssertEqual(timer.urgency, .standard) XCTAssertEqual(timer.state, .active) XCTAssertNotNil(timer.startedAt) XCTAssertEqual(timer.snoozeCount, 0) XCTAssertFalse(timer.id.isEmpty) } func testCreateAlarmWithUrgency() { let target = Date().addingTimeInterval(7200) let timer = createAlarm(CreateAlarmParams( label: "Critical Meeting", targetTime: target, urgency: .critical, cascade: CascadeConfig(preset: .aggressive, intervals: []) )) XCTAssertEqual(timer.urgency, .critical) XCTAssertEqual(timer.cascade.preset, .aggressive) XCTAssertFalse(timer.warnings.isEmpty) } // MARK: - Create Countdown func testCreateCountdown() { let timer = createCountdown(CreateCountdownParams( label: "Pasta", durationSeconds: 600 // 10 min )) XCTAssertEqual(timer.type, .countdown) XCTAssertEqual(timer.label, "Pasta") XCTAssertEqual(timer.duration, 600) XCTAssertEqual(timer.state, .active) XCTAssert(timer.targetTime > Date()) } func testCreateCountdownWithCustomCascade() { let timer = createCountdown(CreateCountdownParams( label: "Test", durationSeconds: 3600, cascade: CascadeConfig(preset: .custom, intervals: [30, 15, 5]) )) // Custom intervals should produce warnings let unfired = timer.warnings.filter { !$0.fired } XCTAssertEqual(unfired.count, 3) } // MARK: - Create Pomodoro func testCreatePomodoro() { let timer = createPomodoro() XCTAssertEqual(timer.type, .pomodoro) XCTAssertEqual(timer.label, "Focus Session") XCTAssertEqual(timer.state, .active) XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 25) XCTAssertEqual(timer.pomodoroConfig?.breakMinutes, 5) XCTAssertEqual(timer.pomodoroConfig?.rounds, 4) XCTAssertEqual(timer.pomodoroState?.currentRound, 1) XCTAssertEqual(timer.pomodoroState?.isBreak, false) XCTAssertEqual(timer.pomodoroState?.completedRounds, 0) } func testCreatePomodoroCustomConfig() { let config = PomodoroConfig(workMinutes: 50, breakMinutes: 10, longBreakMinutes: 30, rounds: 3) let timer = createPomodoro(CreatePomodoroParams( label: "Deep Work", config: config )) XCTAssertEqual(timer.label, "Deep Work") XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 50) XCTAssertEqual(timer.pomodoroConfig?.rounds, 3) XCTAssertEqual(timer.duration, 3000) // 50 * 60 } // MARK: - State Transitions func testPauseTimer() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let paused = pauseTimer(timer) XCTAssertEqual(paused.state, .paused) XCTAssertNotNil(paused.pausedAt) } func testPauseOnlyActiveOrWarning() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) timer.state = .dismissed let result = pauseTimer(timer) XCTAssertEqual(result.state, .dismissed) // unchanged } func testResumeTimer() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let paused = pauseTimer(timer) let resumed = resumeTimer(paused) XCTAssertEqual(resumed.state, .active) XCTAssertNil(resumed.pausedAt) XCTAssertNotNil(resumed.startedAt) XCTAssert(resumed.targetTime > Date()) } func testResumeOnlyPaused() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let result = resumeTimer(timer) // already active, not paused XCTAssertEqual(result.state, .active) // unchanged } func testFireTimer() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let fired = fireTimer(timer) XCTAssertEqual(fired.state, .firing) XCTAssertNotNil(fired.firedAt) } func testFireIgnoresDismissed() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) timer = dismissTimer(timer) let result = fireTimer(timer) XCTAssertEqual(result.state, .dismissed) } func testSnoozeTimer() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) timer = fireTimer(timer) let snoozed = snoozeTimer(timer, snoozeMinutes: 5) XCTAssertEqual(snoozed.state, .snoozed) XCTAssertEqual(snoozed.snoozeCount, 1) XCTAssertNotNil(snoozed.snoozedUntil) // Snooze until should be ~5 minutes from now let diff = snoozed.snoozedUntil!.timeIntervalSinceNow XCTAssert(diff > 290 && diff < 310) // ~300 seconds with tolerance } func testSnoozeOnlyFiring() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let result = snoozeTimer(timer, snoozeMinutes: 5) XCTAssertEqual(result.state, .active) // unchanged, not firing } func testDismissTimer() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let dismissed = dismissTimer(timer) XCTAssertEqual(dismissed.state, .dismissed) XCTAssertNotNil(dismissed.dismissedAt) } func testCompleteTimer() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let completed = completeTimer(timer) XCTAssertEqual(completed.state, .completed) XCTAssertNotNil(completed.completedAt) } // MARK: - Pomodoro Transitions func testAdvancePomodoroWorkToBreak() { let timer = createPomodoro() guard let advanced = advancePomodoro(timer) else { XCTFail("Should advance") return } XCTAssertEqual(advanced.state, .active) XCTAssertEqual(advanced.pomodoroState?.isBreak, true) XCTAssertEqual(advanced.pomodoroState?.completedRounds, 1) XCTAssertEqual(advanced.duration, 300) // 5 min break } func testAdvancePomodoroBreakToWork() { var timer = createPomodoro() // Advance to break timer = advancePomodoro(timer)! // Advance back to work guard let next = advancePomodoro(timer) else { XCTFail("Should advance") return } XCTAssertEqual(next.pomodoroState?.isBreak, false) XCTAssertEqual(next.pomodoroState?.currentRound, 2) XCTAssertEqual(next.duration, 1500) // 25 min work } func testAdvancePomodoroToLongBreak() { var timer = createPomodoro() // Go through all 4 work rounds for _ in 1...4 { timer = advancePomodoro(timer)! // work → break (or long break) if timer.pomodoroState?.isLongBreak == true || timer.state == .completed { break } if timer.pomodoroState?.isBreak == true { timer = advancePomodoro(timer)! // break → next work } } // After 4 rounds, should be on long break XCTAssertTrue(timer.pomodoroState?.isLongBreak == true || timer.state == .completed) } func testAdvancePomodoroCompletion() { var timer = createPomodoro(CreatePomodoroParams( config: PomodoroConfig(workMinutes: 1, breakMinutes: 1, longBreakMinutes: 1, rounds: 1) )) // Work → long break (only 1 round) timer = advancePomodoro(timer)! XCTAssertTrue(timer.pomodoroState?.isLongBreak == true) // Long break → complete guard let completed = advancePomodoro(timer) else { XCTFail("Should complete") return } XCTAssertEqual(completed.state, .completed) } func testAdvancePomodoroReturnsNilForNonPomodoro() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) XCTAssertNil(advancePomodoro(timer)) } // MARK: - Utility Functions func testGetRemainingSeconds() { let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let remaining = getRemainingSeconds(timer) XCTAssert(remaining > 598 && remaining <= 600) // within 2 seconds } func testGetRemainingSecondsPaused() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) // Simulate some elapsed time timer.elapsedBeforePause = 100 timer.state = .paused let remaining = getRemainingSeconds(timer) XCTAssertEqual(remaining, 500) // 600 - 100 } func testIsTimerActive() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) XCTAssertTrue(isTimerActive(timer)) timer.state = .warning XCTAssertTrue(isTimerActive(timer)) timer.state = .snoozed XCTAssertTrue(isTimerActive(timer)) timer.state = .paused XCTAssertFalse(isTimerActive(timer)) timer.state = .dismissed XCTAssertFalse(isTimerActive(timer)) } func testShouldTimerFire() { var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) let now = Date() // Not yet XCTAssertFalse(shouldTimerFire(timer, now: now)) // Past target time XCTAssertTrue(shouldTimerFire(timer, now: timer.targetTime.addingTimeInterval(1))) // Snoozed and past snooze time timer.state = .snoozed timer.snoozedUntil = now.addingTimeInterval(-10) XCTAssertTrue(shouldTimerFire(timer, now: now)) // Snoozed but not past snooze time timer.snoozedUntil = now.addingTimeInterval(300) XCTAssertFalse(shouldTimerFire(timer, now: now)) } }