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

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

/// <summary>
///     v3 authored-tag surface for FOCAS. Under v3 a tag's wire reference is its <b>RawPath</b> and the
///     driver builds its <c>RawPath → definition</c> table from the authored <c>RawTagEntry</c> list via
///     <see cref="FocasTagDefinitionFactory.FromTagConfig"/> at <c>InitializeAsync</c> — there is no longer
///     a blob-parse fallback where the read reference IS the raw TagConfig JSON (a miss is a miss ⇒
///     BadNodeIdUnknown). These tests cover the mapper's parse/reject cases and the driver's authored-tag
///     read path.
/// </summary>
[Trait("Category", "Unit")]
public class FocasEquipmentTagTests
{
    private const string RawPath = "area/line/lathe/Run";

    [Fact]
    public void Maps_tagconfig_into_a_definition_keyed_by_rawpath()
    {
        var json = """{"address":"R100","dataType":"Byte"}""";
        FocasTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
        def!.Name.ShouldBe(RawPath);
        def.Address.ShouldBe("R100");
        def.DataType.ShouldBe(FocasDataType.Byte);
    }

    [Fact]
    public void Rejects_a_non_address_blob()
        => FocasTagDefinitionFactory.FromTagConfig("""{"FullName":"x"}""", RawPath, out _).ShouldBeFalse();

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

    [Fact]
    public void Rejects_address_as_a_json_number()
        => FocasTagDefinitionFactory.FromTagConfig(
            """{"address":100,"dataType":"Int32"}""", RawPath, out _).ShouldBeFalse();

    [Fact]
    public void Rejects_a_blank_address()
        => FocasTagDefinitionFactory.FromTagConfig(
            """{"address":"   ","dataType":"Int32"}""", RawPath, out _).ShouldBeFalse();

    // FOCAS's only mandatory address field is the canonical Address STRING (no narrow numeric
    // TagConfig field like Modbus's ushort register address), so the Modbus "numeric out-of-range"
    // rejection case has no analog here — covered instead by the json-number + blank-address cases.

    /// <summary>
    ///     End-to-end driver-level proof: a FOCAS driver serving an authored raw tag resolves it by its
    ///     RawPath and reads the wire instead of returning BadNodeIdUnknown.
    /// </summary>
    [Fact]
    public async Task Driver_resolves_an_authored_rawtag_and_reads_instead_of_BadNodeIdUnknown()
    {
        var factory = new FakeFocasClientFactory { Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)42 } } };
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = [FocasRawTags.Entry(new FocasTagDefinition(RawPath, "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
            Probe = new FocasProbeOptions { Enabled = false },
        }, "focas-eq", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var r = await drv.ReadAsync([RawPath], CancellationToken.None);

        r[0].StatusCode.ShouldBe(FocasStatusMapper.Good);
        r[0].StatusCode.ShouldNotBe(FocasStatusMapper.BadNodeIdUnknown);
        r[0].Value.ShouldBe((sbyte)42);
    }

    /// <summary>
    ///     A read reference that is NOT an authored RawPath (e.g. a raw TagConfig JSON — the retired
    ///     equipment-tag blob-ref) is a resolver miss and surfaces BadNodeIdUnknown; the blob-parse
    ///     fallback is gone in v3.
    /// </summary>
    [Fact]
    public async Task Blob_reference_is_a_resolver_miss_in_v3()
    {
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = [],
            Probe = new FocasProbeOptions { Enabled = false },
        }, "focas-eq", new FakeFocasClientFactory());
        await drv.InitializeAsync("{}", CancellationToken.None);

        var blob = """{"address":"R100","dataType":"Byte"}""";
        var r = await drv.ReadAsync([blob], CancellationToken.None);

        r[0].StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>
    ///     Regression guard for Driver.FOCAS-008: the parsed <see cref="FocasAddress"/> for an authored
    ///     tag is cached at <c>InitializeAsync</c> (v3 pre-caches the whole table) so reads/writes skip
    ///     re-parsing the address string on every call.
    /// </summary>
    [Fact]
    public async Task Authored_tag_parsed_address_is_cached_at_init()
    {
        var factory = new FakeFocasClientFactory { Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)7 } } };
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = [FocasRawTags.Entry(new FocasTagDefinition(RawPath, "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
            Probe = new FocasProbeOptions { Enabled = false },
        }, "focas-eq-cache", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // v3 builds the table eagerly at init, so the parsed address is cached before any read.
        drv.IsParsedAddressCached(RawPath).ShouldBeTrue();
        var r = await drv.ReadAsync([RawPath], CancellationToken.None);
        r[0].StatusCode.ShouldBe(FocasStatusMapper.Good);
    }
}
