using System.CommandLine;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;

namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands;

/// <summary>
/// CLI-019 regression tests for <see cref="BundleCommands.StreamBase64ToFile"/>.
/// The pre-fix code did <c>Convert.FromBase64String(...) → File.WriteAllBytes(...)</c>,
/// doubling the bundle's bytes onto the LOH and writing synchronously. The new
/// streaming helper decodes the base64 string in fixed-size chunks straight into
/// a <see cref="FileStream"/>, so peak working set is bounded by the chunk size
/// regardless of how large the bundle is.
/// </summary>
public class BundleCommandsStreamingTests : IDisposable
{
    private readonly string _tempPath;

    public BundleCommandsStreamingTests()
    {
        _tempPath = Path.Combine(Path.GetTempPath(), $"bundle-stream-test-{Guid.NewGuid():N}.bin");
    }

    public void Dispose()
    {
        if (File.Exists(_tempPath))
        {
            File.Delete(_tempPath);
        }
    }

    [Fact]
    public void StreamBase64ToFile_SmallPayload_RoundTrips()
    {
        var bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        var base64 = Convert.ToBase64String(bytes);

        var written = BundleCommands.StreamBase64ToFile(base64, _tempPath);

        Assert.Equal(bytes.Length, written);
        var roundTripped = File.ReadAllBytes(_tempPath);
        Assert.Equal(bytes, roundTripped);
    }

    [Fact]
    public void StreamBase64ToFile_PayloadCrossesChunkBoundary_RoundTrips()
    {
        // Build a payload several chunks wide so the slicing loop runs more than
        // once, with enough trailing bytes that the final slice is short and
        // exercises the padding/short-final-chunk path.
        var size = (BundleCommands.Base64StreamChunkChars / 4 * 3) * 3 + 17;
        var bytes = new byte[size];
        for (var i = 0; i < size; i++) bytes[i] = (byte)(i & 0xFF);

        var base64 = Convert.ToBase64String(bytes);

        var written = BundleCommands.StreamBase64ToFile(base64, _tempPath);

        Assert.Equal(size, written);
        var roundTripped = File.ReadAllBytes(_tempPath);
        Assert.Equal(bytes, roundTripped);
    }

    [Fact]
    public void StreamBase64ToFile_EmptyString_WritesEmptyFile()
    {
        var written = BundleCommands.StreamBase64ToFile(string.Empty, _tempPath);

        Assert.Equal(0, written);
        Assert.True(File.Exists(_tempPath));
        Assert.Empty(File.ReadAllBytes(_tempPath));
    }

    [Fact]
    public void StreamBase64ToFile_InvalidBase64_ThrowsFormatException()
    {
        // '*' is not a valid base64 character, so TryFromBase64Chars returns
        // false and the helper throws — the pre-fix code threw FormatException
        // from Convert.FromBase64String, so the contract is preserved.
        var invalid = "this is not valid base64 !!!*";

        Assert.Throws<FormatException>(() => BundleCommands.StreamBase64ToFile(invalid, _tempPath));
    }

    [Fact]
    public void StreamBase64ToFile_NullBase64_Throws()
    {
        Assert.Throws<ArgumentNullException>(() => BundleCommands.StreamBase64ToFile(null!, _tempPath));
    }

    [Fact]
    public void StreamBase64ToFile_EmptyOutputPath_Throws()
    {
        Assert.Throws<ArgumentException>(() => BundleCommands.StreamBase64ToFile("AAAA", string.Empty));
    }

    // ---- M8 (B4): --sites / --instances comma-split + flag parsing ----------

