using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.VisualTree;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Session;
using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.Ssh;
using DodoSSH.Client.Storage;
using DodoSSH.Client.Terminal;
using NSubstitute;

namespace DodoSSH.Client.App.Layout.Tests;

/// <summary>
/// The session shell's right-hand column: its two widths, and what a long address does to the row it shares.
/// </summary>
/// <remarks>
/// Worth a suite of its own since v5c-4, which gave this control two things it did not have: a session block
/// at its head — the address, and the cross-surface button, both inherited from the 60-pixel header row that
/// pass retired — and a closed state. The first is exactly the shape this harness exists for, a fixed-width
/// column holding a string of unbounded length beside a button that must stay clickable; the second is a
/// width the rest of the window has to cope with, and <c>MainWindow.axaml</c> copes with it by asking this
/// control how wide it is rather than by knowing.
/// </remarks>
public sealed class SessionSidebarTests : IAsyncLifetime
{
    /// <summary>The widths <c>SessionSidebar.axaml</c> declares for its two states.</summary>
    private const double OpenWidth = 300;

    /// <inheritdoc cref="OpenWidth" />
    private const double RailWidth = 34;

    /// <remarks>
    /// Long on purpose, and longer than the column is wide at this font: the address is the one string here
    /// whose length nobody controls, and it shares its row with the button that closes the column.
    /// </remarks>
    private const string LongAddress = "a-very-long-deploy-account@db-primary.eu-west-1.internal.example:22022";

    private string directory = null!;
    private ClientCacheFactory caches = null!;
    private TerminalWorkspace workspace = null!;
    private MainWindowViewModel shell = null!;

    private static CancellationToken Token => TestContext.Current.CancellationToken;

    /// <inheritdoc />
    public ValueTask InitializeAsync()
    {
        // A profile of this test's own rather than ClientPaths.Default: closing the sidebar is written
        // through to disk — see ClientSettings.SessionSidebarOpen — and a suite that used the default paths
        // would be editing the preferences of whoever ran it.
        directory = Path.Combine(Path.GetTempPath(), $"dodossh-sidebar-{Guid.CreateVersion7():N}");
        caches = ClientCacheFactory.ForMemory($"session-sidebar-{Guid.CreateVersion7():N}");

        workspace = new TerminalWorkspace(
            new InMemoryTerminalAssetProvider(new Dictionary<string, TerminalAsset>(StringComparer.Ordinal)),
            Substitute.For<ISshConnectionFactory>(),
            TimeProvider.System);

        shell = new MainWindowViewModel(
            new ClientPaths(directory),
            caches,
            workspace,
            new VaultKnownHostStore(),
            Substitute.For<IDeviceKeyStore>(),
            (_, _) => throw new NotSupportedException("nothing here signs in"),
            TimeProvider.System,
            Substitute.For<ISftpSessionFactory>())
        {
            State = ShellState.Unlocked,
        };

        return ValueTask.CompletedTask;
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        await shell.DisposeAsync();
        await workspace.DisposeAsync();
        caches.Dispose();

        if (Directory.Exists(directory))
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    /// <remarks>
    /// The address trims and the close button stays where it is; that is the whole claim. Asserted through
    /// <see cref="LayoutHarness.Unreachable"/> rather than by reading the address's own width, because what
    /// matters is not how much of the string is shown — an ellipsis is an honest answer — but that nothing
    /// beside it was pushed out of the column to make room.
    /// </remarks>
    [Fact]
    public async Task TheColumnIsThreeHundredWide_AndALongAddressPushesNothingOutOfIt()
    {
        await OnTheSidebarAsync((sidebar, window) =>
        {
            // DesiredSize rather than Bounds, and that distinction is the control's own: the width lives on
            // the Border inside it, so what the surrounding "Auto" column is given — and what this asks for
            // — is what the control asks for, not how wide a host window happened to stretch it.
            sidebar.DesiredSize.Width.ShouldBe(OpenWidth);

            shell.SessionAddress.ShouldBe(LongAddress, "the fixture selected a tab with one");

            LayoutHarness.Unreachable(window).ShouldBeEmpty();
        });
    }

    /// <remarks>
    /// The cross-surface button that used to live in the header row. Read off the control rather than off the
    /// view model, so a row bound to the wrong property — or to nothing, which a compiled binding would still
    /// draw as an empty button — fails this.
    /// </remarks>
    [Fact]
    public async Task TheCrossSurfaceButtonNamesTheOtherSurface()
    {
        await OnTheSidebarAsync((sidebar, _) =>
        {
            var button = sidebar.GetVisualDescendants()
                .OfType<Button>()
                .First(candidate => candidate.Classes.Contains("headerghost"));

            button.Content.ShouldBe("Open SFTP");
        });
    }

    /// <remarks>
    /// Closed, the control is still drawn and is still the thing the window asks for a width — see
    /// <c>MainWindow.axaml</c>'s own "Auto" column. What it must not be is nothing: a rail with the way back
    /// on it is the difference between a panel somebody closed and a panel somebody lost.
    /// </remarks>
    [Fact]
    public async Task ClosingTheColumnLeavesTheRailThatBringsItBack()
    {
        await OnTheSidebarAsync((sidebar, window) =>
        {
            shell.IsSessionSidebarOpen = false;
            LayoutHarness.Settle(window, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);

            sidebar.DesiredSize.Width.ShouldBe(RailWidth);

            var grip = sidebar.GetVisualDescendants()
                .OfType<Button>()
                .Single(candidate => candidate.Classes.Contains("sidebargrip") && candidate.IsEffectivelyVisible);

            var centre = grip.TranslatePoint(new Point(grip.Bounds.Width / 2, grip.Bounds.Height / 2), window)
                ?? throw new InvalidOperationException("the grip is not in this window's tree");

            window.MouseDown(centre, MouseButton.Left);
            window.MouseUp(centre, MouseButton.Left);

            shell.IsSessionSidebarOpen.ShouldBeTrue("the rail's own button is what reopens the column");

            LayoutHarness.Settle(window, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);
            sidebar.DesiredSize.Width.ShouldBe(OpenWidth);
        });
    }

    private Task OnTheSidebarAsync(Action<SessionSidebar, Window> body) =>
        LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                shell.Tabs.Add(new TerminalTabViewModel(1, "db-primary", LongAddress));
                shell.SelectTabCommand.Execute(shell.Tabs[0]);

                var sidebar = new SessionSidebar { DataContext = shell, ShowsSnips = true };
                var window = LayoutHarness.HostAtMinimumSize(
                    sidebar, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);

                try
                {
                    body(sidebar, window);
                }
                finally
                {
                    window.Close();
                }
            },
            Token);
}
