using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Media;
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>
/// How the rail's own switcher, mode-dependent first row and user popover answer a pointer.
/// </summary>
/// <remarks>
/// <para>
/// v5b moved three things onto this control that used to be tested elsewhere or not at all: the SSH/SFTP/S3
/// choice that used to be the tab strip's own fixed tabs (see <c>TerminalTabsTests</c>, which used to hold
/// the equivalent of the first two facts below), the mode-dependent first row the design calls its own
/// <c>mode</c> prop, and the popover that replaced the strip's vault menu. This suite is this control's
/// counterpart to that one — a minimal shell with two tabs and no vault, for the reason
/// <c>TerminalTabsTests</c> gives: nothing here reads <c>Vault</c> except the popover's vault switches,
/// which this suite therefore does not open — that is <c>VaultVisibilityTests</c>' business, over the
/// commands themselves, and this suite would only be re-testing the same command through a slower door.
/// </para>
/// <para>
/// A <c>UserControl</c> in a bare window, for the same reason the palette's and the strip's suites are one:
/// <see cref="LayoutHarnessTests.WhyTheWindowItselfIsNeverShown"/>.
/// </para>
/// </remarks>
public sealed class NavRailTests : IAsyncLifetime
{
    private ClientCacheFactory caches = null!;
    private TerminalWorkspace workspace = null!;
    private MainWindowViewModel shell = null!;

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

