using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Interactivity;
using Avalonia.Threading;
using IFSEngine.Model;
using IFSRenderer.Shared;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// The document-handling and concurrency reports: "save crashes / save as works", and
/// "while adding nodes I somehow broke the renderer, it stopped rendering".
/// </summary>
public class DocumentAndRaceTests
{
/// <summary>
/// Startup loads a template from the install tree, which is a read-only squashfs mount in
/// an AppImage. Treating it as the open document is what made Ctrl+S write into the mount.
/// </summary>
[AvaloniaFact]
public void A_shipped_template_is_never_the_open_document()
{
string template = Path.Combine(AssetPaths.Library, "Templates", "nebula_sample.ifsjson");
Assert.True(File.Exists(template), $"expected a template at {template}");
Assert.True(MainWindow.IsShippedTemplate(template),
"a file under the shipped Library/ must be treated as a template");
Assert.False(MainWindow.IsShippedTemplate(
Path.Combine(Path.GetTempPath(), "my-fractal.ifsjson")),
"a file the user chose elsewhere is a document, not a template");
}
/// <summary>
/// Save must never take the process down. It runs from an async void click handler, where
/// an escaping exception is fatal, and the target can be unwritable for reasons the app
/// does not control.
/// </summary>
[AvaloniaFact]
public async Task Saving_to_an_unwritable_path_reports_instead_of_throwing()
{
(MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();
// A directory where the file cannot be created: the path names a directory, not a file.
string unwritable = Path.Combine(Path.GetTempPath(),
"ifsrenderer-unwritable-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(unwritable);
// The serializer is what the save path calls; prove the failure is a real one first.
await Assert.ThrowsAnyAsync<Exception>(
() => IfsNodesSerializer.SaveJsonFileAsync(queue.Current, unwritable));
// Ctrl+S on a freshly started app is the reported crash. The template is not the
// document, so this falls through to Save as and must not throw either way.
var save = window.FindControl<MenuItem>("MenuSave");
Assert.NotNull(save);
Exception escaped = Record.Exception(() =>
{
save.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
Dispatcher.UIThread.RunJobs();
});
Assert.Null(escaped);
}
/// <summary>
/// Startup must not leave the template as the edited file — the whole point of the fix.
/// </summary>
[AvaloniaFact]
public void Startup_from_a_template_leaves_no_document_to_overwrite()
{
(MainWindow window, _) = MainWindowHarness.Start();
// The window titles itself after the file it loaded...
Assert.Contains("nebula", window.Title, StringComparison.OrdinalIgnoreCase);
// ...but the template's mtime must be untouched by a save attempt.
string template = Path.Combine(AssetPaths.Library, "Templates", "nebula_sample.ifsjson");
DateTime before = File.GetLastWriteTimeUtc(template);
var save = window.FindControl<MenuItem>("MenuSave");
save.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
Dispatcher.UIThread.RunJobs();
Assert.Equal(before, File.GetLastWriteTimeUtc(template));
}
/// <summary>
/// The mechanism behind "adding nodes stopped the renderer".
/// </summary>
/// <remarks>
/// <para>
/// The shipped code rebuilt the UI on the line after <c>Post</c>, so the UI thread walked
/// <c>Iterators</c> while the render thread was mutating the very same collection. This
/// proves that pairing actually throws — which is what reached the dispatch loop, paused
/// it, and left the app looking frozen. It is the missing link between the ordering bug and
/// the stall, and without it the diagnosis would only be plausible.
/// </para>
/// <para>
/// The ordering fix removes the pairing entirely: the rebuild is posted from inside the
/// queued action, so it can never run alongside it.
/// </para>
/// </remarks>
[Fact]
public async Task Enumerating_iterators_while_they_are_mutated_throws()
{
var plugins = await Task.WhenAll(Directory
.GetFiles(AssetPaths.Transforms, "*.ifstf", SearchOption.AllDirectories)
.Take(2)
.Select(TransformPlugin.FromFile));
Assert.NotEmpty(plugins);
var fractal = new IFS();
for (int i = 0; i < 4; i++)
fractal.AddIterator(new Iterator(plugins[0]), false);
Assert.ThrowsAny<InvalidOperationException>(() =>
{
foreach (Iterator _ in fractal.Iterators)
fractal.AddIterator(new Iterator(plugins[0]), false);
});
}
}