using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Options;
using Tiku.Application.Assets;
using Tiku.Infrastructure.Assets;

namespace Tiku.UnitTests.Assets;

public sealed class ClamAvAssetSecurityScannerTests
{
    [Theory]
    [InlineData("stream: OK", AssetSecurityScanVerdict.Clean, null)]
    [InlineData("stream: Eicar-Signature FOUND", AssetSecurityScanVerdict.Infected, "Eicar-Signature")]
    public async Task Instream_protocol_parses_clean_and_infected_responses(
        string response,
        AssetSecurityScanVerdict expectedVerdict,
        string? expectedSignature)
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        var server = ServeScanAsync(listener, response);
        var scanner = CreateScanner(port);
        var payload = Encoding.UTF8.GetBytes("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*");

        var result = await scanner.ScanAsync(new MemoryStream(payload), payload.Length);
        await server;

        Assert.Equal(expectedVerdict, result.Verdict);
        Assert.Equal(expectedSignature, result.Signature);
        Assert.Equal(payload.Length, result.BytesScanned);
    }

    [Fact]
    public async Task Ping_requires_pong()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        var server = Task.Run(async () =>
        {
            using var client = await listener.AcceptTcpClientAsync();
            await using var stream = client.GetStream();
            Assert.Equal("zPING\0", Encoding.ASCII.GetString(await ReadExactAsync(stream, 6)));
            await stream.WriteAsync("PONG\0"u8.ToArray());
            listener.Stop();
        });

        Assert.True(await CreateScanner(port).CheckHealthAsync());
        await server;
    }

    [Fact]
    public async Task Oversized_stream_is_rejected_before_connecting()
    {
        var scanner = CreateScanner(1, 4);
        var exception = await Assert.ThrowsAsync<AssetSecurityScannerException>(() =>
            scanner.ScanAsync(new MemoryStream(new byte[5]), 5));
        Assert.Equal("clamav_stream_too_large", exception.Code);
    }

    [Fact]
    public async Task Unavailable_daemon_is_reported_as_transient_scanner_failure()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        var exception = await Assert.ThrowsAsync<AssetSecurityScannerException>(() =>
            CreateScanner(port).ScanAsync(new MemoryStream([1]), 1));
        Assert.Equal("clamav_unavailable", exception.Code);
    }

    private static ClamAvAssetSecurityScanner CreateScanner(int port, long streamMaxLength = 1024 * 1024)
    {
        return new ClamAvAssetSecurityScanner(Options.Create(new ClamAvOptions
        {
            Host = IPAddress.Loopback.ToString(),
            Port = port,
            TimeoutSeconds = 2,
            ChunkBytes = 1024,
            StreamMaxLength = streamMaxLength
        }));
    }

    private static Task ServeScanAsync(TcpListener listener, string response)
    {
        return Task.Run(async () =>
        {
            using var client = await listener.AcceptTcpClientAsync();
            await using var stream = client.GetStream();
            Assert.Equal("zINSTREAM\0", Encoding.ASCII.GetString(await ReadExactAsync(stream, 10)));
            while (true)
            {
                var lengthBytes = await ReadExactAsync(stream, 4);
                var length = BinaryPrimitives.ReadUInt32BigEndian(lengthBytes);
                if (length == 0) break;
                _ = await ReadExactAsync(stream, checked((int)length));
            }

            await stream.WriteAsync(Encoding.UTF8.GetBytes(response + "\0"));
            listener.Stop();
        });
    }

    private static async Task<byte[]> ReadExactAsync(Stream stream, int length)
    {
        var buffer = new byte[length];
        await stream.ReadExactlyAsync(buffer);
        return buffer;
    }
}