    /// <inheritdoc />
    public ValueTask InitializeAsync()
    {
        caches = ClientCacheFactory.ForMemory($"navrail-{Guid.CreateVersion7():N}");

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

        shell = new MainWindowViewModel(
            ClientPaths.Default,
            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();
    }

    /// <summary>
    /// The switcher's three segments select the surface they name, and light up when it is the one showing.
    /// </summary>
    /// <remarks>
    /// Driven through the segments rather than through the commands directly, for the reason
    /// <c>TerminalTabsTests.TheFixedTabsSelectTheirSurface_AndVaultsRemembersItsPage</c> gave for its own
    /// three fixed tabs: what is being checked is that three buttons in the markup are wired to three
    /// different things, which three commands called by hand would not catch if two of the three were
    /// bound to the same one.
    /// </remarks>
    [Fact]
    public async Task TheSwitcherSegmentsSelectTheirSurface_AndLightTheActiveOne()
    {
        await OnTheRailAsync((rail, window) =>
        {
            shell.IsSshShowing.ShouldBeTrue("nothing has navigated to SFTP or S3 yet");
            Segment(rail, "SSH").Classes.Contains("active").ShouldBeTrue();

            Click(Segment(rail, "SFTP"), window);

            shell.IsTransfersShowing.ShouldBeTrue();
            Segment(rail, "SFTP").Classes.Contains("active").ShouldBeTrue();
            Segment(rail, "SSH").Classes.Contains("active").ShouldBeFalse("exactly one segment lights at once");

            Click(Segment(rail, "S3"), window);

            shell.IsBucketsShowing.ShouldBeTrue();
            shell.IsTransfersShowing.ShouldBeFalse();
            Segment(rail, "S3").Classes.Contains("active").ShouldBeTrue();

            Click(Segment(rail, "SSH"), window);

            shell.IsSshShowing.ShouldBeTrue();
            shell.IsBucketsShowing.ShouldBeFalse();
        });
    }

    /// <summary>
    /// The mode-dependent first row follows the same three flags the switcher above lights.
    /// </summary>
    /// <remarks>
    /// The label is read straight off the row's own bound text rather than off <see cref="MainWindowViewModel"/>
    /// state directly, because what a fidelity pass could break is the binding between the two, not the
    /// property computing the right string on its own — <c>MainWindowViewModelTests</c> would already catch
    /// that half.
    /// </remarks>
    [Fact]
    public async Task TheFirstRailItemsLabel_FollowsTheSwitchersMode()
    {
        await OnTheRailAsync((rail, window) =>
        {
            FirstRow(rail).ShouldBe("Terminal");

            Click(Segment(rail, "SFTP"), window);
            FirstRow(rail).ShouldBe("Files");

            Click(Segment(rail, "S3"), window);
            FirstRow(rail).ShouldBe("Buckets");

            Click(Segment(rail, "SSH"), window);
            FirstRow(rail).ShouldBe("Terminal");
        });
    }

    /// <remarks>
    /// The user popover is a <c>Flyout</c>, and this is the same assertion
    /// <c>TerminalTabsTests.TheCaretBesideVaults_OpensTheVaultMenu</c> made of the strip's own — the pointer
    /// opens the popup, and nothing about its position or its content occlude anything, since it never
    /// crosses into the terminal's own rectangle. See the remark in NavRail.axaml.
    /// </remarks>
    [Fact]
    public async Task ClickingTheUserChip_OpensThePopover()
    {
        await OnTheRailAsync((rail, window) =>
        {
            var chip = UserChip(rail);

            FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse("nothing has been pressed yet");

            Click(chip, window);

            FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeTrue();
        });
    }

    /// <summary>
    /// Each vault switch in the popover draws the vault's own display name and shown state, and is wired
    /// to the command that toggles it.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <see cref="MainWindowViewModel.VaultToggles"/> is fed directly rather than through a real sign-in
    /// and a second vault created on a fake server. <c>VaultVisibilityTests</c>, in the Avalonia-free
    /// <c>DodoSSH.Client.App.Tests</c> project, already proves <see cref="MainWindowViewModel.ToggleVaultCommand"/>
    /// itself — that a hidden vault stays syncing, that the personal one refuses, and everything else the
    /// command actually does once it runs. What is worth proving here, in the project that can lay markup
    /// out at all, is only the wiring: that a row in this popover shows the right vault and calls that
    /// command with that vault when pressed — the fact a fidelity pass to this file could actually break.
    /// </para>
    /// <para>
    /// The command is read off the row rather than pressed, because <see cref="MainWindowViewModel.ToggleVaultCommand"/>
    /// itself declines with no observable effect when <see cref="MainWindowViewModel.Vault"/> is null — which
    /// it is here, for the reason above — so a press would prove nothing a reader could tell from a press
    /// that reached the wrong command entirely.
    /// </para>
    /// </remarks>
    [Fact]
    public async Task PopoverVaultRows_NameTheirVaultAndAreWiredToToggleIt()
    {
        var shown = new VaultToggleViewModel(Guid.CreateVersion7(), "Personal", IsPersonal: true, IsShown: true);
        var hidden = new VaultToggleViewModel(Guid.CreateVersion7(), "Platform secrets", IsPersonal: false, IsShown: false);

        shell.VaultToggles.Add(shown);
        shell.VaultToggles.Add(hidden);

        await OnTheRailAsync((rail, window) =>
        {
            Click(UserChip(rail), window);

            var shownRow = PopoverRow(window, shown);
            var hiddenRow = PopoverRow(window, hidden);

            shownRow.Command.ShouldBeSameAs(shell.ToggleVaultCommand);
            shownRow.CommandParameter.ShouldBeSameAs(shown);
            HasVisibleCheck(shownRow).ShouldBeTrue("the personal vault is always shown");

            hiddenRow.Command.ShouldBeSameAs(shell.ToggleVaultCommand);
            hiddenRow.CommandParameter.ShouldBeSameAs(hidden);
            HasVisibleCheck(hiddenRow).ShouldBeFalse("this one was switched off");
        });
    }

    /// <summary>
    /// Settings, Vaults and Preferences each enter settings mode on the page they promise, and shut the
    /// popover behind them.
    /// </summary>
    /// <remarks>
    /// <para>
    /// v5c: these three used to land on a bare <c>ShellScreen</c> — Settings and Preferences on the very
    /// same one, since the mock's own Settings area did not exist yet. Now that it does, each opens the
    /// settings mode on its own page — see <see cref="MainWindowViewModel.EnterSettings"/> — and "Settings"
    /// and "Preferences" are no longer the same click.
    /// </para>
    /// <para>
    /// Three <see cref="Fact"/>s over one private body rather than a <see cref="Theory"/>: <c>SettingsPage</c>
    /// is <c>internal</c>, and a public theory method may not carry an internal type in its signature.
    /// </para>
    /// </remarks>
    [Fact]
    public Task ThePopoversSettingsRow_EntersSettingsOnGeneralAndClosesThePopover() =>
        APopoverRowLandsOnAsync("Settings", SettingsPage.General);

    [Fact]
    public Task ThePopoversVaultsRow_EntersSettingsOnVaultsAndClosesThePopover() =>
        APopoverRowLandsOnAsync("Vaults", SettingsPage.Vaults);

    [Fact]
    public Task ThePopoversPreferencesRow_EntersSettingsOnPreferencesAndClosesThePopover() =>
        APopoverRowLandsOnAsync("Preferences", SettingsPage.Preferences);

    private Task APopoverRowLandsOnAsync(string label, SettingsPage target) =>
        OnTheRailAsync((rail, window) =>
        {
            var chip = UserChip(rail);
            Click(chip, window);

            Click(PopoverRow(window, label), window);

            shell.IsSettingsMode.ShouldBeTrue();
            shell.ActiveSettingsPage.ShouldBe(target);
            shell.IsShowingPages.ShouldBeTrue();
            FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse("a navigation row shuts the popover behind it");
        });

    /// <remarks>
    /// Through the Account settings page rather than a direct <c>SignOutCommand</c> — see
    /// <see cref="MainWindowViewModel.SignOutFromPopover"/> for why: the confirmation card is drawn inline on
    /// that one page while the vault is unlocked, and arming it from anywhere else would be a card raised
    /// nobody could see.
    /// </remarks>
    [Fact]
    public async Task ThePopoversLogoutRow_EntersSettingsOnAccountAndArmsTheSignOutConfirmation()
    {
        await OnTheRailAsync((rail, window) =>
        {
            var chip = UserChip(rail);
            Click(chip, window);

            Click(PopoverRow(window, "Logout"), window);

            shell.IsSettingsMode.ShouldBeTrue();
            shell.ActiveSettingsPage.ShouldBe(SettingsPage.Account);
            shell.IsConfirmingSignOut.ShouldBeTrue();
            FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse();
        });
    }

    /// <summary>
    /// Every row in the popover rests flat, and the pointer is what fills one.
    /// </summary>
    /// <remarks>
    /// <c>Button.poprow</c> set a radius and a padding and left the Background alone, so each row wore the
    /// Fluent theme's own button fill: the account menu drew as six raised pills where the design draws six
    /// lines of text. Read as a colour off the templated presenter rather than off the Button, because that
    /// is where the theme puts its brush and therefore the only place the absence of one can be proven.
    ///
    /// The hover half is asserted too, and it is what stops "flat" being fixed by making the rows
    /// permanently invisible to the pointer: a menu row that does not answer a pointer at all is a worse
    /// answer than one that answers wrongly.
    /// </remarks>
    [Fact]
    public async Task PopoverRowsAreFlatUntilThePointerFindsThem()
    {
        await OnTheRailAsync((rail, window) =>
        {
            Click(UserChip(rail), window);

            var row = PopoverRow(window, "Settings");
            var presenter = row.GetVisualDescendants()
                .OfType<ContentPresenter>()
                .First(candidate => candidate.Name is "PART_ContentPresenter");

            var resting = presenter.Background as ISolidColorBrush;

            (resting is null || resting.Color.A == 0).ShouldBeTrue(
                $"a popover row rests flat, and this one is filled with {resting?.Color}");

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

            window.MouseMove(centre);
            LayoutHarness.Settle(window, LayoutHarness.NavRailWidth, LayoutHarness.ScreenHeight);

            row.IsPointerOver.ShouldBeTrue("the pointer was moved onto it");
            (presenter.Background as ISolidColorBrush).ShouldNotBeNull().Color.A.ShouldNotBe(
                (byte)0,
                "a row that does not change under the pointer is one nobody can tell is clickable");
        });
    }

    // ---- Helpers ----

    private Task OnTheRailAsync(Action<NavRail, Window> body) =>
        LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                var rail = new NavRail { DataContext = shell };
                var window = new Window { Content = rail };
                LayoutHarness.Settle(window, LayoutHarness.NavRailWidth, LayoutHarness.ScreenHeight);

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

    private static Button Segment(Visual rail, string label) =>
        rail.GetVisualDescendants()
            .OfType<Button>()
            .First(button => button.Classes.Contains("navseg")
                && button.GetVisualDescendants()
                    .OfType<TextBlock>()
                    .Any(text => string.Equals(text.Text, label, StringComparison.Ordinal)));

    private static Button UserChip(Visual rail) =>
        rail.GetVisualDescendants().OfType<Button>().First(button => button.Classes.Contains("navuser"));

    /// <summary>
    /// A row inside the open popover, found by its own data context. Searched from the window rather than
    /// from the rail: a <c>Flyout</c>'s content is a popup, hosted in the window's own overlay layer rather
    /// than nested inside the control that owns it, so it is outside <c>rail.GetVisualDescendants()</c>.
    /// </summary>
    private static Button PopoverRow(Visual window, VaultToggleViewModel toggle) =>
        window.GetVisualDescendants()
            .OfType<Button>()
            .First(button => button.Classes.Contains("poprow") && ReferenceEquals(button.DataContext, toggle));

    /// <summary>A navigation row inside the open popover, found by the word on it.</summary>
    /// <inheritdoc cref="PopoverRow(Visual, VaultToggleViewModel)" path="/summary" />
    private static Button PopoverRow(Visual window, string label) =>
        window.GetVisualDescendants()
            .OfType<Button>()
            .First(button => button.Classes.Contains("poprow")
                && button.GetVisualDescendants()
                    .OfType<TextBlock>()
                    .Any(text => string.Equals(text.Text, label, StringComparison.Ordinal)));

    /// <summary>Whether a vault row's magenta check square is drawn, for whether it is currently shown.</summary>
    private static bool HasVisibleCheck(Button row) =>
        row.GetVisualDescendants().OfType<Border>().Any(
            border => border.Classes.Contains("vaultcheck") && border.IsVisible);

    /// <summary>The mode-dependent first row's own label, read off its bound <c>TextBlock</c>.</summary>
    private static string FirstRow(Visual rail) =>
        rail.GetVisualDescendants()
            .OfType<Button>()
            .First(button => button.Classes.Contains("nav") && !button.Classes.Contains("navseg"))
            .GetVisualDescendants()
            .OfType<TextBlock>()
            .First(text => text.Classes.Contains("navlabel"))
            .Text ?? string.Empty;

    private static void Click(Visual control, Window window)
    {
        var at = control.TranslatePoint(
            new Point(control.Bounds.Width / 2, control.Bounds.Height / 2), window)
            ?? throw new InvalidOperationException("the control is not in this window's tree");

        window.MouseDown(at, MouseButton.Left);
        window.MouseUp(at, MouseButton.Left);
    }
}
