using System.Globalization;

using Jint;

namespace DodoSSH.Client.Terminal.Tests;

/// <summary>
/// One load of <c>terminal.js</c>, in a fake browser, for one test.
/// </summary>
/// <remarks>
/// <para>
/// <b>The page's own source is what runs.</b> Not a transcription of its logic into C# — that would test a
/// copy, and the copy would be the thing that stayed correct. The file is read from the shell project and
/// evaluated as it ships, so a change to the page that breaks the reconnection rules fails here.
/// </para>
/// <para>
/// <b>Jint rather than node, and that is a deliberate trade.</b> A node script would be the obvious way to
/// run JavaScript and would need node on every machine and in every CI job that runs the suite — so it
/// would be a second test command, run separately, and the first thing to be forgotten. This runs inside
/// <c>dotnet test</c> with everything else. What it costs is that the engine is not the engine the page
/// actually runs in: Jint is not Chromium, so this can prove the page's own logic and can prove nothing
/// about how WebView2 or Android's WebView behave. That boundary is exactly where
/// <c>docs/manual-checks.md</c> picks up — see 1.10 and 11.12a.
/// </para>
/// <para>
/// A fresh engine per test, because the page is a script with module-level state and there is no unloading
/// it: two tests sharing one engine would share a socket list, a clock and a backoff.
/// </para>
/// </remarks>
internal sealed class RendererPage
{
    private readonly Engine engine;

    private RendererPage(Engine engine) => this.engine = engine;

    /// <summary>How many sockets the page has opened since it loaded.</summary>
    /// <remarks>
    /// The measurement nearly every test here turns on, and it is deliberately a count of *attempts*
    /// rather than of live sockets: the failures being guarded against are a page that stops trying and a
    /// page that never stops, and both are counted rather than observed.
    /// </remarks>
    public int Attempts => (int)engine.Evaluate("attempts()").AsNumber();

    /// <summary>What the status element says — the banner a user would be looking at.</summary>
    public string Banner => engine.Evaluate("banner()").AsString();

    /// <summary>
    /// Loads the harness and then the page, leaving the page exactly as it is a moment after the WebView
    /// navigated to it: one socket opened and still connecting.
    /// </summary>
    public static RendererPage Load()
    {
        var browser = new Engine();

        // Order matters and is not incidental: the page connects on its last line, so every global it
        // touches — the socket constructor above all — has to be in place before it is evaluated.
        browser.Execute(Read("Renderer/renderer-harness.js"));
        browser.Execute(Read("Renderer/terminal.js"));

        return new RendererPage(browser);
    }

    /// <summary>Runs a line of the harness's own vocabulary — <c>accept(0)</c>, <c>advance(1000)</c>.</summary>
    public void Do(string script) => engine.Execute(script);

    /// <summary>Whether that attempt has been closed, by the page or by the far end.</summary>
    public bool IsClosed(int attempt) =>
        engine.Evaluate(
                string.Create(CultureInfo.InvariantCulture, $"isClosed({attempt})"))
            .AsBoolean();

    /// <remarks>
    /// Both files are copied beside the test assembly by the project file, the page out of the shell
    /// project it belongs to. Read from disk rather than embedded so that the copy which runs here is the
    /// same bytes the host serves, with nothing in between that could go stale.
    /// </remarks>
    private static string Read(string relativePath) =>
        File.ReadAllText(Path.Combine(AppContext.BaseDirectory, relativePath));
}
