using DodoSSH.Client.Ssh;

namespace DodoSSH.Client.App.Tests;

/// <summary>
/// An SSH stack that connects to nothing.
/// </summary>
/// <remarks>
/// The shell suite is about what the view models do, and the real factory would need a reachable
/// sshd — which <c>DodoSSH.Client.Ssh.Tests</c> already covers against a container. What this makes
/// testable is everything the connect path does <em>around</em> the connection.
/// </remarks>
internal sealed class FakeSshConnectionFactory : ISshConnectionFactory, ISftpSessionFactory
{
    /// <summary>Thrown instead of connecting, when set. Used for the host-key paths.</summary>
    internal Exception? Failure { get; set; }

    /// <summary>
    /// Holds a connection open until it is completed, when set.
    /// </summary>
    /// <remarks>
    /// A handshake takes as long as a network takes, and connecting is deliberately no longer allowed to
    /// hold the window still while it does — so there is now behaviour that only exists <em>during</em> a
    /// connection: a tab in the strip with no session behind it. This is how a test gets to look at that
    /// moment rather than at the two on either side of it.
    /// </remarks>
    internal TaskCompletionSource? Gate { get; set; }

    /// <summary>Requests this factory was asked for, in order.</summary>
    internal List<SshConnectionRequest> Requests { get; } = [];

    /// <summary>Requests for a file-transfer session, in order.</summary>
    /// <remarks>
    /// Kept apart from <see cref="Requests"/> deliberately: file transfer is a separate connection, and a
    /// test asserting that opening a terminal did not also open one would have nothing to look at if the two
    /// shared a list.
    /// </remarks>
    internal List<SshConnectionRequest> SftpRequests { get; } = [];

    /// <inheritdoc />
    public async Task<ISshConnection> ConnectAsync(
        SshConnectionRequest request,
        IProgress<SshConnectionPhase>? progress,
        CancellationToken cancellationToken)
    {
        Requests.Add(request);

        // Before the gate rather than after it, which is what makes this fake useful for the connecting
        // card: a test that holds Gate open is a connection stuck partway through, and the step list has to
        // show it stuck on a named step rather than on none.
        progress?.Report(SshConnectionPhase.Reaching);

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

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

        // Only on the way to succeeding. A failure reported as having authenticated would let a test pass
        // while the card showed a refused connection getting one step further than it did.
        progress?.Report(SshConnectionPhase.CheckingHostKey);
        progress?.Report(SshConnectionPhase.Authenticating);

        return new FakeSshConnection(request);
    }

    /// <inheritdoc />
    public Task<ISftpSession> OpenSftpAsync(
        SshConnectionRequest request,
        CancellationToken cancellationToken)
    {
        SftpRequests.Add(request);

        return Failure is { } failure
            ? Task.FromException<ISftpSession>(failure)
            : Task.FromResult<ISftpSession>(new FakeSftpSession(request));
    }
}

/// <summary>A remote filesystem with one directory in it.</summary>
/// <remarks>
/// Enough for the shell suite, which is about what the screen does around a session rather than about
/// moving bytes. The queue's own behaviour is covered against a fuller fake in
/// <c>DodoSSH.Client.Transfer.Tests</c>, and the real subsystem against a container in
/// <c>DodoSSH.Client.Ssh.Tests</c>.
/// </remarks>
internal sealed class FakeSftpSession(SshConnectionRequest request) : ISftpSession
{
    /// <inheritdoc />
    public bool IsConnected { get; private set; } = true;

    /// <inheritdoc />
    public HostKeyPresentation HostKey { get; } =
        new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake");

    /// <inheritdoc />
    public string Cipher { get; } = "aes256-gcm@openssh.com";

    /// <inheritdoc />
    public string HomeDirectory => $"/home/{request.Username}";

    /// <inheritdoc />
    public Task<IReadOnlyList<SftpEntry>> ListAsync(string path, CancellationToken cancellationToken) =>
        Task.FromResult<IReadOnlyList<SftpEntry>>(
        [
            new SftpEntry(
                "notes.txt",
                SftpPath.Combine(path, "notes.txt"),
                SftpEntryKind.File,
                12,
                DateTimeOffset.UnixEpoch,
                "-rw-r--r--"),
        ]);

    /// <inheritdoc />
    public Task<SftpEntry?> StatAsync(string path, CancellationToken cancellationToken) =>
        Task.FromResult<SftpEntry?>(null);

    /// <inheritdoc />
    public Task<Stream> OpenReadAsync(string path, long offset, CancellationToken cancellationToken) =>
        Task.FromResult<Stream>(new MemoryStream("hello there\n"u8.ToArray(), writable: false));

    /// <inheritdoc />
    public Task<Stream> OpenWriteAsync(string path, long offset, CancellationToken cancellationToken) =>
        Task.FromResult<Stream>(new MemoryStream());

    /// <inheritdoc />
    public Task CreateDirectoryAsync(string path, CancellationToken cancellationToken) =>
        Task.CompletedTask;

    /// <inheritdoc />
    public Task DeleteAsync(string path, CancellationToken cancellationToken) => Task.CompletedTask;

    /// <inheritdoc />
    public Task RenameAsync(string fromPath, string toPath, CancellationToken cancellationToken) =>
        Task.CompletedTask;

    /// <inheritdoc />
    public ValueTask DisposeAsync()
    {
        IsConnected = false;
        return ValueTask.CompletedTask;
    }
}

internal sealed class FakeSshConnection(SshConnectionRequest request) : ISshConnection
{
    /// <inheritdoc />
    public bool IsConnected { get; private set; } = true;

    /// <inheritdoc />
    public HostKeyPresentation HostKey { get; } =
        new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake");

    /// <inheritdoc />
    public string Cipher { get; } = "aes256-gcm@openssh.com";

    /// <inheritdoc />
    public Task<ISshShellSession> OpenShellAsync(
        TerminalSize size,
        CancellationToken cancellationToken) =>
        Task.FromResult<ISshShellSession>(new FakeSshShellSession());

    /// <inheritdoc />
    public ValueTask DisposeAsync()
    {
        IsConnected = false;
        return ValueTask.CompletedTask;
    }
}

/// <summary>A shell that is open, silent and never closes on its own.</summary>
/// <remarks>
/// <see cref="ReadAsync"/> blocks rather than returning 0. Returning 0 means the remote closed the
/// channel, which would end the session the moment it was opened and make the test assert against a
/// connection that had already gone.
/// </remarks>
internal sealed class FakeSshShellSession : ISshShellSession
{
    private readonly CancellationTokenSource closed = new();

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

    /// <inheritdoc />
    public async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
    {
        using var linked = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken, closed.Token);

        await Task.Delay(Timeout.InfiniteTimeSpan, linked.Token).ConfigureAwait(false);

        return 0;
    }

    /// <inheritdoc />
    public ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken) =>
        ValueTask.CompletedTask;

    /// <inheritdoc />
    public void Resize(TerminalSize size)
    {
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        IsOpen = false;

        await closed.CancelAsync().ConfigureAwait(false);

        closed.Dispose();
    }
}
