// ── SharedTimerData Tests ───────────────────────────────────── import XCTest @testable import ChronoMind final class SharedTimerDataTests: XCTestCase { // MARK: - TimerSnapshot Conversion func testCMTimerToSnapshot() { let now = Date() let target = now.addingTimeInterval(3600) let warning = CascadeWarning( id: "w1", minutesBefore: 10, fired: true, firedAt: now.addingTimeInterval(-600), scheduledTime: now.addingTimeInterval(-600) ) let warning2 = CascadeWarning( id: "w2", minutesBefore: 5, fired: false, firedAt: nil, scheduledTime: target.addingTimeInterval(-300) ) var timer = createAlarm(CreateAlarmParams( label: "Test Alarm", targetTime: target, urgency: .important )) timer.warnings = [warning, warning2] timer.pomodoroConfig = PomodoroConfig(workMinutes: 25, breakMinutes: 5, longBreakMinutes: 15, rounds: 4) timer.pomodoroState = PomodoroState(currentRound: 2, isBreak: false, isLongBreak: false, completedRounds: 1) let snapshot = timer.toSnapshot() XCTAssertEqual(snapshot.id, timer.id) XCTAssertEqual(snapshot.label, "Test Alarm") XCTAssertEqual(snapshot.type, .alarm) XCTAssertEqual(snapshot.urgency, .important) XCTAssertEqual(snapshot.state, .active) XCTAssertEqual(snapshot.targetTime, target) XCTAssertEqual(snapshot.totalWarnings, 2) XCTAssertEqual(snapshot.firedWarnings, 1) XCTAssertEqual(snapshot.nextWarningTime, target.addingTimeInterval(-300)) XCTAssertEqual(snapshot.pomodoroCurrentRound, 2) XCTAssertEqual(snapshot.pomodoroTotalRounds, 4) XCTAssertEqual(snapshot.pomodoroIsBreak, false) } func testSnapshotWithNoWarnings() { let now = Date() let target = now.addingTimeInterval(600) let timer = createCountdown(CreateCountdownParams( label: "Quick", durationSeconds: 600, urgency: .gentle )) let snapshot = timer.toSnapshot() XCTAssertEqual(snapshot.label, "Quick") XCTAssertEqual(snapshot.type, .countdown) XCTAssertEqual(snapshot.urgency, .gentle) XCTAssertNil(snapshot.pomodoroCurrentRound) XCTAssertNil(snapshot.pomodoroTotalRounds) } // MARK: - TimerSnapshot Helpers func testRemainingSeconds() { let target = Date().addingTimeInterval(120) let snapshot = TimerSnapshot( id: "t1", label: "Test", type: .countdown, urgency: .standard, state: .active, targetTime: target, duration: 120, startedAt: Date(), elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) let remaining = snapshot.remainingSeconds() XCTAssertGreaterThan(remaining, 118) XCTAssertLessThanOrEqual(remaining, 120) } func testRemainingSecondsPaused() { let target = Date().addingTimeInterval(300) let snapshot = TimerSnapshot( id: "t2", label: "Paused", type: .countdown, urgency: .standard, state: .paused, targetTime: target, duration: 600, startedAt: Date(), elapsedBeforePause: 200, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) // When paused, remaining = duration - elapsedBeforePause let remaining = snapshot.remainingSeconds() XCTAssertEqual(remaining, 400) } func testHasFired() { let pastTarget = Date().addingTimeInterval(-10) let snapshot = TimerSnapshot( id: "t3", label: "Fired", type: .alarm, urgency: .critical, state: .firing, targetTime: pastTarget, duration: nil, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) XCTAssertTrue(snapshot.hasFired()) } func testHasNotFired() { let futureTarget = Date().addingTimeInterval(3600) let snapshot = TimerSnapshot( id: "t4", label: "Active", type: .alarm, urgency: .standard, state: .active, targetTime: futureTarget, duration: nil, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) XCTAssertFalse(snapshot.hasFired()) } func testRemainingCompact() { let target = Date().addingTimeInterval(3600) let snapshot = TimerSnapshot( id: "t5", label: "Hour", type: .countdown, urgency: .standard, state: .active, targetTime: target, duration: 3600, startedAt: Date(), elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) let compact = snapshot.remainingCompact() // Should be something like "59m" or "1h" XCTAssertFalse(compact.isEmpty) } // MARK: - Snapshot Codable func testSnapshotEncodeDecode() throws { let snapshot = TimerSnapshot( id: "codec-test", label: "Encode Me", type: .pomodoro, urgency: .critical, state: .warning, targetTime: Date().addingTimeInterval(500), duration: 1500, startedAt: Date(), elapsedBeforePause: 100, snoozeCount: 1, category: "work", pomodoroCurrentRound: 3, pomodoroTotalRounds: 4, pomodoroIsBreak: false, nextWarningTime: Date().addingTimeInterval(200), totalWarnings: 3, firedWarnings: 2 ) let encoder = JSONEncoder() let data = try encoder.encode(snapshot) let decoder = JSONDecoder() let decoded = try decoder.decode(TimerSnapshot.self, from: data) XCTAssertEqual(decoded.id, "codec-test") XCTAssertEqual(decoded.label, "Encode Me") XCTAssertEqual(decoded.type, .pomodoro) XCTAssertEqual(decoded.urgency, .critical) XCTAssertEqual(decoded.state, .warning) XCTAssertEqual(decoded.duration, 1500) XCTAssertEqual(decoded.elapsedBeforePause, 100) XCTAssertEqual(decoded.snoozeCount, 1) XCTAssertEqual(decoded.category, "work") XCTAssertEqual(decoded.pomodoroCurrentRound, 3) XCTAssertEqual(decoded.pomodoroTotalRounds, 4) XCTAssertEqual(decoded.pomodoroIsBreak, false) XCTAssertEqual(decoded.totalWarnings, 3) XCTAssertEqual(decoded.firedWarnings, 2) } func testSnapshotArrayEncodeDecode() throws { let snapshots = [ TimerSnapshot( id: "a1", label: "Timer A", type: .alarm, urgency: .important, state: .active, targetTime: Date().addingTimeInterval(1000), duration: nil, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ), TimerSnapshot( id: "a2", label: "Timer B", type: .countdown, urgency: .gentle, state: .paused, targetTime: Date().addingTimeInterval(2000), duration: 2000, startedAt: Date(), elapsedBeforePause: 500, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 1, firedWarnings: 0 ), ] let data = try JSONEncoder().encode(snapshots) let decoded = try JSONDecoder().decode([TimerSnapshot].self, from: data) XCTAssertEqual(decoded.count, 2) XCTAssertEqual(decoded[0].id, "a1") XCTAssertEqual(decoded[1].id, "a2") XCTAssertEqual(decoded[1].state, .paused) } // MARK: - Equatable func testSnapshotEquatable() { let s1 = TimerSnapshot( id: "eq1", label: "A", type: .alarm, urgency: .standard, state: .active, targetTime: Date(), duration: nil, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) let s2 = TimerSnapshot( id: "eq1", label: "B", type: .countdown, urgency: .critical, state: .firing, targetTime: Date(), duration: 100, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) let s3 = TimerSnapshot( id: "eq2", label: "A", type: .alarm, urgency: .standard, state: .active, targetTime: Date(), duration: nil, startedAt: nil, elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 0, firedWarnings: 0 ) // Same id = equal XCTAssertEqual(s1, s2) // Different id = not equal XCTAssertNotEqual(s1, s3) } }