using libplctag;
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>
///     Driver-timeout-hardening parity with AbCip: a read/write that fails with a non-zero libplctag
///     status or a transport exception must EVICT the cached runtime so the next call re-creates a
///     fresh handle — rather than returning the same failure forever until the probe loop happens to
///     recycle it. Previously AbLegacy evicted on neither path (unlike AbCip).
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyEvictOnFailureTests
{
    private const string Device = "ab://10.0.0.5/1,0";

    // Probe disabled — the probe loop also calls _tagFactory.Create, which would perturb the
    // creation count these tests assert on.
    private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags)
    {
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions(Device)],
            RawTags = AbLegacyRawTags.Entries(tags),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        return (drv, factory);
    }

    /// <summary>A non-zero libplctag status on read evicts the runtime; the next read creates a fresh handle and recovers.</summary>
    [Fact]
    public async Task Read_nonzero_status_evicts_runtime_so_next_read_creates_fresh_handle()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
        var callCount = 0;
        factory.Customise = p =>
        {
            callCount++;
            return callCount == 1
                ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
                : new FakeAbLegacyTag(p) { Status = 0, Value = 42 };
        };
        await drv.InitializeAsync("{}", CancellationToken.None);

        // First read — bad status → runtime evicted, no reuse.
        var first = await drv.ReadAsync(["Counter"], CancellationToken.None);
        first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good);

        // Second read — fresh handle, succeeds.
        var second = await drv.ReadAsync(["Counter"], CancellationToken.None);
        second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        second.Single().Value.ShouldBe(42);
        callCount.ShouldBe(2); // one failed handle + one fresh handle
    }

    /// <summary>A transport exception on read evicts the runtime; the next read creates a fresh handle and recovers.</summary>
    [Fact]
    public async Task Read_transport_exception_evicts_runtime_so_next_read_creates_fresh_handle()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
        var callCount = 0;
        factory.Customise = p =>
        {
            callCount++;
            return callCount == 1
                ? new FakeAbLegacyTag(p) { ThrowOnRead = true, Exception = new System.Net.Sockets.SocketException() }
                : new FakeAbLegacyTag(p) { Status = 0, Value = 7 };
        };
        await drv.InitializeAsync("{}", CancellationToken.None);

        var first = await drv.ReadAsync(["Counter"], CancellationToken.None);
        first.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);

        var second = await drv.ReadAsync(["Counter"], CancellationToken.None);
        second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        second.Single().Value.ShouldBe(7);
        callCount.ShouldBe(2);
    }

    /// <summary>A non-zero libplctag status on write evicts the runtime; the next write creates a fresh handle and recovers.</summary>
    [Fact]
    public async Task Write_nonzero_status_evicts_runtime_so_next_write_creates_fresh_handle()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
        var callCount = 0;
        factory.Customise = p =>
        {
            callCount++;
            return callCount == 1
                ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
                : new FakeAbLegacyTag(p) { Status = 0 };
        };
        await drv.InitializeAsync("{}", CancellationToken.None);

        var first = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None);
        first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good);

        var second = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None);
        second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        callCount.ShouldBe(2);
    }
}
