import Foundation import PlexKit import Testing @testable import FlixyDownloads /// Baut einen Eintrag im gewünschten Zustand, ohne Datenbank. @MainActor private func item( _ key: String, state: DownloadState = .completed, watchedAgo: Duration? = nil, series: String? = nil ) -> DownloadItem { let item = DownloadItem( ratingKey: key, title: "Titel \(key)", sourceURLString: "http://server/\(key)", state: state, seriesKey: series ) if let watchedAgo { item.watchedAt = Date().addingTimeInterval(-watchedAgo.seconds) } return item } @MainActor @Suite("Download-Regeln") struct DownloadRulesTests { // MARK: - Automatisches Löschen @Test("Ohne Regel wird nichts gelöscht") func disabledRulesDeleteNothing() { // Die wichtigste Zusicherung im ganzen Regelwerk: wer nichts // eingestellt hat, verliert nichts. let items = [item("1", watchedAgo: .afterOneWeek)] #expect(DownloadRules.disabled.itemsToDelete(from: items).isEmpty) } @Test("Ungesehene Titel werden nie gelöscht") func unwatchedIsNeverDeleted() { let rules = DownloadRules(deleteWatchedAfter: .immediately) let items = [item("1"), item("2"), item("3")] #expect(rules.itemsToDelete(from: items).isEmpty) } @Test("Nach Ablauf der Frist wird gelöscht") func deletesAfterGracePeriod() { let rules = DownloadRules(deleteWatchedAfter: .afterOneDay) let items = [ item("alt", watchedAgo: .afterOneWeek), item("frisch", watchedAgo: .seconds(60)), ] let doomed = rules.itemsToDelete(from: items).map(\.ratingKey) #expect(doomed == ["alt"]) } @Test("Die Frist schützt frisch Gesehenes") func gracePeriodProtectsRecent() { // Wer eine Folge zu Ende sieht und gleich noch einmal die letzte // Szene sucht, soll sie nicht schon verloren haben. let rules = DownloadRules(deleteWatchedAfter: .afterOneDay) let items = [item("1", watchedAgo: .seconds(3_600))] #expect(rules.itemsToDelete(from: items).isEmpty) } @Test("Unfertige Downloads werden nicht gelöscht") func incompleteDownloadsSurvive() { // Ein laufender Download hat nichts, was gesehen worden sein könnte — // ein alter watchedAt-Wert aus einem früheren Durchlauf darf ihn // nicht abräumen. let rules = DownloadRules(deleteWatchedAfter: .immediately) let running = item("1", state: .downloading, watchedAgo: .afterOneWeek) #expect(rules.itemsToDelete(from: [running]).isEmpty) } // MARK: - Vorrat an Folgen @Test("Ohne Regel wird nichts nachgeladen") func noRefillWithoutRule() { #expect(DownloadRules.disabled.seriesNeedingRefill(in: [item("1", series: "s")]).isEmpty) } @Test("Ein leerer Vorrat verlangt die volle Zahl") func emptyStockNeedsFullCount() { let rules = DownloadRules(keepUnwatchedEpisodes: 3) #expect(rules.missingEpisodeCount(forSeries: "s", in: []) == 3) } @Test("Gesehene Folgen zählen nicht zum Vorrat") func watchedEpisodesDoNotCount() { // Sonst bliebe der Vorrat bei drei gesehenen Folgen stehen und es käme // nie etwas Neues nach — genau das Gegenteil des Gewünschten. let rules = DownloadRules(keepUnwatchedEpisodes: 3) let items = [ item("1", watchedAgo: .seconds(10), series: "s"), item("2", watchedAgo: .seconds(10), series: "s"), item("3", series: "s"), ] #expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 2) } @Test("Wartende Downloads zählen zum Vorrat") func queuedEpisodesCount() { // Sonst würde bei jedem Durchlauf erneut nachgelegt, bis die ganze // Serie auf dem Gerät liegt. let rules = DownloadRules(keepUnwatchedEpisodes: 3) let items = [ item("1", state: .completed, series: "s"), item("2", state: .queued, series: "s"), item("3", state: .downloading, series: "s"), ] #expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 0) } @Test("Gescheiterte Downloads zählen nicht zum Vorrat") func failedEpisodesDoNotCount() { let rules = DownloadRules(keepUnwatchedEpisodes: 2) let items = [ item("1", state: .completed, series: "s"), item("2", state: .failed, series: "s"), ] #expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 1) } @Test("Ein voller Vorrat verlangt nichts") func fullStockNeedsNothing() { let rules = DownloadRules(keepUnwatchedEpisodes: 2) let items = [item("1", series: "s"), item("2", series: "s"), item("3", series: "s")] #expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 0) } @Test("Serien werden getrennt gezählt") func seriesAreCountedSeparately() { let rules = DownloadRules(keepUnwatchedEpisodes: 2) let items = [ item("a1", series: "a"), item("a2", series: "a"), item("b1", series: "b"), ] let refill = rules.seriesNeedingRefill(in: items) #expect(refill["a"] == nil) #expect(refill["b"] == 1) } @Test("Filme ohne Serie lösen kein Nachladen aus") func moviesDoNotTriggerRefill() { let rules = DownloadRules(keepUnwatchedEpisodes: 3) #expect(rules.seriesNeedingRefill(in: [item("1"), item("2")]).isEmpty) } }