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;
///
/// Tests for — the seam that turns a persisted
/// DriverInstance.DriverConfig blob into a live . Mirrors
/// OpcUaClientDriverFactoryExtensions' shape (direct deserialization into the options
/// record, no intermediate DTO).
///
[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();
}
///
/// 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
/// ModbusTcp/Modbus incident.
///
[Fact]
public void DriverTypeName_MatchesConstant()
=> MqttDriverFactoryExtensions.DriverTypeName.ShouldBe(DriverTypeNames.Mqtt);
///
/// 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
/// DriverInstance.DriverType row.
///
[Fact]
public void DriverTypeName_IsExactlyMqtt()
{
MqttDriverFactoryExtensions.DriverTypeName.ShouldBe("Mqtt");
DriverTypeNames.Mqtt.ShouldBe("Mqtt");
DriverTypeNames.All.ShouldContain("Mqtt");
}
///
/// rawTags 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.
///
[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"]);
}
///
/// 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 from
/// makes this throw.
///
[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();
}
///
/// 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.
///
[Fact]
public void SharedJsonOptions_CarryTheStringEnumConverter()
=> MqttJson.Options.Converters.ShouldContain(c => c is JsonStringEnumConverter);
[Fact]
public void CreateInstance_BlankConfig_Throws()
=> Should.Throw(() => MqttDriverFactoryExtensions.CreateInstance("d1", " "));
[Fact]
public void CreateInstance_JsonNullLiteral_ThrowsInvalidOperation()
=> Should.Throw(() => MqttDriverFactoryExtensions.CreateInstance("d1", "null"));
///
/// Minimal discovery recorder — local to this suite because the driver test project does not
/// reference Commons, where the runtime's capturing builder lives (same reason
/// MqttDriverDiscoveryTests carries its own).
///
private sealed class RecordingBuilder : IAddressSpaceBuilder
{
public List 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) { }
}
}
}