namespace DodoSSH.Client.Terminal.Tests;

/// <summary>
/// The renderer page's half of staying attached — <c>terminal.js</c>'s <c>connect()</c> and what drives it.
/// </summary>
/// <remarks>
/// <para>
/// The host's half is <see cref="TerminalDataPlaneTests"/>, and the two are one mechanism: a socket that
/// drops is ordinary here, and the page coming back for another is what makes it ordinary. What these
/// tests protect is the property that failure of this mechanism has no other symptom — a terminal whose
/// page has given up looks exactly like a terminal whose remote has gone quiet, except for a line of text
/// nobody reads twice.
/// </para>
/// <para>
/// Four of these were written against a page that failed them — the stale close, the handshake that never
/// finishes, and the two wake-ups — and the rest describe behaviour that was already right and is easy to
/// break while fixing those. The two that assert a wake-up does *nothing* pass against either version,
/// which is the point of them: they are what stops the cure being worse, and they can only ever fail
/// against a future change. See <see cref="RendererPage"/> for how the real file is loaded and for what
/// this cannot reach.
/// </para>
/// </remarks>
public sealed class RendererReconnectionTests
{
    [Fact]
    public void ThePage_ConnectsWhenItLoads()
    {
        var page = RendererPage.Load();

        page.Attempts.ShouldBe(1);
    }

    [Fact]
    public void ADroppedSocket_IsRetriedAndTheBannerClears()
    {
        var page = RendererPage.Load();

        page.Do("accept(0)");
        page.Banner.ShouldBe("");

        page.Do("drop(0)");
        page.Banner.ShouldStartWith("Reconnecting");

        page.Do("advance(1000)");
        page.Attempts.ShouldBe(2);

        page.Do("accept(1)");
        page.Banner.ShouldBe("");
    }

    /// <remarks>
    /// The wait grows within one outage and goes back to a second once a socket has actually opened, so
    /// that the next outage is not paid for at the previous one's rate.
    /// </remarks>
    [Fact]
    public void TheWait_GrowsWithinAnOutageAndResetsAfterIt()
    {
        var page = RendererPage.Load();

        page.Do("fail(0); advance(1000)");
        page.Attempts.ShouldBe(2);

        page.Do("fail(1); advance(1999)");
        page.Attempts.ShouldBe(2);

        page.Do("advance(1)");
        page.Attempts.ShouldBe(3);

        page.Do("accept(2); drop(2); advance(1000)");
        page.Attempts.ShouldBe(4);
    }

    /// <summary>
    /// A close for a socket the page has already replaced must not start a reconnect.
    /// </summary>
    /// <remarks>
    /// The loop this forbids costs nothing to enter and never leaves: the host aborts the displaced socket
    /// on every takeover — see <c>TerminalDataPlane.UpgradeAsync</c> — so a stale close that schedules a
    /// retry displaces the socket that has just succeeded, whose own close schedules the next. The visible
    /// end of it is a terminal that reconnects every second forever with the banner up for most of it.
    /// </remarks>
    [Fact]
    public void AStaleClose_DoesNotDisplaceTheSocketThatSucceeded()
    {
        var page = RendererPage.Load();

        page.Do("accept(0); drop(0); advance(1000)");
        page.Do("accept(1)");
        page.Attempts.ShouldBe(2);

        // The first socket's end, arriving after the page has moved on.
        page.Do("deliverLateClose(0)");
        page.Do("advance(60000)");

        page.Attempts.ShouldBe(2);
        page.Banner.ShouldBe("");
    }

    /// <summary>
    /// An attempt that never finishes its handshake is given up on rather than waited on forever.
    /// </summary>
    /// <remarks>
    /// Every other retry in the page is scheduled by a close or an error, so a socket that reports neither
    /// — which is what a renderer suspended mid-handshake leaves behind — used to schedule nothing at all.
    /// The page then held a banner saying it was reconnecting with no timer pending and no socket coming,
    /// for the rest of its life.
    /// </remarks>
    [Fact]
    public void AHandshakeThatNeverFinishes_IsAbandonedAndRetried()
    {
        var page = RendererPage.Load();

        // Nothing whatever from the first attempt: no open, no error, no close.
        page.Do("advance(5000)");
        page.Attempts.ShouldBe(1);

        page.Do("advance(1000)");
        page.Attempts.ShouldBe(2);
        page.IsClosed(0).ShouldBeTrue();

        page.Do("accept(1)");
        page.Banner.ShouldBe("");
    }

    /// <summary>
    /// Coming back to the page reconnects it, without waiting for a timer that may not be running.
    /// </summary>
    /// <remarks>
    /// The case the whole wake-up path exists for, and the one a test can only approximate: the harness's
    /// clock stands still here because a real hidden page's clock is throttled rather than stopped, and
    /// standing still is the honest worst case of that. What is being asserted is that the page does not
    /// need the clock at all to notice it is back.
    /// </remarks>
    [Fact]
    public void BecomingVisibleAgain_ReconnectsWithoutTheTimer()
    {
        var page = RendererPage.Load();

        page.Do("accept(0); becomeHidden(); drop(0)");
        page.Attempts.ShouldBe(1);

        page.Do("becomeVisible()");
        page.Attempts.ShouldBe(2);

        // And the timer that was pending when the page woke must not open a third socket on top of the
        // one that just succeeded — which would be the takeover loop, entered from the other side.
        page.Do("accept(1); advance(60000)");
        page.Attempts.ShouldBe(2);
        page.Banner.ShouldBe("");
    }

    [Fact]
    public void TakingTheKeyboardBack_AlsoReconnects()
    {
        var page = RendererPage.Load();

        page.Do("accept(0); drop(0); takeFocus()");

        page.Attempts.ShouldBe(2);
    }

    /// <remarks>
    /// The wake-ups fire on gestures as ordinary as clicking the window, so the check they make has to be
    /// the thing that keeps them cheap rather than the frequency. A page whose socket is up must treat all
    /// of them as nothing at all — anything else would be the takeover loop with a person's mouse driving it.
    /// </remarks>
    [Fact]
    public void WakingUpOverAHealthySocket_DoesNothing()
    {
        var page = RendererPage.Load();

        page.Do("accept(0)");
        page.Do("takeFocus(); becomeVisible(); comeOnline(); becomeHidden(); becomeVisible()");

        page.Attempts.ShouldBe(1);
        page.Banner.ShouldBe("");
    }

    /// <remarks>
    /// An attempt already in flight is left to finish or to time out. Restarting it on every wake-up would
    /// mean a page being clicked during a slow handshake never completing one.
    /// </remarks>
    [Fact]
    public void WakingUpWhileConnecting_LeavesTheAttemptAlone()
    {
        var page = RendererPage.Load();

        page.Do("takeFocus(); becomeVisible(); comeOnline()");

        page.Attempts.ShouldBe(1);
    }
}
