using System.Text.Json;
using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;

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

/// <summary>
///     Tests for <see cref="MqttDriverFactoryExtensions"/> — the seam that turns a persisted
///     <c>DriverInstance.DriverConfig</c> blob into a live <see cref="MqttDriver"/>. Mirrors
///     <c>OpcUaClientDriverFactoryExtensions</c>' shape (direct deserialization into the options
///     record, no intermediate DTO).
/// </summary>
[Trait("Category", "Unit")]
public sealed class MqttDriverFactoryExtensionsTests
{
    [Fact]
    public void Register_ThenCreate_BuildsMqttDriver()
    {
        var registry = new DriverFactoryRegistry();
        MqttDriverFactoryExtensions.Register(registry);

        var factory = registry.TryGet(MqttDriverFactoryExtensions.DriverTypeName);
        factory.ShouldNotBeNull();

        var driver = factory("d1", """{"host":"h","port":1883,"mode":"Plain"}""");

        driver.ShouldBeOfType<MqttDriver>();
    }

    /// <summary>
    ///     The factory's registration key and the canonical constant must be the same string. A
    ///     drift here is invisible at compile time and silently breaks dispatch — the repo's
    ///     <c>ModbusTcp</c>/<c>Modbus</c> incident.
    /// </summary>
    [Fact]
    public void DriverTypeName_MatchesConstant()
        => MqttDriverFactoryExtensions.DriverTypeName.ShouldBe(DriverTypeNames.Mqtt);

    /// <summary>
    ///     Third independent pin on the literal itself: both the constant and the factory name could
    ///     be renamed together and still agree with each other while disagreeing with every persisted
    ///     <c>DriverInstance.DriverType</c> row.
    /// </summary>
    [Fact]
    public void DriverTypeName_IsExactlyMqtt()
    {
        MqttDriverFactoryExtensions.DriverTypeName.ShouldBe("Mqtt");
        DriverTypeNames.Mqtt.ShouldBe("Mqtt");
        DriverTypeNames.All.ShouldContain("Mqtt");
    }

    /// <summary>
    ///     <c>rawTags</c> is the ONLY source of the driver's discoverable node set (plain MQTT has no
    ///     browsable address space), so a factory that parsed everything else correctly but dropped
    ///     this array would produce a driver that connects, subscribes to nothing and materializes
    ///     nothing — green everywhere, dark in production.
    /// </summary>
    [Fact]
    public async Task CreateInstance_CarriesRawTagsThroughToDiscovery()
    {
        const string tagConfig = """{"topic":"f/t","payloadFormat":"Raw","dataType":"String"}""";
        var json = JsonSerializer.Serialize(new
        {
            host = "h",
            port = 1883,
            mode = "Plain",
            rawTags = new[]
            {
                new { rawPath = "Plant/Mqtt/dev1/Temp", tagConfig, writeIdempotent = false },
            },
        });

        var driver = MqttDriverFactoryExtensions.CreateInstance("d1", json);

        var builder = new RecordingBuilder();
        await driver.DiscoverAsync(builder, CancellationToken.None);

        builder.VariableFullNames.ShouldBe(["Plant/Mqtt/dev1/Temp"]);
    }

    /// <summary>
    ///     Enum-valued knobs must bind from their string NAMES — the repo's documented systemic
    ///     enum-serialization bug is an AdminUI-authored config whose enum field one seam can read
    ///     and another cannot. Removing <see cref="JsonStringEnumConverter"/> from
    ///     <see cref="MqttJson.Options"/> makes this throw.
    /// </summary>
    [Fact]
    public void CreateInstance_EnumsAuthoredAsNames_Bind()
    {
        const string json = """
            {"host":"h","port":8883,"mode":"SparkplugB","protocolVersion":"V311",
             "sparkplug":{"groupId":"Plant1","hostId":"h1"}}
            """;

        var driver = MqttDriverFactoryExtensions.CreateInstance("d1", json);

        driver.ShouldBeOfType<MqttDriver>();
    }

    /// <summary>
    ///     Direct pin on the single shared instance: the enum converter is what the test above
    ///     exercises behaviourally, and this asserts it structurally so a reader can see the one
    ///     instance every MQTT config seam parses through.
    /// </summary>
    [Fact]
    public void SharedJsonOptions_CarryTheStringEnumConverter()
        => MqttJson.Options.Converters.ShouldContain(c => c is JsonStringEnumConverter);

    [Fact]
    public void CreateInstance_BlankConfig_Throws()
        => Should.Throw<ArgumentException>(() => MqttDriverFactoryExtensions.CreateInstance("d1", "  "));

    [Fact]
    public void CreateInstance_JsonNullLiteral_ThrowsInvalidOperation()
        => Should.Throw<InvalidOperationException>(() => MqttDriverFactoryExtensions.CreateInstance("d1", "null"));

    /// <summary>
    ///     Minimal discovery recorder — local to this suite because the driver test project does not
    ///     reference <c>Commons</c>, where the runtime's capturing builder lives (same reason
    ///     <c>MqttDriverDiscoveryTests</c> carries its own).
    /// </summary>
    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        public List<string> VariableFullNames { get; } = [];

        public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;

        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
        {
            VariableFullNames.Add(attributeInfo.FullName);
            return new Handle(attributeInfo.FullName);
        }

        public void AddProperty(string browseName, DriverDataType dataType, object? value) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            public string FullReference => fullRef;

            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }

        private sealed class NullSink : IAlarmConditionSink
        {
            public void OnTransition(AlarmEventArgs args) { }
        }
    }
}
