using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree;
using CommunityToolkit.Mvvm.Input;
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 v5b in-screen tab row answers a pointer, on both of the two ways it is used.
/// </summary>
/// <remarks>
/// The replacement for <c>TerminalTabsTests</c>, which measured the window-wide strip this control replaced
/// — see <c>SessionTabRow.axaml</c>'s own remark for why one control now serves both the terminal surface and
/// the SFTP surface. The gestures that do not vary by surface — middle-click closes, "+" opens the palette,
/// the tab pointer feedback — are carried over from that suite essentially unchanged; what is new here is
/// <see cref="TabRowTabClickRunsTheGivenCommand"/> and <see cref="SftpRowMarksTheColourItIsToldTo"/>, which
/// prove the one thing this control adds: the click and the accent colour are handed in rather than fixed.
/// </remarks>
public sealed class SessionTabRowTests : 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($"session-tabs-{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();
    }

    [Fact]
    public async Task AMiddleClickOnATabClosesThatTab()
    {
        await OnTheStripAsync((strip, window) =>
        {
            var doomed = shell.Tabs[0];
            var survivor = shell.Tabs[1];

            window.MouseDown(Centre(TabButton(strip, doomed), window), MouseButton.Middle);

            shell.Tabs.ShouldHaveSingleItem().ShouldBe(survivor);
        });
    }

    [Fact]
    public async Task AMiddleClickOnTheStripBackgroundClosesNothing()
    {
        await OnTheStripAsync((strip, window) =>
        {
            window.MouseDown(new Point(700, 17), MouseButton.Middle);

            shell.Tabs.Count.ShouldBe(2);
        });
    }

    [Fact]
    public async Task AMiddleClickOnTheButtonThatOpensAConnectionClosesNothing()
    {
        await OnTheStripAsync((strip, window) =>
        {
            window.MouseDown(Centre(PlusButton(strip), window), MouseButton.Middle);

            shell.Tabs.Count.ShouldBe(2);
            shell.IsSearching.ShouldBeFalse("a middle click is not how the palette opens either");
        });
    }

    [Fact]
    public async Task AMiddleClickOnTheCrossClosesExactlyOneTab()
    {
        await OnTheStripAsync((strip, window) =>
        {
            var survivor = shell.Tabs[1];

            window.MouseDown(Centre(CloseButton(strip, shell.Tabs[0]), window), MouseButton.Middle);

            shell.Tabs.ShouldHaveSingleItem().ShouldBe(survivor);
        });
    }

    [Fact]
    public async Task ALeftClickOnTheCrossClosesTheTabAndDoesNotSelectIt()
    {
        await OnTheStripAsync((strip, window) =>
        {
            var doomed = shell.Tabs[0];
            var survivor = shell.Tabs[1];

            shell.SelectTabCommand.Execute(survivor);

            var cross = CloseButton(strip, doomed);
            window.MouseDown(Centre(cross, window), MouseButton.Left);
            window.MouseUp(Centre(cross, window), MouseButton.Left);

            shell.Tabs.ShouldHaveSingleItem().ShouldBe(survivor);
            shell.SelectedTab.ShouldBe(survivor);
        });
    }

    /// <remarks>
    /// The terminal row's own click: <c>TabCommand</c> bound to <c>SelectTabCommand</c>, the same as the
    /// window-wide strip's used to be.
    /// </remarks>
    [Fact]
    public async Task ALeftClickOnATabRunsTheBoundTabCommand()
    {
        await OnTheStripAsync((strip, window) =>
        {
            var wanted = shell.Tabs[1];

            shell.ShowScreenCommand.Execute(ShellScreen.Preferences);
            shell.IsTerminalShowing.ShouldBeFalse();

            var button = TabButton(strip, wanted);
            window.MouseDown(Centre(button, window), MouseButton.Left);
            window.MouseUp(Centre(button, window), MouseButton.Left);

            shell.Tabs.Count.ShouldBe(2, "selecting is not closing");
            shell.SelectedTab.ShouldBe(wanted);
            shell.IsTerminalShowing.ShouldBeTrue();
        });
    }

    /// <remarks>
    /// The one thing this control adds over the strip it replaces: which command a tab click runs is handed
    /// in, not fixed. Proved with a command that is not <c>SelectTabCommand</c> at all, so a row that quietly
    /// ignored <c>TabCommand</c> and fell back to selecting the tab itself would fail this rather than pass
    /// it by coincidence.
    /// </remarks>
    [Fact]
    public async Task TabRowTabClickRunsTheGivenCommand()
    {
        await LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                shell.Tabs.Add(new TerminalTabViewModel(1, "prod-db", "deploy@db.internal:22"));

                var seen = new List<TerminalTabViewModel>();
                var strip = new SessionTabRow
                {
                    DataContext = shell,
                    TabCommand = new RelayCommand<TerminalTabViewModel>(tab => seen.Add(tab!)),
                };

                var window = new Window { Content = strip };
                LayoutHarness.Settle(window, 900, 600);

                try
                {
                    var button = TabButton(strip, shell.Tabs[0]);
                    window.MouseDown(Centre(button, window), MouseButton.Left);
                    window.MouseUp(Centre(button, window), MouseButton.Left);

                    seen.ShouldHaveSingleItem().ShouldBe(shell.Tabs[0]);
                    shell.SelectedTab.ShouldBeNull("the given command ran instead of SelectTabCommand");
                }
                finally
                {
                    window.Close();
                }
            },
            Token);
    }

    [Fact]
    public async Task TheButtonThatOpensAConnectionOpensThePalette()
    {
        await OnTheStripAsync((strip, window) =>
        {
            var plus = PlusButton(strip);
            window.MouseDown(Centre(plus, window), MouseButton.Left);
            window.MouseUp(Centre(plus, window), MouseButton.Left);

            shell.IsSearching.ShouldBeTrue();
        });
    }

    /// <remarks>
    /// The strip's own height budget moved with it — see <see cref="LayoutHarness.SessionTabRowHeight"/> —
    /// but the invariant this test protects is the same one <c>TerminalTabsTests</c> protected: a row of
    /// tabs must not grow the chrome around it, however many are open.
    /// </remarks>
    [Fact]
    public async Task TheRowIsTheHeightTheBudgetAssumes_AndDoesNotGrowWithTabs()
    {
        await LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                for (var i = 0; i < 12; i++)
                {
                    shell.Tabs.Add(new TerminalTabViewModel((uint)i, $"host-{i}", $"deploy@host-{i}:22"));
                }

                var strip = new SessionTabRow { DataContext = shell, TabCommand = shell.SelectTabCommand };
                var window = LayoutHarness.HostAtMinimumSize(
                    strip, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);

                try
                {
                    strip.DesiredSize.Height.ShouldBe(LayoutHarness.SessionTabRowHeight);

                    LayoutHarness.Unreachable(window).ShouldBeEmpty();
                }
                finally
                {
                    window.Close();
                }
            },
            Token);
    }

    /// <remarks>
    /// v5b's tab, unlike the pill it replaced, paints no fill at all until it is either active or hovered —
    /// so "the plus differs from a tab" can no longer be proven against a resting, inactive one, which reads
    /// identically to the plus at rest by design. This proves the same two facts a different way: an inactive
    /// tab still changes under the pointer, and the plus never gets the treatment an <em>active</em> tab does
    /// — the coloured top border and the DeepChrome fill — which is the one visual claim "not a thing being
    /// chosen between" actually has to hold.
    /// </remarks>
    [Fact]
    public async Task ATabLightsUnderThePointer_AndThePlusIsNotDrawnAsAnActiveTab()
    {
        await OnTheStripAsync(
            (strip, window) =>
            {
                var tab = TabButton(strip, shell.Tabs[0]);
                var resting = Fill(tab);

                window.MouseMove(Centre(tab, window));
                LayoutHarness.Settle(window, 900, 600);

                tab.IsPointerOver.ShouldBeTrue("the pointer was moved onto it");

                Fill(tab).ShouldNotBe(
                    resting,
                    "a tab that does not change under the pointer is one nobody can tell is clickable");

                window.MouseMove(new Point(0, 0));
                LayoutHarness.Settle(window, 900, 600);

                shell.SelectTabCommand.Execute(shell.Tabs[0]);
                LayoutHarness.Settle(window, 900, 600);

                var activeTab = TabButton(strip, shell.Tabs[0]);
                var plus = PlusButton(strip);

                Fill(plus).ShouldNotBe(
                    Fill(activeTab),
                    "the button that opens a connection never carries the active tab's DeepChrome fill");

                Presenter(plus).BorderThickness.ShouldBe(
                    default(Thickness),
                    "it carries no outline, because it is not a thing being chosen between");
            });
    }

    /// <remarks>
    /// The SFTP row's own accent: <c>Classes="sftp"</c> on this control's own usage — see
    /// <c>MainWindow.axaml</c> — is what selects <c>Magenta</c> over the terminal row's <c>TerminalTabAccent</c>
    /// for an active tab's top border. Read as a colour rather than a class list, so this catches the App.axaml
    /// selector actually resolving to a different brush rather than merely having the class applied.
    /// </remarks>
    [Fact]
    public async Task SftpRowMarksTheColourItIsToldTo()
    {
        await LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                shell.Tabs.Add(new TerminalTabViewModel(1, "prod-db", "deploy@db.internal:22"));
                shell.SelectTabCommand.Execute(shell.Tabs[0]);

                var sshRow = new SessionTabRow { DataContext = shell, TabCommand = shell.SelectTabCommand };
                var sftpRow = new SessionTabRow
                {
                    DataContext = shell,
                    TabCommand = shell.SelectTabCommand,
                    Classes = { "sftp" },
                };

                var sshWindow = new Window { Content = sshRow };
                var sftpWindow = new Window { Content = sftpRow };
                LayoutHarness.Settle(sshWindow, 900, 600);
                LayoutHarness.Settle(sftpWindow, 900, 600);

                try
                {
                    var sshBorder = Presenter(TabButton(sshRow, shell.Tabs[0])).BorderBrush as ISolidColorBrush;
                    var sftpBorder = Presenter(TabButton(sftpRow, shell.Tabs[0])).BorderBrush as ISolidColorBrush;

                    sshBorder.ShouldNotBeNull().Color.ShouldBe(Color.Parse("#7C5CFF"), "TerminalTabAccent");
                    sftpBorder.ShouldNotBeNull().Color.ShouldBe(Color.Parse("#DD1296"), "Magenta");
                }
                finally
                {
                    sshWindow.Close();
                    sftpWindow.Close();
                }
            },
            Token);
    }

    // ---- Helpers ----

    private static ContentPresenter Presenter(Visual button) =>
        button.GetVisualDescendants()
            .OfType<ContentPresenter>()
            .First(presenter => presenter.Name is "PART_ContentPresenter");

    private static Color? Fill(Visual button) =>
        Presenter(button).Background is ISolidColorBrush brush ? brush.Color : null;

    private Task OnTheStripAsync(Action<SessionTabRow, Window> body) =>
        LayoutHarness.OnTheUiThreadAsync(
            () =>
            {
                shell.Tabs.Add(new TerminalTabViewModel(1, "prod-db", "deploy@db.internal:22"));
                shell.Tabs.Add(new TerminalTabViewModel(2, "web-01", "deploy@web-01.internal:22"));

                var strip = new SessionTabRow { DataContext = shell, TabCommand = shell.SelectTabCommand };
                var window = new Window { Content = strip };
                LayoutHarness.Settle(window, 900, 600);

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

    private static Button TabButton(Visual strip, TerminalTabViewModel tab) =>
        strip.GetVisualDescendants()
            .OfType<Button>()
            .First(button => ReferenceEquals(button.DataContext, tab) && button.Classes.Contains("sesstab"));

    private static Button CloseButton(Visual strip, TerminalTabViewModel tab) =>
        strip.GetVisualDescendants()
            .OfType<Button>()
            .First(button => ReferenceEquals(button.DataContext, tab) && button.Classes.Contains("close"));

    private static Button PlusButton(Visual strip) =>
        strip.GetVisualDescendants().OfType<Button>().First(button => button.Classes.Contains("plus"));

    private static Point Centre(Visual control, Visual window) =>
        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");
}
