using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Sparkplug;
using Shouldly;
using Xunit;

// Local alias so this file can spell the mapping table the way design doc §3.5 does
// (`SparkplugDataType.Int8`, etc.) — `SparkplugDataType` is a `global using` alias for the generated
// `Org.Eclipse.Tahu.Protobuf.DataType` declared in `SparkplugDataType.cs` (Contracts project); that
// `global using` is scoped to the project that declares it and does not cross the ProjectReference
// into this test project, so it is redeclared here, locally, rather than duplicating the enum itself.
// See the remarks on `SparkplugDataTypeExtensions` for the full alias-vs-duplicate rationale.
using SparkplugDataType = Org.Eclipse.Tahu.Protobuf.DataType;

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

/// <summary>
///     <see cref="SparkplugTopic"/> parse/format coverage (Task 17). <see cref="SparkplugTopic.TryParse"/>
///     is fed every topic string a live <c>spBv1.0/{group}/#</c> subscription delivers, so a large
///     share of this suite is "garbage in, <see langword="false"/> out, never a throw" — see
///     <see cref="TryParse_NeverThrows_ForArbitraryInput"/>.
/// </summary>
public sealed class SparkplugTopicTests
{
    [Fact]
    public void Parse_DeviceData_ExtractsAllSegments()
    {
        var t = SparkplugTopic.Parse("spBv1.0/Plant1/DDATA/EdgeA/Filler1");

        t.GroupId.ShouldBe("Plant1");
        t.Type.ShouldBe(SparkplugMessageType.DDATA);
        t.EdgeNodeId.ShouldBe("EdgeA");
        t.DeviceId.ShouldBe("Filler1");
        t.HostId.ShouldBeNull();
    }

    [Fact]
    public void Parse_NodeData_HasNoDeviceSegment()
    {
        var t = SparkplugTopic.Parse("spBv1.0/Plant1/NDATA/EdgeA");

        t.GroupId.ShouldBe("Plant1");
        t.Type.ShouldBe(SparkplugMessageType.NDATA);
        t.EdgeNodeId.ShouldBe("EdgeA");
        t.DeviceId.ShouldBeNull();
        t.HostId.ShouldBeNull();
    }

    [Theory]
    [InlineData("spBv1.0/Plant1/NBIRTH/EdgeA", SparkplugMessageType.NBIRTH)]
    [InlineData("spBv1.0/Plant1/NDATA/EdgeA", SparkplugMessageType.NDATA)]
    [InlineData("spBv1.0/Plant1/NDEATH/EdgeA", SparkplugMessageType.NDEATH)]
    [InlineData("spBv1.0/Plant1/NCMD/EdgeA", SparkplugMessageType.NCMD)]
    public void Parse_NodeScopedTypes_Recognised(string topic, SparkplugMessageType expected)
        => SparkplugTopic.Parse(topic).Type.ShouldBe(expected);

    [Theory]
    [InlineData("spBv1.0/Plant1/DBIRTH/EdgeA/Filler1", SparkplugMessageType.DBIRTH)]
    [InlineData("spBv1.0/Plant1/DDATA/EdgeA/Filler1", SparkplugMessageType.DDATA)]
    [InlineData("spBv1.0/Plant1/DDEATH/EdgeA/Filler1", SparkplugMessageType.DDEATH)]
    [InlineData("spBv1.0/Plant1/DCMD/EdgeA/Filler1", SparkplugMessageType.DCMD)]
    public void Parse_DeviceScopedTypes_Recognised(string topic, SparkplugMessageType expected)
        => SparkplugTopic.Parse(topic).Type.ShouldBe(expected);

    [Fact]
    public void Parse_V3StateForm_ExtractsHostId()
    {
        var t = SparkplugTopic.Parse("spBv1.0/STATE/otopcua-host-1");

        t.Type.ShouldBe(SparkplugMessageType.STATE);
        t.HostId.ShouldBe("otopcua-host-1");
        t.GroupId.ShouldBeNull();
        t.EdgeNodeId.ShouldBeNull();
        t.DeviceId.ShouldBeNull();
    }

