import Foundation import Testing @testable import MediaKit @Suite("Welche Folge dran ist") struct EpisodeSelectionTests { /// Eine Folge, so knapp wie möglich. `duration` steht dabei, weil /// `watchProgress` ohne sie immer `nil` ist — genau die Falle, die einen /// Test lautlos wirkungslos macht. private func folge( _ staffel: Int?, _ nummer: Int?, gesehen: Bool = false, fortschritt: Double? = nil, id: String? = nil ) -> MediaItem { MediaItem( id: id ?? "s\(staffel.map(String.init) ?? "x")e\(nummer.map(String.init) ?? "x")", kind: .episode, title: "Folge \(nummer.map(String.init) ?? "?")", duration: 1_000, index: nummer, parentIndex: staffel, playCount: gesehen ? 1 : 0, resumeMilliseconds: fortschritt.map { Int($0 * 1_000) } ) } // MARK: - Der Normalfall /// Der Fall aus der Beschreibung: vier Folgen gesehen, die fünfte ist /// dran. @Test("Die älteste ungesehene Folge") func firstUnwatched() { let staffel = (1...6).map { folge(1, $0, gesehen: $0 <= 4) } let wahl = EpisodeSelection.nextUp(in: staffel, scope: .season) #expect(wahl?.episode.id == "s1e5") #expect(wahl?.resumes == false) } @Test("Über Staffeln hinweg gilt dieselbe Regel") func crossesSeasons() { let serie = (1...3).map { folge(1, $0, gesehen: true) } + (1...3).map { folge(2, $0, gesehen: $0 == 1) } let wahl = EpisodeSelection.nextUp(in: serie, scope: .series) #expect(wahl?.episode.id == "s2e2") } /// Drei Staffeln durch, in der vierten bei Folge zwei — der zweite Fall, /// den der Nutzer ausdrücklich genannt hat. @Test("Drei Staffeln durch, in der vierten weiter") func deepIntoASeries() { var serie: [MediaItem] = [] for staffel in 1...3 { serie += (1...8).map { folge(staffel, $0, gesehen: true) } } serie += [folge(4, 1, gesehen: true)] + (2...8).map { folge(4, $0) } serie += (1...8).map { folge(5, $0) } #expect(EpisodeSelection.nextUp(in: serie, scope: .series)?.episode.id == "s4e2") } // MARK: - Fortsetzen @Test("Eine angefangene Folge wird fortgesetzt") func resumesPartial() { let staffel = [folge(1, 1, gesehen: true), folge(1, 2, fortschritt: 0.4)] let wahl = EpisodeSelection.nextUp(in: staffel, scope: .season) #expect(wahl?.episode.id == "s1e2") #expect(wahl?.resumes == true) } /// Ein Fortschritt, den der Server kurz vor Schluss noch nicht gelöscht /// hat, öffnete den Player sonst mitten im Abspann — und das sieht aus, /// als hätte er die Folge übersprungen. @Test("Kurz vor Schluss wird nicht fortgesetzt") func doesNotResumeAtTheVeryEnd() { let staffel = [folge(1, 1, fortschritt: 0.98)] #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.resumes == false) } @Test("Ohne Fortschritt wird von vorn begonnen") func startsFromZero() { #expect(EpisodeSelection.nextUp(in: [folge(1, 1)], scope: .season)?.resumes == false) } /// **Die älteste ungesehene, nicht die am weitesten fortgeschrittene.** /// Wer sich eine Folge aufgehoben hat, soll sie nicht dadurch verlieren, /// dass er weiter vorn hineingeschaut hat. @Test("Eine spätere angefangene Folge zieht nicht vor") func laterProgressDoesNotWin() { let staffel = [ folge(1, 1, gesehen: true), folge(1, 2), folge(1, 5, fortschritt: 0.5), ] #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.episode.id == "s1e2") } // MARK: - Alles gesehen @Test("Ist alles gesehen, beginnt die Serie von vorn") func watchedSeriesRestarts() { let serie = (1...3).map { folge(1, $0, gesehen: true) } + (1...3).map { folge(2, $0, gesehen: true) } let wahl = EpisodeSelection.nextUp(in: serie, scope: .series) #expect(wahl?.episode.id == "s1e1") #expect(wahl?.resumes == false) } @Test("Ist die Staffel gesehen, beginnt sie von vorn") func watchedSeasonRestarts() { let staffel = (1...3).map { folge(4, $0, gesehen: true) } #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.episode.id == "s4e1") } // MARK: - Reihenfolge /// Jellyfin sortiert `children(of:)` nach dem Namen, und das stellt /// „Folge 10" vor „Folge 2". Sich darauf zu verlassen ginge genau so /// lange gut, bis eine Staffel zehn Folgen hat. @Test("Die Reihenfolge des Servers zählt nicht") func ignoresServerOrder() { let durcheinander = [folge(1, 10), folge(1, 2), folge(1, 1)] #expect(EpisodeSelection.nextUp(in: durcheinander, scope: .season)?.episode.id == "s1e1") } @Test("Lücken in der Nummerierung ändern nichts") func gapsDoNotMatter() { let staffel = [folge(1, 1, gesehen: true), folge(1, 7), folge(1, 9)] #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.episode.id == "s1e7") } /// Eine fehlende Nummer ist keine Null, sondern „der Server hat nichts /// gesagt". Solche Einträge gehören ans Ende. @Test("Folgen ohne Nummer stehen hinten") func unnumberedGoLast() { let staffel = [folge(1, nil, id: "ohne"), folge(1, 1)] #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.episode.id == "s1e1") } @Test("Zweimal dieselbe Eingabe ergibt zweimal dasselbe") func isRepeatable() { let staffel = [folge(1, nil, id: "b"), folge(1, nil, id: "a")] #expect(EpisodeSelection.nextUp(in: staffel, scope: .season)?.episode.id == "a") } // MARK: - Specials /// Staffel 0 steht vor Staffel 1 und ist trotzdem nicht der Anfang. @Test("Specials werden bei der Serie übergangen") func skipsSpecialsInSeries() { let serie = [folge(0, 1), folge(1, 1)] #expect(EpisodeSelection.nextUp(in: serie, scope: .series)?.episode.id == "s1e1") } /// Wer die Specials-Kachel angeklickt hat, meint die Specials. @Test("Bei der Staffel bleiben sie stehen") func keepsSpecialsInSeason() { let specials = [folge(0, 1), folge(0, 2)] #expect(EpisodeSelection.nextUp(in: specials, scope: .season)?.episode.id == "s0e1") } /// Gäbe es **nur** Specials, dürfte die Serie nicht wortlos nichts tun. @Test("Eine Serie aus lauter Specials ergibt nichts") func onlySpecialsIsNothing() { #expect(EpisodeSelection.nextUp(in: [folge(0, 1)], scope: .series) == nil) } // MARK: - Was keine Folge ist /// Jellyfin liefert in Listen nicht immer eine brauchbare Art. Sie /// wegzuwerfen leerte die Liste — also bleibt `nil` stehen. @Test("Behälter fliegen raus, Unbekanntes bleibt") func dropsContainersKeepsUnknown() { let gemischt = [ MediaItem(id: "staffel", kind: .season, title: "Staffel 1", index: 1), MediaItem(id: "serie", kind: .show, title: "Serie"), MediaItem(id: "unbekannt", kind: nil, title: "?", index: 1, parentIndex: 1), ] let wahl = EpisodeSelection.nextUp(in: gemischt, scope: .series) #expect(wahl?.episode.id == "unbekannt") } @Test("Eine leere Liste ergibt nichts") func emptyIsNothing() { #expect(EpisodeSelection.nextUp(in: [], scope: .series) == nil) #expect(EpisodeSelection.nextUp(in: [], scope: .season) == nil) } }