using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

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

[Trait("Category", "Unit")]
public class AbCipEquipmentTagTests
{
    private const string Raw = "line1/eq/tag";

    [Fact]
    public void Parses_equipment_tagconfig_into_a_definition()
    {
        var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Motor1.Speed","dataType":"Real"}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Name.ShouldBe(Raw);
        def.TagPath.ShouldBe("Motor1.Speed");
        def.DeviceHostAddress.ShouldBe(""); // v3: mapper never reads deviceHostAddress
        def.DataType.ShouldBe(AbCipDataType.Real);
        def.Writable.ShouldBeTrue();
    }

    [Fact]
    public void Defaults_optional_fields_when_only_the_mandatory_tagPath_is_present()
    {
        var json = """{"tagPath":"PT_101"}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Name.ShouldBe(Raw);
        def.TagPath.ShouldBe("PT_101");
        def.DeviceHostAddress.ShouldBe(""); // routing key travels on the RawTagEntry, not the blob
        def.DataType.ShouldBe(AbCipDataType.DInt); // AbCipTagConfigModel's default atomic type
    }

    [Fact]
    public void Rejects_a_non_abcip_blob()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"region":"x"}""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Rejects_a_blob_with_an_empty_tagPath()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":"  ","dataType":"DInt"}""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Rejects_tagPath_given_as_a_non_string()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":42}""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Rejects_garbage()
        => AbCipTagDefinitionFactory.FromTagConfig("not json", Raw, out _).ShouldBeFalse();

    /// <summary>
    ///     End-to-end driver-level proof: an authored raw AbCip tag (RawPath + TagConfig blob delivered as a
    ///     <c>RawTagEntry</c>) resolves by its RawPath and the read goes to the wire instead of returning
    ///     BadNodeIdUnknown. This is the v3 replacement for the retired blob-parse-fallback resolver.
    /// </summary>
    [Fact]
    public async Task Driver_resolves_a_raw_tag_and_reads_instead_of_BadNodeIdUnknown()
    {
        const string device = "ab://10.0.0.5/1,0";
        const string rawPath = "line1/motor/speed";
        var tagConfig = """{"tagPath":"Motor1.Speed","dataType":"DInt"}""";
        var factory = new FakeAbCipTagFactory();
        var opts = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(device)],
            RawTags = [new global::ZB.MOM.WW.OtOpcUa.Core.Abstractions.RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: device)],
        };
        var drv = new AbCipDriver(opts, "abcip-eq", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Value = 4242 };

        var snapshots = await drv.ReadAsync([rawPath], CancellationToken.None);

        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
        snapshots.Single().StatusCode.ShouldNotBe(AbCipStatusMapper.BadNodeIdUnknown);
        snapshots.Single().Value.ShouldBe(4242);
        // The runtime is keyed by the resolved libplctag tag path, not the RawPath identity.
        factory.Tags.ShouldContainKey("Motor1.Speed");
    }

    /// <summary>
    ///     R2-11 Phase C strictness end-to-end: a raw tag whose TagConfig carries a TYPO'd <c>dataType</c>
    ///     fails to map (<c>FromTagConfig</c> ⇒ false) so it never enters the driver's RawPath table; a read
    ///     of that RawPath surfaces <see cref="AbCipStatusMapper.BadNodeIdUnknown"/> instead of a
    ///     silently-defaulted wrong-width Good.
    /// </summary>
    [Fact]
    public async Task Driver_read_of_a_typod_dataType_tag_surfaces_BadNodeIdUnknown()
    {
        const string device = "ab://10.0.0.5/1,0";
        const string rawPath = "line1/motor/speed";
        var typoConfig = """{"tagPath":"Motor1.Speed","dataType":"DIntt"}""";
        var factory = new FakeAbCipTagFactory();
        var opts = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(device)],
            RawTags = [new global::ZB.MOM.WW.OtOpcUa.Core.Abstractions.RawTagEntry(rawPath, typoConfig, WriteIdempotent: false, DeviceName: device)],
        };
        var drv = new AbCipDriver(opts, "abcip-eq", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Value = 4242 };

        var snapshots = await drv.ReadAsync([rawPath], CancellationToken.None);

        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
    }
}
