using System; using System.IO; using System.Linq; using AcDream.App.UI; using AcDream.Core.Journal; using AcDream.Runtime.Gameplay; namespace AcDream.App.Tests.UI; /// /// Campaign QJ slice QJ5: the per-character journal file. /// public sealed class JournalPersistenceTests : IDisposable { private static readonly DateTime Now = new(2026, 8, 21, 12, 0, 0, DateTimeKind.Utc); private readonly string _directory = Path.Combine(Path.GetTempPath(), "acdream-journal-" + Guid.NewGuid().ToString("N")); public void Dispose() { try { Directory.Delete(_directory, recursive: true); } catch (IOException) { /* the test already said what it needed to */ } } private (RuntimeJournalState Journal, JournalPersistence File, List Reports) New() { var journal = new RuntimeJournalState(); var reports = new List(); return (journal, new JournalPersistence(journal, _directory, reports.Add), reports); } [Fact] public void APageWrittenInOneSessionIsThereInTheNext() { // The whole point of the feature. (RuntimeJournalState first, JournalPersistence firstFile, _) = New(); firstFile.Load("Acdream"); first.NewPage(); first.UpdateCurrent("label", "A title", "Some notes."); Assert.True(firstFile.Save(Now)); first.Dispose(); (RuntimeJournalState second, JournalPersistence secondFile, _) = New(); secondFile.Load("Acdream"); JournalPage page = Assert.Single(second.View.Pages); Assert.Equal("A title", page.Title); Assert.Equal("Some notes.", page.Notes); second.Dispose(); } [Fact] public void EachCharacterGetsItsOwnFile() { // Sharing one file would show a character another's notes. (RuntimeJournalState journal, JournalPersistence file, _) = New(); file.Load("Acdream"); journal.NewPage(); journal.UpdateCurrent("a", "Acdream's page", string.Empty); file.Save(Now); file.Load("Someone Else"); Assert.Empty(journal.View.Pages); journal.Dispose(); } [Fact] public void ACharacterWithNoFileLoadsAnEmptyJournalWithoutComplaining() { // First-time use is the ordinary case, not an error. (RuntimeJournalState journal, JournalPersistence file, List reports) = New(); file.Load("Newcomer"); Assert.Empty(journal.View.Pages); Assert.Empty(reports); journal.Dispose(); } [Fact] public void AMalformedFileReportsAndLeavesTheJournalEmpty() { // Half-reading a notebook loses pages, and the next save would then // write that loss back over the original. (RuntimeJournalState journal, JournalPersistence file, List reports) = New(); Directory.CreateDirectory(_directory); File.WriteAllText( Path.Combine(_directory, JournalFile.FileNameFor("acdream", "Acdream")), " no page marker first\n"); file.Load("Acdream"); Assert.Equal(JournalFile.MalformedFileMessage, Assert.Single(reports)); Assert.Empty(journal.View.Pages); journal.Dispose(); } [Fact] public void SavingIsSkippedWhenNothingChanged() { // Retail's save fires on every hide; rewriting an unchanged file on // every tab switch is pure disk churn. (RuntimeJournalState journal, JournalPersistence file, _) = New(); file.Load("Acdream"); journal.NewPage(); Assert.True(file.Save(Now)); Assert.False(file.Save(Now)); journal.Dispose(); } [Fact] public void SavingBeforeAnyCharacterIsLoadedIsANoOp() { // The panel can be disposed before a character ever entered the world. (RuntimeJournalState journal, JournalPersistence file, _) = New(); Assert.False(file.Save(Now)); Assert.False(Directory.Exists(_directory)); journal.Dispose(); } [Fact] public void ARunningTimerPersistsItsREMAININGTime() { // Saving the value it started at would resurrect the full duration on // every reload. (RuntimeJournalState journal, JournalPersistence file, _) = New(); file.Load("Acdream"); journal.NewPage(); journal.SetTimer(0, 1, 0); journal.StartTimer(Now); file.Save(Now.AddMinutes(30)); journal.Dispose(); (RuntimeJournalState reloaded, JournalPersistence reloadedFile, _) = New(); reloadedFile.Load("Acdream"); Assert.Equal(1800d, Assert.Single(reloaded.View.Pages).RunningTimerSeconds); reloaded.Dispose(); } [Fact] public void CloseSavesAndForgetsTheCharacter() { (RuntimeJournalState journal, JournalPersistence file, _) = New(); file.Load("Acdream"); journal.NewPage(); file.Close(Now); Assert.Null(file.CurrentPath); Assert.False(file.Save(Now)); journal.Dispose(); } [Fact] public void AnUnwritableDirectoryReportsRatherThanThrowing() { // Losing a note is bad; taking the client down while the player is // writing one is worse. var journal = new RuntimeJournalState(); var reports = new List(); string blocked = Path.Combine(_directory, "blocked"); Directory.CreateDirectory(_directory); File.WriteAllText(blocked, "not a directory"); var file = new JournalPersistence(journal, blocked, reports.Add); file.Load("Acdream"); journal.NewPage(); Assert.False(file.Save(Now)); Assert.NotEmpty(reports); journal.Dispose(); } }