using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Interactivity;
using Avalonia.Threading;
using IFSRenderer.AvaloniaShell.Controls;
using IFSRenderer.AvaloniaShell.Tests.Fakes;
using IFSRenderer.Shared;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// One test per bug reported against the <c>linux-port-ab998cd</c> test build.
/// </summary>
/// <remarks>
/// <para>
/// Each of these fails on the code as shipped, which is the only reason to trust it. The
/// gesture is performed through the real controls, then the queue is drained and the
/// dispatcher pumped — mirroring what the render thread and the UI thread actually do.
/// </para>
/// </remarks>
public class MainWindowRegressionTests
{
/// <summary>
/// Brings up the window against a GPU-less queue and waits for startup to settle.
/// </summary>
/// <remarks>
/// Settings are redirected to a scratch directory: the harness must never read or write
/// the developer's real <c>~/.config/ifsrenderer</c>, and the Welcome window has to stay
/// shut or startup would block on a dialog.
/// </remarks>
// ------------------------------------------------------------------ the bugs
/// <summary>
/// "I don't think it's normal behavior that when adding a node, it doesn't appear until
/// you add or remove another node too."
/// </summary>
/// <remarks>
/// The rebuild used to run on the line after <c>Post</c>, so it read the fractal before the
/// queued mutation had been applied and rendered the PREVIOUS state. Draining the queue
/// first is what this asserts: after the engine has run the edit, the list must show it.
/// </remarks>
[AvaloniaFact]
public void Adding_an_iterator_shows_it_without_a_further_edit()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
var list = window.FindControl<ListBox>("IteratorList");
int before = queue.Current.Iterators.Count;
MainWindowHarness.Click(window, "BtnAddIterator");
MainWindowHarness.Settle(queue);
Assert.Equal(before + 1, queue.Current.Iterators.Count);
Assert.Equal(before + 1, list.ItemCount);
}
/// <summary>Removing has the same ordering hazard as adding.</summary>
[AvaloniaFact]
public void Removing_an_iterator_updates_the_list_immediately()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
MainWindowHarness.Click(window, "BtnAddIterator");
MainWindowHarness.Settle(queue);
var list = window.FindControl<ListBox>("IteratorList");
int before = queue.Current.Iterators.Count;
Assert.True(before > 1, "need more than one iterator to remove");
MainWindowHarness.Click(window, "BtnRemoveIterator");
MainWindowHarness.Settle(queue);
Assert.Equal(before - 1, queue.Current.Iterators.Count);
Assert.Equal(before - 1, list.ItemCount);
}
/// <summary>
/// "this also applies to post processing on the right side" — the same ordering bug in
/// <c>AddPostFx</c>, reported as the worst of the set.
/// </summary>
[AvaloniaFact]
public void Adding_a_post_effect_shows_the_layer_immediately()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
var combo = window.FindControl<ComboBox>("PostFxAddCombo");
var layers = window.FindControl<StackPanel>("PostFxLayers");
Assert.True(combo.ItemCount > 0, "no post effects were loaded from the library");
int before = queue.Current.PostEffects.Count;
combo.SelectedIndex = 0;
MainWindowHarness.Settle(queue);
Assert.Equal(before + 1, queue.Current.PostEffects.Count);
Assert.Equal(before + 1, layers.Children.Count);
}
/// <summary>
/// "Orientation sliders don't do anything."
/// </summary>
/// <remarks>
/// The port wrote all four quaternion components and then normalised. Normalising projects
/// the value back onto the unit sphere, so a purely RADIAL change cancels exactly — and
/// from the identity pose (0,0,0,1) the W slider is precisely that, making it a no-op. The
/// canonical <c>CameraSettingsViewModel.WOrientation</c> writes one component and does not
/// normalise.
/// </remarks>
[AvaloniaFact]
public void Orientation_w_slider_actually_rotates_the_camera()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
queue.Current.Camera.Orientation = Quaternion.Identity;
var oriW = window.FindControl<ValueSlider>("OriW");
Assert.NotNull(oriW);
oriW.Value = queue.Current.Camera.Orientation.W;
MainWindowHarness.Click(oriW, "UpButton");
MainWindowHarness.Settle(queue);
Assert.NotEqual(Quaternion.Identity, queue.Current.Camera.Orientation);
Assert.Equal(1.001f, queue.Current.Camera.Orientation.W, 4);
}
/// <summary>Each orientation axis must move only its own component.</summary>
[AvaloniaFact]
public void Orientation_x_slider_leaves_the_other_components_alone()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
queue.Current.Camera.Orientation = Quaternion.Identity;
var oriX = window.FindControl<ValueSlider>("OriX");
oriX.Value = 0;
MainWindowHarness.Click(oriX, "UpButton");
MainWindowHarness.Settle(queue);
Quaternion q = queue.Current.Camera.Orientation;
Assert.Equal(0.001f, q.X, 4);
Assert.Equal(0f, q.Y);
Assert.Equal(0f, q.Z);
Assert.Equal(1f, q.W);
}
/// <summary>
/// "I somehow broke the renderer, it stopped rendering."
/// </summary>
/// <remarks>
/// A throw from the dispatch loop pauses rendering. That was reported through the status
/// bar, which the twice-a-second ticker overwrote within half a second, so the stall had no
/// visible cause. The error is now sticky until rendering is resumed.
/// </remarks>
[AvaloniaFact]
public void A_render_failure_stays_reported_and_clears_on_resume()
{
(MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
queue.SimulateRenderFailure(new InvalidOperationException("compute blew up"));
Dispatcher.UIThread.RunJobs();
Assert.NotNull(queue.LastError);
Assert.True(queue.Paused);
queue.Paused = false;
Assert.Null(queue.LastError);
}
}