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

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

/// <summary>
///     Regression coverage for Driver.AbLegacy-006 — a per-tag libplctag runtime (a single
///     <c>Tag</c> handle) is cached and shared between the server read path and the poll loop.
///     A <c>Tag</c> is not safe for concurrent operations, so the driver must serialise the
///     Read → GetStatus → DecodeValue (and Encode → Write → GetStatus) sequence per runtime.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyRuntimeConcurrencyTests
{
    /// <summary>
    ///     A fake runtime that records the maximum number of operations in flight against the
    ///     <em>same</em> handle. If the driver fails to serialise, two callers overlap inside the
    ///     Read → GetStatus → Decode window and <see cref="MaxConcurrent"/> exceeds 1.
    /// </summary>
    private sealed class OverlapDetectingFake : FakeAbLegacyTag
    {
        private int _inFlight;

        /// <summary>Gets the maximum number of concurrent operations detected.</summary>
        public int MaxConcurrent { get; private set; }

        /// <summary>Initializes a new instance of the OverlapDetectingFake class.</summary>
        /// <param name="p">The tag creation parameters.</param>
        public OverlapDetectingFake(AbLegacyTagCreateParams p) : base(p) { }

        /// <summary>Reads the tag asynchronously while tracking concurrent operations.</summary>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A task representing the read operation.</returns>
        public override async Task ReadAsync(CancellationToken ct)
        {
            EnterOp();
            try
            {
                // Yield + small delay so an unserialised second caller is guaranteed to overlap.
                await Task.Delay(15, ct).ConfigureAwait(false);
                await base.ReadAsync(ct).ConfigureAwait(false);
            }
            finally { LeaveOp(); }
        }

        /// <summary>Writes to the tag asynchronously while tracking concurrent operations.</summary>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A task representing the write operation.</returns>
        public override async Task WriteAsync(CancellationToken ct)
        {
            EnterOp();
            try
            {
                await Task.Delay(15, ct).ConfigureAwait(false);
                await base.WriteAsync(ct).ConfigureAwait(false);
            }
            finally { LeaveOp(); }
        }

        private void EnterOp()
        {
            var n = Interlocked.Increment(ref _inFlight);
            lock (this) { if (n > MaxConcurrent) MaxConcurrent = n; }
        }

        private void LeaveOp() => Interlocked.Decrement(ref _inFlight);
    }

    /// <summary>Verifies that concurrent reads of the same tag are serialised against the shared runtime.</summary>
    [Fact]
    public async Task Concurrent_reads_of_same_tag_are_serialised_against_the_shared_runtime()
    {
        OverlapDetectingFake? shared = null;
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p =>
            {
                shared = new OverlapDetectingFake(p) { Value = 7 };
                return shared;
            },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Eight callers race for the same tag — mimics the server read path + poll loop(s)
        // hitting one cached runtime at once.
        var reads = Enumerable.Range(0, 8)
            .Select(_ => drv.ReadAsync(["X"], CancellationToken.None))
            .ToArray();
        await Task.WhenAll(reads);

        shared.ShouldNotBeNull();
        shared!.MaxConcurrent.ShouldBe(1, "operations on a shared libplctag Tag must not overlap");
        reads.ShouldAllBe(r => r.Result.Single().Value!.Equals(7));
    }

    /// <summary>Verifies that concurrent read and write operations on the same tag do not overlap.</summary>
    [Fact]
    public async Task Concurrent_read_and_write_of_same_tag_do_not_overlap()
    {
        OverlapDetectingFake? shared = null;
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p =>
            {
                shared = new OverlapDetectingFake(p) { Value = 1 };
                return shared;
            },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var readTask = drv.ReadAsync(["X"], CancellationToken.None);
        var writeTask = drv.WriteAsync([new WriteRequest("X", 99)], CancellationToken.None);
        await Task.WhenAll(readTask, writeTask);

        shared.ShouldNotBeNull();
        shared!.MaxConcurrent.ShouldBe(1);
    }
}
