import XCTest @testable import KisaniCal /// Persistence tests: snapshot immutability, report idempotency/dedup, /// reconciliation selection, and retention pruning. Each test runs against a /// fresh temp directory. (Verified standalone via swiftc; mirrored here for the /// Xcode test target.) final class AnalyticsStoreTests: XCTestCase { private var tmp: URL! private var store: AnalyticsStore! private let now = Date(timeIntervalSince1970: 1_783_500_000) // 2026-07-08 private func cal(_ tz: String = "UTC") -> Calendar { var c = Calendar(identifier: .gregorian) c.timeZone = TimeZone(identifier: tz)! c.locale = Locale(identifier: "en_US_POSIX") return c } override func setUp() { super.setUp() tmp = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("analytics_test_\(UUID().uuidString)") store = AnalyticsStore(root: tmp) } override func tearDown() { try? FileManager.default.removeItem(at: tmp) super.tearDown() } func testPastSnapshotIsFrozen() { let past = DaySample(dateKey: "2026-06-01", didWorkout: true, scheduled: true, sessions: 1, sets: 10, volume: 500) XCTAssertTrue(store.saveSnapshot(past, isPast: true)) let edited = DaySample(dateKey: "2026-06-01", didWorkout: true, scheduled: true, sessions: 9, sets: 99, volume: 9999) XCTAssertFalse(store.saveSnapshot(edited, isPast: true), "past snapshot must not be overwritten") XCTAssertEqual(store.snapshot(dateKey: "2026-06-01")?.sets, 10) } func testTodaySnapshotUpdatable() { _ = store.saveSnapshot(DaySample(dateKey: "2026-07-08", didWorkout: false, scheduled: true), isPast: false) XCTAssertTrue(store.saveSnapshot(DaySample(dateKey: "2026-07-08", didWorkout: true, scheduled: true, sessions: 1, sets: 20, volume: 1000), isPast: false)) XCTAssertEqual(store.snapshot(dateKey: "2026-07-08")?.sets, 20) } func testWeeklyReportIdempotentAndImmutable() { let wr = AnalyticsEngine.weeklyReport( weekStartKey: "2026-06-29", days: [DaySample(dateKey: "2026-06-29", didWorkout: true, scheduled: true, sessions: 1, sets: 20, volume: 1000)], previousWeekDays: [DaySample(dateKey: "2026-06-22", didWorkout: true, scheduled: true, sessions: 1, sets: 18, volume: 800)], generatedAt: now) XCTAssertTrue(store.saveWeeklyReport(wr)) XCTAssertFalse(store.saveWeeklyReport(wr), "re-saving must be a no-op (dedup)") XCTAssertEqual(store.weeklyReport(weekStartKey: "2026-06-29")?.progression, .improving) } func testMonthlyReportDedup() { let mr = AnalyticsEngine.monthlyReport( monthKey: "2026-06", days: [DaySample(dateKey: "2026-06-01", didWorkout: true, scheduled: true, sessions: 1, sets: 20, volume: 1000)], previousMonthDays: [DaySample(dateKey: "2026-05-01", didWorkout: true, scheduled: true, sessions: 1, sets: 18, volume: 800)], generatedAt: now) XCTAssertTrue(store.saveMonthlyReport(mr)) XCTAssertFalse(store.saveMonthlyReport(mr)) XCTAssertNotNil(store.monthlyReport(monthKey: "2026-06")) } func testReconciliationSelection() { let c = cal() let weekKeys = AnalyticsEngine.completedWeekStartKeys(now: now, months: 12, calendar: c) XCTAssertTrue((50...53).contains(weekKeys.count)) let currentWeekKey = AnalyticsEngine.dateKey(for: AnalyticsEngine.weekInterval(containing: now, calendar: c)!.start, calendar: c) XCTAssertFalse(weekKeys.contains(currentWeekKey), "current (incomplete) week excluded") let monthKeys = AnalyticsEngine.completedMonthKeys(now: now, months: 12, calendar: c) XCTAssertEqual(monthKeys.count, 12) XCTAssertFalse(monthKeys.contains("2026-07"), "current month excluded") // A saved report is not re-selected (idempotent reconciliation). let wr = AnalyticsEngine.weeklyReport(weekStartKey: weekKeys.last!, days: [], previousWeekDays: [], generatedAt: now) _ = store.saveWeeklyReport(wr) XCTAssertFalse(AnalyticsEngine.missingKeys(weekKeys, existing: store.existingWeeklyKeys()).contains(weekKeys.last!)) } func testRetentionPrune() { let c = cal() _ = store.saveSnapshot(DaySample(dateKey: "2025-01-01", didWorkout: true), isPast: true) // >12mo old _ = store.saveSnapshot(DaySample(dateKey: "2026-07-08", didWorkout: true), isPast: false) let removed = store.prune(now: now, months: 12, calendar: c) XCTAssertGreaterThanOrEqual(removed, 1) XCTAssertNil(store.snapshot(dateKey: "2025-01-01"), "expired snapshot pruned") XCTAssertNotNil(store.snapshot(dateKey: "2026-07-08"), "recent snapshot survives") } }