using System.Runtime.CompilerServices;
using Google.Protobuf;
using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;

/// <summary>
///     Guards the committed <c>Golden/*.bin</c> Sparkplug-B wire vectors: they must still be exactly
///     what <see cref="SparkplugGoldenPayloads"/> produces, and they must actually reach the test
///     binary's output directory.
/// </summary>
/// <remarks>
///     <para>
///         Without the drift guard the vectors would be write-once decoration — a re-vendored proto,
///         a protoc upgrade, or an edited builder could change the encoding while every decode test
///         kept passing against stale bytes. With it, the committed files are a genuine pin on the
///         wire format the driver is expected to read.
///     </para>
///     <para>
///         <b>To regenerate</b> (only after a deliberate, reviewed change to the vectors):
///         <c>OTOPCUA_SPARKPLUG_GOLDEN_REGEN=1 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests
///         --filter "FullyQualifiedName~SparkplugGoldenVectorTests"</c>, then review and commit the
///         resulting <c>.bin</c> diff.
///     </para>
/// </remarks>
public sealed class SparkplugGoldenVectorTests
{
    private const string RegenEnvVar = "OTOPCUA_SPARKPLUG_GOLDEN_REGEN";

    /// <summary>The committed NBIRTH vector is byte-identical to its builder's output.</summary>
    [Fact]
    public void Nbirth_CommittedBytes_MatchTheBuilder() =>
        AssertMatchesBuilder(SparkplugGoldenPayloads.NbirthFile, SparkplugGoldenPayloads.BuildNbirth().ToByteArray());

    /// <summary>The committed NDATA vector is byte-identical to its builder's output.</summary>
    [Fact]
    public void Ndata_CommittedBytes_MatchTheBuilder() =>
        AssertMatchesBuilder(SparkplugGoldenPayloads.NdataFile, SparkplugGoldenPayloads.BuildNdata().ToByteArray());

    /// <summary>
    ///     Both vectors are present in the test binary's output directory — i.e. the csproj really does
    ///     copy <c>Golden/**</c>. A missing copy item is invisible in source and turns every decode test
    ///     into a <see cref="FileNotFoundException"/> at runtime.
    /// </summary>
    [Fact]
    public void GoldenVectors_AreCopiedToTheOutputDirectory()
    {
        File.Exists(SparkplugGoldenPayloads.PathTo(SparkplugGoldenPayloads.NbirthFile)).ShouldBeTrue();
        File.Exists(SparkplugGoldenPayloads.PathTo(SparkplugGoldenPayloads.NdataFile)).ShouldBeTrue();
    }

    /// <summary>Compares a committed vector to freshly built bytes, regenerating first when asked to.</summary>
    /// <param name="fileName">The vector's file name.</param>
    /// <param name="built">The bytes the builder produces now.</param>
    private static void AssertMatchesBuilder(string fileName, byte[] built)
    {
        if (Environment.GetEnvironmentVariable(RegenEnvVar) == "1")
        {
            // Write to BOTH the source tree (so the change can be committed) and the output directory
            // (so the assertion below reads what was just written rather than a stale copied file).
            var source = Path.Combine(SourceDirectory(), SparkplugGoldenPayloads.Directory, fileName);
            System.IO.Directory.CreateDirectory(Path.GetDirectoryName(source)!);
            File.WriteAllBytes(source, built);

            var output = SparkplugGoldenPayloads.PathTo(fileName);
            System.IO.Directory.CreateDirectory(Path.GetDirectoryName(output)!);
            File.WriteAllBytes(output, built);
        }

        var path = SparkplugGoldenPayloads.PathTo(fileName);
        File.Exists(path).ShouldBeTrue(
            $"Golden vector '{fileName}' is missing from the output directory. If the csproj copy item is "
            + $"intact, regenerate with {RegenEnvVar}=1.");

        File.ReadAllBytes(path).ShouldBe(
            built,
            $"Golden vector '{fileName}' no longer matches SparkplugGoldenPayloads. Either the builder or "
            + $"the vendored proto changed. If the change is intended, regenerate with {RegenEnvVar}=1 and "
            + "commit the .bin diff.");
    }

    /// <summary>This file's own source directory, resolved at compile time.</summary>
    /// <param name="path">Supplied by the compiler; never pass it.</param>
    /// <returns>The directory holding the test sources.</returns>
    private static string SourceDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!;
}
