using FluentAssertions;
using Nostos.Backend.Services.Portability;
using Nostos.Shared.Enums;
using Xunit;
namespace Nostos.Backend.Tests.Portability;
///
/// An archive written before rating, favourite and review moved onto the work
/// kept them on every edition. Reading it must lose none of them: it goes
/// through the same roll-up as the migration and as "link as one book".
///
public sealed class PortableLegacyBookValuesTests
{
private static readonly DateTime Jan = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime Jun = new(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
[Fact]
public void An_old_archive_with_disagreeing_editions_keeps_both_values()
{
var work = Guid.NewGuid();
var older = Edition(work, rating: 4, review: "Older words", favourite: false, lastRead: Jan);
var newer = Edition(work, rating: 2, review: "Newer words", favourite: true, lastRead: Jun);
var current = PortableLegacyBookValues.ToCurrent(Data(5, [work], [older, newer]));
var book = current.Works.Single();
(book.Rating, book.PersonalReview, book.IsFavorite).Should().Be((2, "Newer words", true));
var olderNow = current.Books.Single(b => b.Id == older.Id).Progress;
olderNow.Rating.Should().Be(4, "the edition that lost keeps its rating as an exception");
olderNow.PersonalReview.Should().Be("Older words");
var newerNow = current.Books.Single(b => b.Id == newer.Id).Progress;
(newerNow.Rating, newerNow.PersonalReview).Should().Be((0, null));
current.Books.Should().OnlyContain(b => !b.Progress.IsFavorite, "the favourite is the book's now");
current.Version.Should().Be(5, "the declared version still agrees with the manifest");
}
[Fact]
public void An_old_archive_with_one_rated_edition_gives_the_book_that_rating()
{
var work = Guid.NewGuid();
var rated = Edition(work, rating: 5, review: null, favourite: false, lastRead: null);
var bare = Edition(work, rating: 0, review: null, favourite: false, lastRead: Jun);
var current = PortableLegacyBookValues.ToCurrent(Data(4, [work], [rated, bare]));
current.Works.Single().Rating.Should().Be(5);
current.Books.Should().OnlyContain(b => b.Progress.Rating == 0);
}
[Fact]
public void A_current_archive_is_left_exactly_as_it_is()
{
var work = Guid.NewGuid();
var edition = Edition(work, rating: 3, review: null, favourite: false, lastRead: Jan);
var data = Data(PortableArchiveFormat.DataVersion, [work], [edition]);
PortableLegacyBookValues.ToCurrent(data).Should().BeSameAs(data);
}
[Fact]
public void A_current_archive_with_values_on_a_work_below_version_six_is_refused()
{
var work = Guid.NewGuid();
var data = Data(5, [work], [Edition(work, 0, null, false, null)]);
data = data with { Works = [data.Works.Single() with { Rating = 4 }] };
var act = () => PortableArchiveValidation.ValidatePortableData(data);
act.Should().Throw().Which.Code.Should().Be("unexpected_version_data");
}
[Fact]
public void A_version_six_archive_that_still_carries_an_edition_favourite_is_refused()
{
var work = Guid.NewGuid();
var data = Data(6, [work], [Edition(work, 0, null, favourite: true, lastRead: null)]);
var act = () => PortableArchiveValidation.ValidatePortableData(data);
act.Should().Throw().Which.Code.Should().Be("unexpected_version_data");
}
private static PortableBook Edition(Guid work, int rating, string? review, bool favourite, DateTime? lastRead) =>
new(
Guid.NewGuid(), work, "ebook", nameof(BookStatus.Ready), null, "Book", null,
new PortableBookMetadata(null, null, null, null, null, null, null, null, null, null, null, null),
new PortableReadingProgress(null, 0, rating, favourite, review, lastRead, null),
Jan, null, null, null, null, null, null, false, false);
private static PortableLibraryData Data(int version, Guid[] works, PortableBook[] books) =>
new(
version,
works.Select(id => new PortableWork(id, "Book", null, Jan)).ToList(),
books.ToList(),
[], [], [], [], [], [], [], null,
WritingNotes: [], NoteImportBookLinks: [], ReadingListEntries: []);
}