using DodoSSH.Client.Session;

namespace DodoSSH.Client.App.Tests;

/// <summary>
/// A release channel that answers whatever the test tells it to.
/// </summary>
/// <remarks>
/// <para>
/// <b><see cref="ApplyAndRestart"/> records the call instead of making it, and that is the whole reason
/// <c>IUpdateChannel</c> exists as an interface.</b> The real one replaces the running process and never
/// returns, so a test could not observe it at all — and the single most important thing to be able to
/// assert about this feature is a negative: that a downloaded update is <em>never</em> applied unless
/// somebody pressed the button. A counter is what makes that assertion writable.
/// </para>
/// <para>
/// <see cref="HoldDownload"/> exists for the same reason in the other direction: without a way to stop a
/// download halfway, the Downloading state would be a state no test could ever catch the view model in.
/// </para>
/// </remarks>
internal sealed class FakeUpdateChannel : IUpdateChannel
{
    /// <summary>What the next check finds. Null means this build is current.</summary>
    internal AvailableUpdate? Available { get; set; }

    /// <summary>When set, checking throws it.</summary>
    internal Exception? CheckFailure { get; set; }

    /// <summary>When set, downloading throws it.</summary>
    internal Exception? DownloadFailure { get; set; }

    /// <summary>When set, applying throws it — after counting, since the call was still made.</summary>
    /// <remarks>
    /// The phone's install can fail on the spot rather than by ending the process: a session the platform
    /// refuses, or an unknown-sources answer that changed under it. What that must not do is escape a
    /// command handler, so this is what lets a test hold the view model to reporting it.
    /// </remarks>
    internal Exception? RestartFailure { get; set; }

    /// <summary>When set, a download waits on it before completing.</summary>
    internal TaskCompletionSource? HoldDownload { get; set; }

    /// <summary>The percentages a download reports on its way through.</summary>
    internal IReadOnlyList<int> ProgressSteps { get; set; } = [25, 50, 100];

    internal int Checks { get; private set; }

    internal int Downloads { get; private set; }

    internal int Restarts { get; private set; }

    internal AvailableUpdate? RestartedWith { get; private set; }

    /// <inheritdoc />
    public bool IsSupported { get; set; } = true;

    /// <summary>Settable, because the two heads answer it differently and both paths need driving.</summary>
    /// <remarks>
    /// True is the desktop's: the shell disposes itself before applying. False is the phone's, where the
    /// install can be declined — and the test that matters is that declining leaves a working session
    /// rather than a torn-down one.
    /// </remarks>
    public bool ApplyingEndsTheProcess { get; set; } = true;

    /// <inheritdoc />
    public string CurrentVersion { get; set; } = "1.0.0";

    /// <inheritdoc />
    public Task<AvailableUpdate?> CheckAsync(CancellationToken cancellationToken)
    {
        Checks++;

        return CheckFailure is { } failure
            ? Task.FromException<AvailableUpdate?>(failure)
            : Task.FromResult(Available);
    }

    /// <inheritdoc />
    public async Task DownloadAsync(
        AvailableUpdate update,
        IProgress<int> progress,
        CancellationToken cancellationToken)
    {
        Downloads++;

        if (DownloadFailure is { } failure)
        {
            throw failure;
        }

        if (HoldDownload is { } gate)
        {
            await gate.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
        }

        foreach (var percent in ProgressSteps)
        {
            cancellationToken.ThrowIfCancellationRequested();
            progress.Report(percent);
        }
    }

    /// <inheritdoc />
    public void ApplyAndRestart(AvailableUpdate update)
    {
        Restarts++;
        RestartedWith = update;

        if (RestartFailure is { } failure)
        {
            throw failure;
        }
    }
}
