using libplctag;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

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

[Trait("Category", "Unit")]
public sealed class AbCipDriverReadTests
{
    private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
    {
        var factory = new FakeAbCipTagFactory();
        var opts = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(tags),
        };
        var drv = new AbCipDriver(opts, "drv-1", factory);
        return (drv, factory);
    }

    /// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown status.</summary>
    [Fact]
    public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
    {
        var (drv, _) = NewDriver();
        await drv.InitializeAsync("{}", CancellationToken.None);

        var snapshots = await drv.ReadAsync(["does-not-exist"], CancellationToken.None);

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

    /// <summary>Verifies that a tag on an unknown device maps to BadNodeIdUnknown status.</summary>
    [Fact]
    public async Task Tag_on_unknown_device_maps_to_BadNodeIdUnknown()
    {
        var factory = new FakeAbCipTagFactory();
        var opts = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "Tag1", AbCipDataType.DInt)),
        };
        var drv = new AbCipDriver(opts, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var snapshots = await drv.ReadAsync(["Orphan"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that a successful DInt read returns Good status with the correct value.</summary>
    [Fact]
    public async Task Successful_DInt_read_returns_Good_with_value()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Customise the fake before the first read so the tag returns 4200.
        factory.Customise = p => new FakeAbCipTag(p) { Value = 4200 };

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

        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
        snapshots.Single().Value.ShouldBe(4200);
        factory.Tags["Motor1.Speed"].InitializeCount.ShouldBe(1);
        factory.Tags["Motor1.Speed"].ReadCount.ShouldBe(1);
    }

    /// <summary>Verifies that repeated reads reuse the runtime without reinitializing.</summary>
    [Fact]
    public async Task Repeat_read_reuses_runtime_without_reinitialise()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Value = 1 };

        await drv.ReadAsync(["Speed"], CancellationToken.None);
        await drv.ReadAsync(["Speed"], CancellationToken.None);
        await drv.ReadAsync(["Speed"], CancellationToken.None);

        factory.Tags["Motor1.Speed"].InitializeCount.ShouldBe(1); // lazy init happens once
        factory.Tags["Motor1.Speed"].ReadCount.ShouldBe(3);
    }

    /// <summary>Verifies that non-zero libplctag status is mapped via AbCipStatusMapper.</summary>
    [Fact]
    public async Task NonZero_libplctag_status_maps_via_AbCipStatusMapper()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Ghost", "ab://10.0.0.5/1,0", "Missing.Tag", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorNotFound };

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

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

    /// <summary>Verifies that an exception during read surfaces BadCommunicationError status.</summary>
    [Fact]
    public async Task Exception_during_read_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.Real));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { ThrowOnRead = true };

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

        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
        snapshots.Single().Value.ShouldBeNull();
        drv.GetHealth().State.ShouldBe(DriverState.Degraded);
    }

    /// <summary>Verifies that batched reads preserve order and per-tag status.</summary>
    [Fact]
    public async Task Batched_reads_preserve_order_and_per_tag_status()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
            new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "B", AbCipDataType.Real),
            new AbCipTagDefinition("C", "ab://10.0.0.5/1,0", "C", AbCipDataType.String));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => p.TagName switch
        {
            "A" => new FakeAbCipTag(p) { Value = 42 },
            "B" => new FakeAbCipTag(p) { Value = 3.14f },
            _ => new FakeAbCipTag(p) { Value = "hello" },
        };

        var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);

        snapshots.Count.ShouldBe(3);
        snapshots[0].Value.ShouldBe(42);
        snapshots[1].Value.ShouldBe(3.14f);
        snapshots[2].Value.ShouldBe("hello");
        snapshots.ShouldAllBe(s => s.StatusCode == AbCipStatusMapper.Good);
    }

    /// <summary>Verifies that a successful read marks health as Healthy.</summary>
    [Fact]
    public async Task Successful_read_marks_health_Healthy()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Pressure", "ab://10.0.0.5/1,0", "PT_101", AbCipDataType.Real));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Value = 14.7f };

        await drv.ReadAsync(["Pressure"], CancellationToken.None);
        drv.GetHealth().State.ShouldBe(DriverState.Healthy);
        drv.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
    }

    /// <summary>Verifies that tag creation parameters are built correctly from device and profile.</summary>
    [Fact]
    public async Task TagCreateParams_are_built_from_device_and_profile()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Counter", "ab://10.0.0.5/1,0", "Program:P.Counter", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.ReadAsync(["Counter"], CancellationToken.None);

        var p = factory.Tags["Program:P.Counter"].CreationParams;
        p.Gateway.ShouldBe("10.0.0.5");
        p.Port.ShouldBe(44818);
        p.CipPath.ShouldBe("1,0");
        p.LibplctagPlcAttribute.ShouldBe("controllogix");
        p.TagName.ShouldBe("Program:P.Counter");
    }

    /// <summary>Verifies that cancellation propagates from read operations.</summary>
    [Fact]
    public async Task Cancellation_propagates_from_read()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Slow", "ab://10.0.0.5/1,0", "Slow", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p)
        {
            ThrowOnRead = true,
            Exception = new OperationCanceledException(),
        };

        using var cts = new CancellationTokenSource();
        await Should.ThrowAsync<OperationCanceledException>(
            () => drv.ReadAsync(["Slow"], cts.Token));
    }

    /// <summary>Verifies that ShutdownAsync disposes each tag runtime.</summary>
    [Fact]
    public async Task ShutdownAsync_disposes_each_tag_runtime()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
            new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "B", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Value = 1 };
        await drv.ReadAsync(["A", "B"], CancellationToken.None);

        await drv.ShutdownAsync(CancellationToken.None);

        factory.Tags["A"].Disposed.ShouldBeTrue();
        factory.Tags["B"].Disposed.ShouldBeTrue();
    }

    /// <summary>Verifies that initialization failure disposes the tag and surfaces communication error.</summary>
    [Fact]
    public async Task Initialize_failure_disposes_tag_and_surfaces_communication_error()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("DoomedTag", "ab://10.0.0.5/1,0", "Nope", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { ThrowOnInitialize = true };

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

        snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
        factory.Tags["Nope"].Disposed.ShouldBeTrue();
    }
}
