import Testing import Foundation @testable import CalendarProvider private let day: TimeInterval = 86_400 private let base = Date(timeIntervalSince1970: 1_770_000_000) private func event(_ id: String, at offset: TimeInterval) -> OnyxEvent { OnyxEvent(id: id, title: id, start: base + offset, end: base + offset + 3600, isAllDay: false, colorHex: nil, location: nil, calendarID: "c", calendarTitle: "C") } @Suite("Beide Quellen zusammen") struct SourceMergeTests { @Test("Termine beider Quellen erscheinen, nach Zeit sortiert") func mergesAndSorts() { let apple = EventWindow(events: [event("spät", at: 2 * day)]) let calendarr = EventWindow(events: [event("früh", at: day)], coverage: DateInterval(start: base, duration: 49 * day)) let merged = EventWindow.merged([apple, calendarr]) #expect(merged.events.map(\.id) == ["früh", "spät"]) } @Test("Die Abdeckung ist der Schnitt, nicht die Vereinigung") func coverageIsIntersection() { // Der Kern: ein Tag gilt nur dann als bekannt, wenn ihn **jede** // aktive Quelle abdeckt. Deckt Calendarr den März nicht ab, dann ist // der März unbekannt — auch wenn der Apple-Kalender dafür Auskunft // geben könnte. Sonst zeigt das Raster einen halben März als „frei". let unbounded = EventWindow(events: [], coverage: nil) let narrow = EventWindow(events: [], coverage: DateInterval(start: base, duration: 49 * day)) let merged = EventWindow.merged([unbounded, narrow]) #expect(merged.coverage == DateInterval(start: base, duration: 49 * day)) } @Test("Zwei begrenzte Quellen ergeben die gemeinsame Schnittmenge") func twoBoundedIntersect() { let a = EventWindow(events: [], coverage: DateInterval(start: base, duration: 30 * day)) let b = EventWindow(events: [], coverage: DateInterval(start: base + 10 * day, duration: 30 * day)) let merged = EventWindow.merged([a, b]) #expect(merged.coverage?.start == base + 10 * day) #expect(merged.coverage?.end == base + 30 * day) } @Test("Ohne Überschneidung ist nichts abgedeckt") func disjointCoverageIsEmpty() { let a = EventWindow(events: [], coverage: DateInterval(start: base, duration: day)) let b = EventWindow(events: [], coverage: DateInterval(start: base + 10 * day, duration: day)) let merged = EventWindow.merged([a, b]) #expect(merged.coverage?.duration == 0) } @Test("Nur unbegrenzte Quellen bleiben unbegrenzt") func allUnboundedStaysUnbounded() { let merged = EventWindow.merged([EventWindow(events: []), EventWindow(events: [])]) #expect(merged.coverage == nil) } @Test("Der Stand ist der älteste — er begrenzt die Verlässlichkeit") func asOfIsTheOldest() { // Zeigt man den neueren Stand, behauptet man eine Aktualität, die // die andere Quelle nicht hat. let a = EventWindow(events: [], asOf: base) let b = EventWindow(events: [], asOf: base + day) #expect(EventWindow.merged([a, b]).asOf == base) } @Test("Eine einzelne Quelle bleibt unverändert") func singleIsUnchanged() { let only = EventWindow(events: [event("x", at: 0)], coverage: DateInterval(start: base, duration: day), asOf: base) #expect(EventWindow.merged([only]) == only) } @Test("Gar keine Quelle ergibt leer, nicht abgedeckt") func noneIsEmpty() { #expect(EventWindow.merged([]) == .empty) } } @Suite("Zustand beider Quellen") struct SourceStateMergeTests { @Test("Ein Fehler einer Quelle verdeckt die andere nicht") func oneFailureKeepsTheOther() { // Calendarr nie gestartet zu haben darf den Apple-Kalender nicht // ausknipsen — sonst ist „beide" schlechter als jede einzelne. let merged = CalendarSourceKind.merge([.ready(EventWindow(events: [event("a", at: 0)])), .neverWritten]) guard case .ready(let window) = merged else { Issue.record("Erwartet .ready, bekommen \(merged)"); return } #expect(window.events.count == 1) } @Test("Fällt eine Quelle aus, gilt nur noch die Abdeckung der anderen") func failedSourceContributesNoCoverage() { let good = EventWindow(events: [], coverage: DateInterval(start: base, duration: 10 * day)) guard case .ready(let window) = CalendarSourceKind.merge([.ready(good), .denied]) else { Issue.record("Erwartet .ready"); return } #expect(window.coverage == good.coverage) } @Test("Fallen alle Quellen aus, wird der erste Grund gemeldet") func allFailedReportsFirstReason() { // Irgendeine Begründung ist besser als eine leere Liste, die aussieht, // als wäre nichts los. #expect(CalendarSourceKind.merge([.denied, .neverWritten]) == .denied) } @Test("Ohne Quellen ist der Zustand leer und bereit") func noSourcesIsReadyEmpty() { #expect(CalendarSourceKind.merge([]) == .ready(.empty)) } }