    [Fact]
    public void Parse_LegacyStateForm_ExtractsHostId()
    {
        // Pre-3.0 peers publish `STATE/{hostId}` with no `spBv1.0` namespace prefix — tolerated on
        // receive per design §3.1, even though this driver only ever *emits* the v3.0 form.
        var t = SparkplugTopic.Parse("STATE/otopcua-host-1");

        t.Type.ShouldBe(SparkplugMessageType.STATE);
        t.HostId.ShouldBe("otopcua-host-1");
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("not/a/sparkplug/topic/at/all/too/many/segments")]
    [InlineData("spBv1.0")]
    [InlineData("spBv1.0/")]
    [InlineData("spBv1.0/Plant1")]
    [InlineData("spBv1.0/Plant1/BOGUS/EdgeA")]
    [InlineData("spBv1.0/Plant1/NDATA")]
    [InlineData("spBv1.0/Plant1/NDATA/EdgeA/UnexpectedDevice")]
    [InlineData("spBv1.0/Plant1/DDATA/EdgeA")]
    [InlineData("spBv1.0/Plant1/DDATA/EdgeA/Filler1/Extra")]
    [InlineData("wrong-namespace/Plant1/NDATA/EdgeA")]
    [InlineData("spBv1.0//NDATA/EdgeA")]
    [InlineData("spBv1.0/Plant1/NDATA/")]
    [InlineData("spBv1.0/Plant1/DDATA/EdgeA/")]
    [InlineData("spBv1.0/Plant+/NDATA/EdgeA")]
    [InlineData("spBv1.0/Plant1/NDATA/Edge#A")]
    [InlineData("spBv1.0/STATE")]
    [InlineData("spBv1.0/STATE/")]
    [InlineData("spBv1.0/STATE/Host/Extra")]
    [InlineData("STATE")]
    [InlineData("STATE/")]
    public void TryParse_NeverThrows_ForArbitraryInput(string? topic)
    {
        var ex = Record.Exception(() => SparkplugTopic.TryParse(topic, out var result));
        ex.ShouldBeNull();
        SparkplugTopic.TryParse(topic, out var result2).ShouldBeFalse();
        result2.ShouldBeNull();
    }

    [Fact]
    public void Parse_InvalidTopic_ThrowsFormatException()
        => Should.Throw<FormatException>(() => SparkplugTopic.Parse("not-a-sparkplug-topic"));

    [Fact]
    public void Format_NodeScoped_BuildsExpectedTopic()
        => SparkplugTopic.Format("Plant1", SparkplugMessageType.NCMD, "EdgeA")
            .ShouldBe("spBv1.0/Plant1/NCMD/EdgeA");

    [Fact]
    public void Format_DeviceScoped_BuildsExpectedTopic()
        => SparkplugTopic.Format("Plant1", SparkplugMessageType.DCMD, "EdgeA", "Filler1")
            .ShouldBe("spBv1.0/Plant1/DCMD/EdgeA/Filler1");

    [Fact]
    public void Format_State_Throws_UseFormatStateInstead()
        => Should.Throw<ArgumentException>(() => SparkplugTopic.Format("Plant1", SparkplugMessageType.STATE, "EdgeA"));

    [Fact]
    public void FormatState_BuildsV3Topic()
        => SparkplugTopic.FormatState("otopcua-host-1").ShouldBe("spBv1.0/STATE/otopcua-host-1");

    [Theory]
    [InlineData("spBv1.0/Plant1/NBIRTH/EdgeA")]
    [InlineData("spBv1.0/Plant1/NDATA/EdgeA")]
    [InlineData("spBv1.0/Plant1/NDEATH/EdgeA")]
    [InlineData("spBv1.0/Plant1/NCMD/EdgeA")]
    [InlineData("spBv1.0/Plant1/DBIRTH/EdgeA/Filler1")]
    [InlineData("spBv1.0/Plant1/DDATA/EdgeA/Filler1")]
    [InlineData("spBv1.0/Plant1/DDEATH/EdgeA/Filler1")]
    [InlineData("spBv1.0/Plant1/DCMD/EdgeA/Filler1")]
    [InlineData("spBv1.0/STATE/otopcua-host-1")]
    public void ToTopicString_RoundTrips(string topic)
        => SparkplugTopic.Parse(topic).ToTopicString().ShouldBe(topic);

    [Fact]
    public void ToTopicString_LegacyStateTopic_NormalisesToV3Form()
        // Parsed from the legacy no-prefix form, but formatting always emits the v3.0 shape.
        => SparkplugTopic.Parse("STATE/otopcua-host-1").ToTopicString().ShouldBe("spBv1.0/STATE/otopcua-host-1");

    // ---- The plan's illustrative datatype-map smoke theory (spelled with the protoc-mangled member
    //      names: the generated enum has `Uint8`, not `UInt8` — see SparkplugDataTypeTests for the
    //      full table). Kept here alongside the topic tests per the Task 17 plan's original grouping;
    //      SparkplugDataTypeTests.cs carries the exhaustive coverage + the completeness/drift guard. ----
    [Theory]
    [InlineData(SparkplugDataType.Int8, DriverDataType.Int16)]
    [InlineData(SparkplugDataType.Uint8, DriverDataType.UInt16)]
    [InlineData(SparkplugDataType.Float, DriverDataType.Float32)]
    public void ToDriverDataType_MapsAndWidens(SparkplugDataType s, DriverDataType d)
        => s.ToDriverDataType().ShouldBe(d);
}