    [Fact]
    public void ParseNameList_CommaSeparated_SplitsAndTrims()
    {
        var result = BundleCommands.ParseNameList(" North-01 , East-02 ,West-03 ");
        Assert.NotNull(result);
        Assert.Equal(new[] { "North-01", "East-02", "West-03" }, result);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void ParseNameList_NullOrBlank_ReturnsNull(string? token)
    {
        Assert.Null(BundleCommands.ParseNameList(token));
    }

    [Fact]
    public void ParseNameList_EmptyEntries_AreDropped()
    {
        var result = BundleCommands.ParseNameList("a,,b, ,c");
        Assert.Equal(new[] { "a", "b", "c" }, result);
    }

    [Fact]
    public void BundleExport_SitesAndInstancesFlags_ParseWithoutError()
    {
        var url = new Option<string>("--url") { Recursive = true };
        var format = new Option<string>("--format") { Recursive = true };
        var username = new Option<string>("--username") { Recursive = true };
        var password = new Option<string>("--password") { Recursive = true };
        var bundle = BundleCommands.Build(url, format, username, password);

        var parse = bundle.Parse(new[]
        {
            "export", "--output", "/tmp/out.scadabundle",
            "--sites", "NORTH-01,East Plant",
            "--instances", "NORTH-01.Pump1,NORTH-01.Pump2",
        });

        Assert.Empty(parse.Errors);
    }

    // ---- M8 (D3): --map-site / --map-connection spec parsing ----------------

    [Fact]
    public void ParseSiteMappings_Null_ReturnsEmpty()
    {
        Assert.Empty(BundleCommands.ParseSiteMappings(null));
    }

    [Fact]
    public void ParseSiteMappings_SrcEqualsDst_MapsToExisting()
    {
        var specs = BundleCommands.ParseSiteMappings(new[] { "NORTH-01=PLANT-A" });
        var spec = Assert.Single(specs);
        Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
        Assert.Equal("PLANT-A", spec.TargetSiteIdentifier);
    }

    [Theory]
    [InlineData("NORTH-01")]   // no '=' part   -> create-new
    [InlineData("NORTH-01=")]  // empty rhs     -> create-new
    [InlineData("NORTH-01=  ")] // whitespace rhs -> create-new
    public void ParseSiteMappings_NoTarget_MeansCreateNew(string token)
    {
        var specs = BundleCommands.ParseSiteMappings(new[] { token });
        var spec = Assert.Single(specs);
        Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
        Assert.Null(spec.TargetSiteIdentifier);
    }

    [Fact]
    public void ParseSiteMappings_Repeated_AccumulatesAll()
    {
        var specs = BundleCommands.ParseSiteMappings(new[] { "A=X", "B" });
        Assert.Equal(2, specs.Count);
        Assert.Equal("X", specs[0].TargetSiteIdentifier);
        Assert.Null(specs[1].TargetSiteIdentifier);
    }

    [Fact]
    public void ParseSiteMappings_EmptySource_Throws()
    {
        Assert.Throws<FormatException>(() => BundleCommands.ParseSiteMappings(new[] { "=PLANT-A" }));
    }

    [Fact]
    public void ParseConnectionMappings_SrcEqualsDst_MapsToExisting()
    {
        var specs = BundleCommands.ParseConnectionMappings(new[] { "NORTH-01/OpcA=OpcLive" });
        var spec = Assert.Single(specs);
        Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
        Assert.Equal("OpcA", spec.SourceConnectionName);
        Assert.Equal("OpcLive", spec.TargetConnectionName);
    }

    [Theory]
    [InlineData("NORTH-01/OpcA")]   // no '=' part -> create-new
    [InlineData("NORTH-01/OpcA=")]  // empty rhs   -> create-new
    public void ParseConnectionMappings_NoTarget_MeansCreateNew(string token)
    {
        var specs = BundleCommands.ParseConnectionMappings(new[] { token });
        var spec = Assert.Single(specs);
        Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
        Assert.Equal("OpcA", spec.SourceConnectionName);
        Assert.Null(spec.TargetConnectionName);
    }

    [Theory]
    [InlineData("OpcA")]          // missing site/name shape
    [InlineData("NORTH-01/")]     // empty connection name
    [InlineData("/OpcA")]         // empty site
    public void ParseConnectionMappings_MalformedSource_Throws(string token)
    {
        Assert.Throws<FormatException>(() => BundleCommands.ParseConnectionMappings(new[] { token }));
    }

    [Fact]
    public void BundleImport_MapAndCreateMissingFlags_ParseWithoutError()
    {
        var url = new Option<string>("--url") { Recursive = true };
        var format = new Option<string>("--format") { Recursive = true };
        var username = new Option<string>("--username") { Recursive = true };
        var password = new Option<string>("--password") { Recursive = true };
        var bundle = BundleCommands.Build(url, format, username, password);

        var parse = bundle.Parse(new[]
        {
            "import", "--input", "/tmp/in.scadabundle",
            "--map-site", "NORTH-01=PLANT-A",
            "--map-connection", "NORTH-01/OpcA=OpcLive",
            "--create-missing-sites",
            "--create-missing-connections",
        });

        Assert.Empty(parse.Errors);
    }
}
