using System; using System.Collections.Generic; using API.DTOs; using API.DTOs.ReadingLists; using API.Entities.Enums; using API.Services; using API.Services.Tasks.Scanner.Parser; using Xunit; namespace API.Tests.Services; #nullable enable public class EntityNamingServiceTests { private readonly EntityNamingService _sut = new(); #region FormatChapterTitle Tests [Theory] [InlineData(LibraryType.Manga, "1", null, "Chapter 1")] [InlineData(LibraryType.Manga, "1448", null, "Chapter 1448")] [InlineData(LibraryType.Manga, "1.5", null, "Chapter 1.5")] [InlineData(LibraryType.Image, "5", null, "Chapter 5")] public void FormatChapterTitle_Manga_ReturnsChapterFormat( LibraryType libraryType, string range, string? title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Comic, "1", null, "Issue #1")] [InlineData(LibraryType.Comic, "25", null, "Issue #25")] [InlineData(LibraryType.ComicVine, "100", null, "Issue #100")] public void FormatChapterTitle_Comic_ReturnsIssueFormat( LibraryType libraryType, string range, string? title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Book, "1", "The Fellowship", "Book The Fellowship")] [InlineData(LibraryType.LightNovel, "1", "Some Title", "Book 1 - Some Title")] public void FormatChapterTitle_Book_ReturnsBookFormat( LibraryType libraryType, string range, string? title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, "1448", "The Big Fight", "Chapter 1448 - The Big Fight")] [InlineData(LibraryType.Comic, "5", "The Origin", "Issue #5 - The Origin")] [InlineData(LibraryType.Image, "10", "Epilogue", "Chapter 10 - Epilogue")] public void FormatChapterTitle_WithUniqueTitle_AppendsTitleToBase( LibraryType libraryType, string range, string title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, "1448", "Chapter 1448", "Chapter 1448")] [InlineData(LibraryType.Manga, "1448", "Ch. 1448", "Chapter 1448")] [InlineData(LibraryType.Manga, "1448", "Ch 1448", "Chapter 1448")] [InlineData(LibraryType.Manga, "10", "Episode 10", "Chapter 10")] [InlineData(LibraryType.Comic, "5", "Issue #5", "Issue #5")] [InlineData(LibraryType.Comic, "5", "Issue 5", "Issue #5")] [InlineData(LibraryType.Manga, "1", "#1", "Chapter 1")] public void FormatChapterTitle_WithRedundantTitle_DoesNotDuplicate( LibraryType libraryType, string range, string title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, "1448", "1448", "Chapter 1448")] [InlineData(LibraryType.Comic, "5", "5", "Issue #5")] public void FormatChapterTitle_TitleMatchesRange_DoesNotDuplicate( LibraryType libraryType, string range, string title, string expected) { var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Fact] public void FormatChapterTitle_Special_ReturnsCleanedTitle() { var result = _sut.FormatChapterTitle( LibraryType.Manga, isSpecial: true, "SP01", "SP01 - Bonus Chapter"); // Assuming Parser.CleanSpecialTitle removes "SP01 - " prefix Assert.NotNull(result); } [Fact] public void FormatChapterTitle_WithCustomLabels_UsesProvidedLabels() { var result = _sut.FormatChapterTitle( LibraryType.Manga, isSpecial: false, range: "5", title: null, chapterLabel: "Kapitel"); Assert.Equal("Kapitel 5", result); } [Fact] public void FormatChapterTitle_Comic_WithoutHash_OmitsHashMark() { var result = _sut.FormatChapterTitle( LibraryType.Comic, isSpecial: false, range: "5", title: null, withHash: false); Assert.Equal("Issue 5", result); } [Fact] public void FormatChapterTitle_WithChapterDto_ExtractsFieldsCorrectly() { var chapter = CreateChapterDto(range: "42", title: "The Answer", isSpecial: false); var result = _sut.FormatChapterTitle(LibraryType.Manga, chapter); Assert.Equal("Chapter 42 - The Answer", result); } #endregion #region FormatVolumeName Tests [Fact] public void FormatVolumeName_StandardLibrary_ReturnsVolumeLabel() { var volume = CreateVolumeDto(name: "1", minNumber: 1); var result = _sut.FormatVolumeName(LibraryType.Manga, volume); Assert.Equal("Volume 1", result); } [Fact] public void FormatVolumeName_AlreadyHasVolumePrefix_DoesNotDuplicate() { var volume = CreateVolumeDto(name: "Volume 1", minNumber: 1); var result = _sut.FormatVolumeName(LibraryType.Manga, volume); Assert.Equal("Volume 1", result); } [Theory] [InlineData("Volume 1")] [InlineData("Vol. 1")] [InlineData("Vol 1")] [InlineData("V. 1")] public void FormatVolumeName_WithVariousPrefixes_DoesNotDuplicate(string volumeName) { var volume = CreateVolumeDto(name: volumeName, minNumber: 1); var result = _sut.FormatVolumeName(LibraryType.Manga, volume); Assert.Equal(volumeName, result); } [Fact] public void FormatVolumeName_SpecialVolume_ReturnsNull() { var volume = CreateVolumeDto(name: "Specials", minNumber: Parser.SpecialVolumeNumber); var result = _sut.FormatVolumeName(LibraryType.Manga, volume); Assert.Null(result); } [Fact] public void FormatVolumeName_WithCustomLabel_UsesProvidedLabel() { var volume = CreateVolumeDto(name: "1", minNumber: 1); var result = _sut.FormatVolumeName(LibraryType.Manga, volume, volumeLabel: "Band"); Assert.Equal("Band 1", result); } [Fact] public void FormatVolumeName_BookLibrary_WithTitleName_ReturnsTitleName() { var chapter = CreateChapterDto(titleName: "The Fellowship of the Ring"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.FormatVolumeName(LibraryType.Book, volume); Assert.Equal("The Fellowship of the Ring", result); } [Fact] public void FormatVolumeName_BookLibrary_LooseLeaf_ReturnsVolumeName() { var chapter = CreateChapterDto(titleName: "Some Title"); var volume = CreateVolumeDto( name: "0", minNumber: Parser.LooseLeafVolumeNumber, chapters: [chapter]); var result = _sut.FormatVolumeName(LibraryType.Book, volume); Assert.Equal("0", result); } [Fact] public void FormatVolumeName_BookLibrary_NoTitleName_ExtractsFromRange() { var chapter = CreateChapterDto(range: "Book Title.epub", titleName: null); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.FormatVolumeName(LibraryType.Book, volume); Assert.Equal("1 - Book Title", result); } [Fact] public void FormatVolumeName_BookLibrary_SpecialChapter_ReturnsNull() { var chapter = CreateChapterDto(isSpecial: true); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.FormatVolumeName(LibraryType.Book, volume); Assert.Null(result); } #endregion #region BuildFullTitle Tests [Fact] public void BuildFullTitle_NoVolume_ReturnsSeriesAndChapter() { var series = CreateSeriesDto("Hajime no Ippo"); var chapter = CreateChapterDto(range: "1448", title: "The Big Fight"); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume: null, chapter); Assert.Equal("Hajime no Ippo - Chapter 1448 - The Big Fight", result); } [Fact] public void BuildFullTitle_SpecialVolume_ReturnsSeriesAndChapter() { var series = CreateSeriesDto("One Piece"); var volume = CreateVolumeDto(name: "Specials", minNumber: Parser.SpecialVolumeNumber); var chapter = CreateChapterDto(range: "SP01", title: "Bonus", isSpecial: true); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter); Assert.StartsWith("One Piece - ", result); } [Fact] public void BuildFullTitle_LooseLeafVolume_SingleChapter_ReturnsSeriesOnly() { var series = CreateSeriesDto("My Series"); var chapter = CreateChapterDto(range: "1"); var volume = CreateVolumeDto( name: "0", minNumber: Parser.LooseLeafVolumeNumber, chapters: [chapter]); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter); Assert.Equal("My Series", result); } [Fact] public void BuildFullTitle_LooseLeafVolume_MultipleChapters_IncludesChapter() { var series = CreateSeriesDto("My Series"); var chapter1 = CreateChapterDto(range: "1"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto( name: "0", minNumber: Parser.LooseLeafVolumeNumber, chapters: [chapter1, chapter2]); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter1); Assert.Equal("My Series - Chapter 1", result); } [Fact] public void BuildFullTitle_SingleChapterVolume_ReturnsSeriesAndVolume() { var series = CreateSeriesDto("Attack on Titan"); var chapter = CreateChapterDto(range: "1"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter); Assert.Equal("Attack on Titan - Volume 1", result); } [Fact] public void BuildFullTitle_MultipleChapterVolume_IncludesVolumeAndChapter() { var series = CreateSeriesDto("Naruto"); var chapter1 = CreateChapterDto(range: "1", title: "Uzumaki Naruto"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter1); Assert.Equal("Naruto - Volume 1 - Chapter 1 - Uzumaki Naruto", result); } [Fact] public void BuildFullTitle_Comic_UsesIssueFormat() { var series = CreateSeriesDto("Batman"); var chapter = CreateChapterDto(range: "1", title: "The Beginning"); var result = _sut.BuildFullTitle(LibraryType.Comic, series, volume: null, chapter); Assert.Equal("Batman - Issue #1 - The Beginning", result); } [Fact] public void BuildFullTitle_Book_SingleChapterVolume_UsesBookTitle() { var series = CreateSeriesDto("Lord of the Rings"); var chapter = CreateChapterDto(titleName: "The Fellowship of the Ring"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.BuildFullTitle(LibraryType.Book, series, volume, chapter); Assert.Equal("Lord of the Rings - The Fellowship of the Ring", result); } [Fact] public void BuildFullTitle_WithCustomLabels_UsesProvidedLabels() { var series = CreateSeriesDto("Manga Series"); var chapter = CreateChapterDto(range: "5"); var result = _sut.BuildFullTitle( LibraryType.Manga, series, volume: null, chapter, chapterLabel: "Kapitel"); Assert.Equal("Manga Series - Kapitel 5", result); } [Fact] public void BuildFullTitle_RedundantChapterTitle_DoesNotDuplicate() { var series = CreateSeriesDto("Hajime no Ippo"); var chapter = CreateChapterDto(range: "1448", title: "Chapter 1448"); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume: null, chapter); Assert.Equal("Hajime no Ippo - Chapter 1448", result); } [Fact] public void BuildFullTitle_VolumeAlreadyHasPrefix_DoesNotDuplicate() { var series = CreateSeriesDto("My Series"); var chapter1 = CreateChapterDto(range: "1"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "Volume 1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume, chapter1); Assert.Equal("My Series - Volume 1 - Chapter 1", result); } #endregion #region Edge Cases [Theory] [InlineData("")] [InlineData(" ")] public void FormatChapterTitle_EmptyTitle_TreatedAsNull(string title) { var result = _sut.FormatChapterTitle(LibraryType.Manga, isSpecial: false, "1", title); Assert.Equal("Chapter 1", result); } [Fact] public void FormatVolumeName_EmptyChaptersList_ReturnsVolumeName() { var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: []); var result = _sut.FormatVolumeName(LibraryType.Book, volume); Assert.Equal("1", result); } [Theory] [InlineData("CHAPTER 5", "5", "Chapter 5")] [InlineData("chapter 5", "5", "Chapter 5")] [InlineData("ISSUE #10", "10", "Issue #10")] public void FormatChapterTitle_CaseInsensitiveRedundancyCheck( string title, string range, string expected) { var libraryType = title.StartsWith("ISSUE", StringComparison.OrdinalIgnoreCase) ? LibraryType.Comic : LibraryType.Manga; var result = _sut.FormatChapterTitle(libraryType, isSpecial: false, range, title); Assert.Equal(expected, result); } [Fact] public void BuildFullTitle_NullVolume_NullChapterTitle_HandlesGracefully() { var series = CreateSeriesDto("Series Name"); var chapter = CreateChapterDto(range: "1", title: null); var result = _sut.BuildFullTitle(LibraryType.Manga, series, volume: null, chapter); Assert.Equal("Series Name - Chapter 1", result); } #endregion #region Helper Methods private static SeriesDto CreateSeriesDto(string name) { return new SeriesDto { Id = 1, Name = name, LibraryId = 1 }; } private static VolumeDto CreateVolumeDto( string name, float minNumber, ICollection? chapters = null) { return new VolumeDto { Id = 1, Name = name, MinNumber = minNumber, MaxNumber = minNumber, Chapters = chapters ?? new List() }; } private static ChapterDto CreateChapterDto( string range = "1", string? title = null, string? titleName = null, bool isSpecial = false) { return new ChapterDto { Id = 1, Range = range, Title = title ?? string.Empty, TitleName = titleName ?? string.Empty, IsSpecial = isSpecial, VolumeId = 1, Files = new List() }; } private static ReadingListItemDto CreateReadingListItemDto( LibraryType libraryType, MangaFormat format, string? chapterNumber, string? volumeNumber, string? chapterTitleName, bool isSpecial) { return new ReadingListItemDto { Id = 1, Order = 1, ChapterId = 1, SeriesId = 1, VolumeId = 1, LibraryId = 1, LibraryType = libraryType, SeriesFormat = format, ChapterNumber = chapterNumber, VolumeNumber = volumeNumber, ChapterTitleName = chapterTitleName, IsSpecial = isSpecial, SeriesName = "Test Series", PagesRead = 0, PagesTotal = 100 }; } #endregion #region BuildChapterTitle Tests [Fact] public void BuildChapterTitle_SingleChapterVolume_ReturnsVolumeOnly() { var chapter = CreateChapterDto(range: "1"); var volume = CreateVolumeDto(name: "5", minNumber: 5, chapters: [chapter]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter); Assert.Equal("Volume 5", result); } [Fact] public void BuildChapterTitle_MultipleChapterVolume_ReturnsVolumeAndChapter() { var chapter1 = CreateChapterDto(range: "1", title: "The Beginning"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter1); Assert.Equal("Volume 1 - Chapter 1 - The Beginning", result); } [Fact] public void BuildChapterTitle_SpecialVolume_ReturnsChapterOnly() { var chapter = CreateChapterDto(range: "SP01", title: "Bonus", isSpecial: true); var volume = CreateVolumeDto(name: "Specials", minNumber: Parser.SpecialVolumeNumber, chapters: [chapter]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter); Assert.NotEmpty(result); Assert.DoesNotContain("Volume", result); } [Fact] public void BuildChapterTitle_LooseLeafVolume_SingleChapter_ReturnsEmpty() { var chapter = CreateChapterDto(range: "1"); var volume = CreateVolumeDto(name: "0", minNumber: Parser.LooseLeafVolumeNumber, chapters: [chapter]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter); Assert.Equal(string.Empty, result); } [Fact] public void BuildChapterTitle_LooseLeafVolume_MultipleChapters_ReturnsChapterOnly() { var chapter1 = CreateChapterDto(range: "1"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "0", minNumber: Parser.LooseLeafVolumeNumber, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter1); Assert.Equal("Chapter 1", result); } [Fact] public void BuildChapterTitle_Comic_SingleChapterVolume_ReturnsVolumeOnly() { var chapter = CreateChapterDto(range: "1"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.BuildChapterTitle(LibraryType.Comic, volume, chapter); Assert.Equal("Volume 1", result); } [Fact] public void BuildChapterTitle_Comic_MultipleChapterVolume_UsesIssueFormat() { var chapter1 = CreateChapterDto(range: "1"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Comic, volume, chapter1); Assert.Equal("Volume 1 - Issue #1", result); } [Fact] public void BuildChapterTitle_Book_SingleChapterVolume_WithTitleName_ReturnsTitleName() { var chapter = CreateChapterDto(titleName: "The Fellowship of the Ring"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter]); var result = _sut.BuildChapterTitle(LibraryType.Book, volume, chapter); Assert.Equal("The Fellowship of the Ring", result); } [Fact] public void BuildChapterTitle_Book_MultipleChapterVolume_ReturnsVolumeAndBook() { var chapter1 = CreateChapterDto(range: "1", title: "Part One"); var chapter2 = CreateChapterDto(range: "2", title: "Part Two"); var volume = CreateVolumeDto(name: "1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Book, volume, chapter1); Assert.Contains("Book Part One", result); } [Fact] public void BuildChapterTitle_WithCustomLabels_UsesProvidedLabels() { var chapter1 = CreateChapterDto(range: "5"); var chapter2 = CreateChapterDto(range: "6"); var volume = CreateVolumeDto(name: "2", minNumber: 2, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle( LibraryType.Manga, volume, chapter1, volumeLabel: "Band", chapterLabel: "Kapitel"); Assert.Equal("Band 2 - Kapitel 5", result); } [Fact] public void BuildChapterTitle_VolumeAlreadyHasPrefix_DoesNotDuplicate() { var chapter1 = CreateChapterDto(range: "1"); var chapter2 = CreateChapterDto(range: "2"); var volume = CreateVolumeDto(name: "Volume 1", minNumber: 1, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter1); Assert.Equal("Volume 1 - Chapter 1", result); Assert.DoesNotContain("Volume Volume", result); } [Fact] public void BuildChapterTitle_RedundantChapterTitle_DoesNotDuplicate() { var chapter1 = CreateChapterDto(range: "1448", title: "Chapter 1448"); var chapter2 = CreateChapterDto(range: "1449"); var volume = CreateVolumeDto(name: "100", minNumber: 100, chapters: [chapter1, chapter2]); var result = _sut.BuildChapterTitle(LibraryType.Manga, volume, chapter1); Assert.Equal("Volume 100 - Chapter 1448", result); } #endregion #region FormatReadingListItemTitle Tests // Manga Library & Archive [Theory] [InlineData(Parser.DefaultChapter, "1", null, false, "Volume 1")] [InlineData("1", "1", null, false, "Chapter 1")] [InlineData("1", "1", "The Title", false, "Chapter 1")] [InlineData(Parser.DefaultChapter, "1", "The Title", false, "Volume 1")] [InlineData(Parser.DefaultChapter, Parser.LooseLeafVolume, "The Title", false, "The Title")] public void FormatReadingListItemTitle_MangaArchive_ReturnsExpected( string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } // Comic Library & Archive [Theory] [InlineData(Parser.DefaultChapter, "1", null, false, "Volume 1")] [InlineData("1", "1", null, false, "Issue #1")] [InlineData("1", "1", "The Title", false, "Issue #1")] [InlineData(Parser.DefaultChapter, "1", "The Title", false, "Volume 1")] [InlineData(Parser.DefaultChapter, Parser.LooseLeafVolume, "The Title", false, "The Title")] public void FormatReadingListItemTitle_ComicArchive_ReturnsExpected( string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( LibraryType.Comic, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Fact] public void FormatReadingListItemTitle_ComicArchive_Special_ReturnsChapterNumber() { var result = _sut.FormatReadingListItemTitle( LibraryType.Comic, MangaFormat.Archive, chapterNumber: "The Special Title", volumeNumber: Parser.LooseLeafVolume, chapterTitleName: null, isSpecial: true); Assert.Equal("The Special Title", result); } // Book Library & Archive [Theory] [InlineData(Parser.DefaultChapter, "1", null, false, "Volume 1")] [InlineData("1", "1", null, false, "Book 1")] [InlineData("1", "1", "The Title", false, "Book 1")] [InlineData(Parser.DefaultChapter, "1", "The Title", false, "Volume 1")] [InlineData(Parser.DefaultChapter, Parser.LooseLeafVolume, "The Title", false, "The Title")] public void FormatReadingListItemTitle_BookArchive_ReturnsExpected( string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } // Manga Library & EPUB [Theory] [InlineData(Parser.DefaultChapter, "1", null, false, "Volume 1")] [InlineData("1", "1", null, false, "Volume 1")] [InlineData("1", "1", "The Title", false, "Volume 1")] [InlineData(Parser.DefaultChapter, "1", "The Title", false, "The Title")] [InlineData(Parser.DefaultChapter, Parser.LooseLeafVolume, "The Title", false, "The Title")] public void FormatReadingListItemTitle_MangaEpub_ReturnsExpected( string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Epub, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } // Book Library & EPUB [Theory] [InlineData(Parser.DefaultChapter, "1", null, false, "Volume 1")] [InlineData("1", "1", null, false, "Volume 1")] [InlineData("1", "1", "The Title", false, "Volume 1")] [InlineData(Parser.DefaultChapter, "1", "The Title", false, "The Title")] [InlineData(Parser.DefaultChapter, Parser.LooseLeafVolume, "The Title", false, "The Title")] public void FormatReadingListItemTitle_BookEpub_ReturnsExpected( string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Epub, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, "5", "1", null, false, "Chapter 5")] [InlineData(LibraryType.Manga, "10.5", "1", null, false, "Chapter 10.5")] [InlineData(LibraryType.Image, "3", "1", null, false, "Chapter 3")] public void FormatReadingListItemTitle_Manga_ReturnsChapterFormat( LibraryType libraryType, string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Comic, "1", "1", null, false, "Issue #1")] [InlineData(LibraryType.Comic, "25", "1", null, false, "Issue #25")] [InlineData(LibraryType.ComicVine, "100", "1", null, false, "Issue #100")] public void FormatReadingListItemTitle_Comic_ReturnsIssueFormat( LibraryType libraryType, string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Book, "1", "1", null, false, "Book 1")] [InlineData(LibraryType.LightNovel, "5", "1", null, false, "Book 5")] public void FormatReadingListItemTitle_Book_ReturnsBookFormat( LibraryType libraryType, string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, Parser.DefaultChapter, "5", null, false, "Volume 5")] [InlineData(LibraryType.Comic, Parser.DefaultChapter, "10", null, false, "Volume 10")] public void FormatReadingListItemTitle_DefaultChapterWithVolume_ReturnsVolumeOnly( LibraryType libraryType, string chapterNumber, string volumeNumber, string? chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, Parser.DefaultChapter, Parser.LooseLeafVolume, "My Special Title", false, "My Special Title")] [InlineData(LibraryType.Comic, Parser.DefaultChapter, Parser.LooseLeafVolume, "Origin Story", false, "Origin Story")] public void FormatReadingListItemTitle_DefaultChapterWithTitleName_ReturnsTitleName( LibraryType libraryType, string chapterNumber, string volumeNumber, string chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Theory] [InlineData(LibraryType.Manga, "SP01", "0", "Bonus Chapter", true, "Bonus Chapter")] [InlineData(LibraryType.Comic, "Special", "0", "Annual #1", true, "Annual #1")] public void FormatReadingListItemTitle_SpecialWithTitleName_ReturnsTitleName( LibraryType libraryType, string chapterNumber, string volumeNumber, string chapterTitleName, bool isSpecial, string expected) { var result = _sut.FormatReadingListItemTitle( libraryType, MangaFormat.Archive, chapterNumber, volumeNumber, chapterTitleName, isSpecial); Assert.Equal(expected, result); } [Fact] public void FormatReadingListItemTitle_SpecialWithoutTitleName_ReturnsCleanedChapterNumber() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: "SP01 - Bonus", volumeNumber: "0", chapterTitleName: null, isSpecial: true); // Should return cleaned version of chapter number Assert.NotNull(result); Assert.NotEmpty(result); } [Fact] public void FormatReadingListItemTitle_WithCustomLabels_UsesProvidedLabels() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: "5", volumeNumber: "1", chapterTitleName: null, isSpecial: false, chapterLabel: "Kapitel"); Assert.Equal("Kapitel 5", result); } [Fact] public void FormatReadingListItemTitle_VolumeOnlyWithCustomLabel_UsesProvidedLabel() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: Parser.DefaultChapter, volumeNumber: "3", chapterTitleName: null, isSpecial: false, volumeLabel: "Band"); Assert.Equal("Band 3", result); } #endregion #region FormatReadingListItemTitle - Epub Tests [Fact] public void FormatReadingListItemTitle_Epub_DefaultChapterWithTitleName_ReturnsTitleName() { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Epub, chapterNumber: Parser.DefaultChapter, volumeNumber: "1", chapterTitleName: "The Fellowship of the Ring", isSpecial: false); Assert.Equal("The Fellowship of the Ring", result); } [Fact] public void FormatReadingListItemTitle_Epub_DefaultChapterNoTitleName_ReturnsVolume() { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Epub, chapterNumber: Parser.DefaultChapter, volumeNumber: "1", chapterTitleName: null, isSpecial: false); Assert.Equal("Volume 1", result); } [Fact] public void FormatReadingListItemTitle_Epub_SpecialVolume_ReturnsCleanedChapter() { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Epub, chapterNumber: "Bonus Content", volumeNumber: Parser.SpecialVolume, chapterTitleName: null, isSpecial: false); Assert.Equal("Bonus Content", result); } [Fact] public void FormatReadingListItemTitle_Epub_RegularChapter_ReturnsVolumeWithChapter() { var result = _sut.FormatReadingListItemTitle( LibraryType.Book, MangaFormat.Epub, chapterNumber: "5", volumeNumber: "1", chapterTitleName: null, isSpecial: false); Assert.Equal("Volume 5", result); } #endregion #region FormatReadingListItemTitle - DTO Overload Tests [Fact] public void FormatReadingListItemTitle_WithDto_ExtractsFieldsCorrectly() { var item = CreateReadingListItemDto( libraryType: LibraryType.Manga, format: MangaFormat.Archive, chapterNumber: "42", volumeNumber: "5", chapterTitleName: null, isSpecial: false); var result = _sut.FormatReadingListItemTitle(item); Assert.Equal("Chapter 42", result); } [Fact] public void FormatReadingListItemTitle_WithDto_SpecialItem_ReturnsTitleName() { var item = CreateReadingListItemDto( libraryType: LibraryType.Manga, format: MangaFormat.Archive, chapterNumber: "SP01", volumeNumber: "0", chapterTitleName: "Bonus Chapter", isSpecial: true); var result = _sut.FormatReadingListItemTitle(item); Assert.Equal("Bonus Chapter", result); } [Fact] public void FormatReadingListItemTitle_WithDto_EpubWithTitle_ReturnsTitleName() { var item = CreateReadingListItemDto( libraryType: LibraryType.Book, format: MangaFormat.Epub, chapterNumber: Parser.DefaultChapter, volumeNumber: "1", chapterTitleName: "The Hobbit", isSpecial: false); var result = _sut.FormatReadingListItemTitle(item); Assert.Equal("The Hobbit", result); } [Fact] public void FormatReadingListItemTitle_WithDto_ComicFormat_ReturnsIssue() { var item = CreateReadingListItemDto( libraryType: LibraryType.Comic, format: MangaFormat.Archive, chapterNumber: "15", volumeNumber: "1", chapterTitleName: null, isSpecial: false); var result = _sut.FormatReadingListItemTitle(item); Assert.Equal("Issue #15", result); } #endregion [Fact] public void FormatReadingListItemTitle_NullChapterNumber_HandlesGracefully() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: null, volumeNumber: "1", chapterTitleName: "Fallback Title", isSpecial: false); // Should fall back to title name or handle gracefully Assert.NotNull(result); } [Fact] public void FormatReadingListItemTitle_EmptyStrings_HandlesGracefully() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: "", volumeNumber: "", chapterTitleName: "", isSpecial: false); // Should not throw and should return something Assert.NotNull(result); } [Theory] [InlineData("1.5")] [InlineData("10")] [InlineData("100.25")] public void FormatReadingListItemTitle_NumericChapterNumbers_PreservedAsIs(string chapterNumber) { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: chapterNumber, volumeNumber: "1", chapterTitleName: null, isSpecial: false); Assert.Contains(chapterNumber, result); } [Fact] public void FormatReadingListItemTitle_NonNumericChapterNumber_GetsCleaned() { var result = _sut.FormatReadingListItemTitle( LibraryType.Manga, MangaFormat.Archive, chapterNumber: "SP01 - Special Chapter", volumeNumber: "1", chapterTitleName: null, isSpecial: false); // Should clean the special title format Assert.NotNull(result); Assert.DoesNotContain(" - ", result.Replace("Chapter ", "")); } #region Extra Tests [Fact] public void BuildChapterTitle_ManualTest1() { var chapterDto = new ChapterDto { Id = 2002, Number = Parser.LooseLeafVolume, Range = Parser.LooseLeafVolume, MinNumber = Parser.LooseLeafVolumeNumber, MaxNumber = Parser.LooseLeafVolumeNumber, SortOrder = Parser.LooseLeafVolumeNumber, IsSpecial = false, Title = Parser.LooseLeafVolume, TitleName = "The Vexations of a Shut-In Vampire Princess, Vol. 4", Pages = 40, PagesRead = 20, CoverImageLocked = false, VolumeId = 1446, }; var volumeDto = new VolumeDto { Id = 1446, Number = 4, Name = "4", MinNumber = 4, MaxNumber = 4, Pages = 40, PagesRead = 20, SeriesId = 256, Chapters = [chapterDto], }; var chapterTitle = _sut.BuildChapterTitle(LibraryType.LightNovel, volumeDto, chapterDto); Assert.Equal("The Vexations of a Shut-In Vampire Princess, Vol. 4", chapterTitle); } [Fact] public void BuildChapterTitle_ManualTest2() { var chapterDto = new ChapterDto { Number = Parser.LooseLeafVolume, Range = Parser.LooseLeafVolume, Title = Parser.LooseLeafVolume, TitleName = "Accel World, Vol. 5: The Floating Starlight Bridge", MinNumber = Parser.LooseLeafVolumeNumber, MaxNumber = Parser.LooseLeafVolumeNumber, IsSpecial = false, }; var volumeDto = new VolumeDto { Number = 5, Name = "5", MinNumber = 5, MaxNumber = 5, Chapters = [chapterDto], }; var chapterTitle = _sut.BuildChapterTitle(LibraryType.LightNovel, volumeDto, chapterDto); Assert.Equal("Accel World, Vol. 5: The Floating Starlight Bridge", chapterTitle); } [Fact] public void BuildChapterTitle_ManualTest3() { var chapterDto = new ChapterDto { Number = Parser.LooseLeafVolume, Range = "After Sundown", Title = "After Sundown", MinNumber = Parser.LooseLeafVolumeNumber, MaxNumber = Parser.LooseLeafVolumeNumber, IsSpecial = true, }; var volumeDto = new VolumeDto { Number = Parser.SpecialVolumeNumber, Name = Parser.SpecialVolumeNumber.ToString(), MinNumber = Parser.SpecialVolumeNumber, MaxNumber = Parser.SpecialVolumeNumber, Chapters = [chapterDto], }; var chapterTitle = _sut.BuildChapterTitle(LibraryType.Book, volumeDto, chapterDto); Assert.Equal("After Sundown", chapterTitle); } [Fact] public void BuildChapterTitle_ManualTest4() { var chapterDto = new ChapterDto { Number = Parser.LooseLeafVolume, Range = "A Girl on the Shore (Umibe no Onnanoko)", Title = "A Girl on the Shore (Umibe no Onnanoko)", MinNumber = Parser.LooseLeafVolumeNumber, MaxNumber = Parser.LooseLeafVolumeNumber, IsSpecial = true, }; var volumeDto = new VolumeDto { Number = Parser.SpecialVolumeNumber, Name = Parser.SpecialVolumeNumber.ToString(), MinNumber = Parser.SpecialVolumeNumber, MaxNumber = Parser.SpecialVolumeNumber, Chapters = [chapterDto], }; var chapterTitle = _sut.BuildChapterTitle(LibraryType.Manga, volumeDto, chapterDto); Assert.Equal("A Girl on the Shore (Umibe no Onnanoko)", chapterTitle); } #endregion }