using System.Text;
using DodoSSH.Client.Terminal;

namespace DodoSSH.Client.Ssh.Tests;

/// <summary>
/// The pump against a real shell, with the transport replaced by a recorder.
/// </summary>
/// <remarks>
/// Sits between the pump's unit tests, which use a fake shell, and the full end-to-end test, which
/// adds the loopback socket. Its value is diagnostic: when output does not reach a renderer, this
/// says whether the pump and SSH.NET are producing anything at all.
/// </remarks>
[Collection(SshCollection.Name)]
public sealed class PumpOverRealSshTests(SshServerFixture fixture)
{
    /// <summary>Trusts the container's host key, then connects. First contact is refused by design.</summary>
    private async Task<ISshConnection> ConnectTrustedAsync()
    {
        var knownHosts = new InMemoryKnownHostStore();
        var factory = new SshNetConnectionFactory(knownHosts);

        var request = new SshConnectionRequest(
            fixture.Host,
            fixture.Port,
            SshServerFixture.Username,
            new SshPasswordCredential(SshServerFixture.Password));

        var unknown = await Should.ThrowAsync<SshHostKeyUnknownException>(async () =>
            await factory.ConnectAsync(request, progress: null, TestContext.Current.CancellationToken));

        await knownHosts.TrustAsync(unknown.Presentation, TestContext.Current.CancellationToken);

        return await factory.ConnectAsync(request, progress: null, TestContext.Current.CancellationToken);
    }

    [Fact]
    public async Task ThePump_ForwardsRealShellOutput()
    {
        await using var connection = await ConnectTrustedAsync();

        var shell = await connection.OpenShellAsync(
            new TerminalSize(100, 30, 1000, 750), TestContext.Current.CancellationToken);

        var transport = new CountingTransport();

        await using var pump = new TerminalSessionPump(
            1,
            shell,
            transport,
            TimeProvider.System,
            new TerminalPumpOptions { FlushInterval = TimeSpan.FromMilliseconds(20) });

        transport.Pump = pump;

        var run = pump.RunAsync(TestContext.Current.CancellationToken);

        // Marker split so the PTY's echo of the command line does not satisfy the match.
        await pump.WriteInputAsync(
            Encoding.UTF8.GetBytes("echo \"DODO\"\"SSH-OK\"\n"),
            TestContext.Current.CancellationToken);

        var deadline = TimeProvider.System.GetUtcNow() + TimeSpan.FromSeconds(20);

        while (TimeProvider.System.GetUtcNow() < deadline
            && !transport.Text.Contains("DODOSSH-OK", StringComparison.Ordinal))
        {
            await Task.Delay(50, TestContext.Current.CancellationToken);
        }

        transport.Text.Contains("DODOSSH-OK", StringComparison.Ordinal).ShouldBeTrue(
            $"BytesRead={pump.BytesRead}, FramesSent={pump.FramesSent}, "
            + $"OutputFrames={transport.OutputFrames}, Text=<{transport.Text}>");

        await pump.DisposeAsync();

        try
        {
            await run;
        }
        catch (OperationCanceledException)
        {
            // Expected: disposing the pump cancels its run.
        }
    }

    /// <summary>Accumulates output and acknowledges it, as a keeping-up renderer would.</summary>
    private sealed class CountingTransport : ITerminalTransport
    {
        private readonly StringBuilder text = new();
        private readonly Lock gate = new();

        internal TerminalSessionPump? Pump { get; set; }

        internal int OutputFrames { get; private set; }

        internal string Text
        {
            get
            {
                lock (gate)
                {
                    return text.ToString();
                }
            }
        }

        public ValueTask SendAsync(ReadOnlyMemory<byte> frame, CancellationToken cancellationToken)
        {
            if (!TerminalFrame.TryRead(frame.Span, out var opcode, out _, out var payload)
                || opcode != (byte)TerminalServerOpcode.Output)
            {
                return ValueTask.CompletedTask;
            }

            lock (gate)
            {
                OutputFrames++;
                text.Append(Encoding.UTF8.GetString(payload));
            }

            Pump?.Acknowledge((uint)payload.Length);

            return ValueTask.CompletedTask;
        }
    }
}
