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 AbCipDriverWriteTests
{
    private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
    {
        var factory = new FakeAbCipTagFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(tags),
        }, "drv-1", factory);
        return (drv, factory);
    }

    /// <summary>Verifies that 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 results = await drv.WriteAsync(
            [new WriteRequest("does-not-exist", 1)], CancellationToken.None);

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

    /// <summary>Verifies that non-writable tags map to BadNotWritable status.</summary>
    [Fact]
    public async Task Non_writable_tag_maps_to_BadNotWritable()
    {
        var (drv, _) = NewDriver(
            new AbCipTagDefinition("ReadOnly", "ab://10.0.0.5/1,0", "RO", AbCipDataType.DInt, Writable: false));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("ReadOnly", 7)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNotWritable);
    }

    /// <summary>Verifies that successful DInt writes encode and flush values.</summary>
    [Fact]
    public async Task Successful_DInt_write_encodes_and_flushes()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("Speed", 4200)], CancellationToken.None);

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

    /// <summary>Verifies that bit-in-DInt writes succeed via read-modify-write.</summary>
    [Fact]
    public async Task Bit_in_dint_write_now_succeeds_via_RMW()
    {
        // Task #181 pass 2 lifted this gap — BOOL-within-DINT writes now go through
        // WriteBitInDIntAsync + a parallel parent-DINT runtime, so the result is Good rather
        // than BadNotSupported. Full RMW semantics covered by AbCipBoolInDIntRmwTests.
        var factory = new FakeAbCipTagFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Flags.3", AbCipDataType.Bool)),
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("Flag3", true)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
    }

    /// <summary>Verifies that non-zero libplctag status after write maps correctly.</summary>
    [Fact]
    public async Task Non_zero_libplctag_status_after_write_maps_via_AbCipStatusMapper()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorTimeout };

        var results = await drv.WriteAsync(
            [new WriteRequest("Broken", 1)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadTimeout);
    }

    /// <summary>Verifies that type mismatch surfaces BadTypeMismatch status.</summary>
    [Fact]
    public async Task Type_mismatch_surfaces_BadTypeMismatch()
    {
        var (drv, _) = NewDriver(
            new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Force a FormatException inside Convert.ToInt32 via a runtime that forwards to real Convert.
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new RealConvertFake(p),
        };
        var drv2 = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt)),
        }, "drv-2", factory);
        await drv2.InitializeAsync("{}", CancellationToken.None);

        var results = await drv2.WriteAsync(
            [new WriteRequest("Speed", "not-a-number")], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
    }

    /// <summary>Verifies that overflow surfaces BadOutOfRange status.</summary>
    [Fact]
    public async Task Overflow_surfaces_BadOutOfRange()
    {
        var factory = new FakeAbCipTagFactory { Customise = p => new RealConvertFake(p) };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Narrow", "ab://10.0.0.5/1,0", "N", AbCipDataType.Int)),
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("Narrow", 1_000_000)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
    }

    /// <summary>Verifies that exceptions during write surface BadCommunicationError.</summary>
    [Fact]
    public async Task Exception_during_write_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new ThrowOnWriteFake(p);

        var results = await drv.WriteAsync(
            [new WriteRequest("Broken", 1)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
        drv.GetHealth().State.ShouldBe(DriverState.Degraded);
    }

    /// <summary>Verifies that batch write preserves order across success and failure.</summary>
    [Fact]
    public async Task Batch_preserves_order_across_success_and_failure()
    {
        var factory = new FakeAbCipTagFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(
                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, Writable: false),
                new AbCipTagDefinition("C", "ab://10.0.0.5/1,0", "C", AbCipDataType.DInt))
            ,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
        [
            new WriteRequest("A", 1),
            new WriteRequest("B", 2),
            new WriteRequest("UnknownTag", 3),
            new WriteRequest("C", 4),
        ], CancellationToken.None);

        results.Count.ShouldBe(4);
        results[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
        results[1].StatusCode.ShouldBe(AbCipStatusMapper.BadNotWritable);
        results[2].StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
        results[3].StatusCode.ShouldBe(AbCipStatusMapper.Good);
    }

    /// <summary>Verifies that cancellation propagates from write operations.</summary>
    [Fact]
    public async Task Cancellation_propagates_from_write()
    {
        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 CancelOnWriteFake(p);

        await Should.ThrowAsync<OperationCanceledException>(
            () => drv.WriteAsync([new WriteRequest("Slow", 1)], CancellationToken.None));
    }

    // ---- test-fake variants that exercise the real type / error handling ----

    /// <summary>Test fake that uses real Convert methods to exercise type conversion errors.</summary>
    private sealed class RealConvertFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
    {
        /// <inheritdoc />
        public override void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
        {
            switch (type)
            {
                case AbCipDataType.Int: _ = Convert.ToInt16(value); break;
                case AbCipDataType.DInt: _ = Convert.ToInt32(value); break;
                default: _ = Convert.ToInt32(value); break;
            }
            Value = value;
        }
    }

    private sealed class ThrowingBoolBitFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
    {
        /// <inheritdoc />
        public override void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
        {
            if (type == AbCipDataType.Bool && bitIndex is not null)
                throw new NotSupportedException("bit-in-DINT deferred");
            Value = value;
        }
    }

    /// <summary>Test fake that throws on write to simulate communication errors.</summary>
    private sealed class ThrowOnWriteFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
    {
        /// <inheritdoc />
        public override Task WriteAsync(CancellationToken ct) =>
            Task.FromException(new InvalidOperationException("wire dropped"));
    }

    /// <summary>Test fake that cancels during write to simulate cancellation.</summary>
    private sealed class CancelOnWriteFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
    {
        /// <inheritdoc />
        public override Task WriteAsync(CancellationToken ct) =>
            Task.FromException(new OperationCanceledException());
    }
}
