using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     archreview 06/S-1: Galaxy writes must fail CLOSED. When <c>AdviseSupervisory</c> fails, the raw
///     Write (which the file comment notes "doesn't throw but the value never reaches the galaxy") must
///     NOT be issued — the writer returns <c>BadCommunicationError</c> so the #5 node-revert fires instead
///     of leaving a phantom-Good node. These tests drive the real write pipeline through the internal
///     <see cref="IMxWriteOps"/> seam (the SDK session types are sealed + internal-ctor and cannot be faked).
/// </summary>
public sealed class GatewayGalaxyDataWriterFailClosedTests
{
    private static GatewayGalaxyDataWriter NewWriter()
        => new(new GalaxyMxSession(new Config.GalaxyMxAccessOptions(ClientName: "OtOpcUa-Test")), writeUserId: 0);

    // ---------------- T5: seam ----------------

    /// <summary>A secured-write classification routes through WriteSecured and never advises (regression via the seam).</summary>
    [Fact]
    public async Task WriteOne_SecuredClassification_RoutesThroughWriteSecured_NoAdvise()
    {
        var ops = new FakeMxWriteOps();
        var writer = NewWriter();

        var result = await writer.WriteOneForTestAsync(
            ops, new WriteRequest("Tag.Sec", true), SecurityClassification.SecuredWrite, CancellationToken.None);

        result.StatusCode.ShouldBe(StatusCodeMap.Good);
        ops.Invokes.Count(r => r.Command.Kind == MxCommandKind.WriteSecured).ShouldBe(1);
        ops.Invokes.Any(r => r.Command.Kind == MxCommandKind.AdviseSupervisory).ShouldBeFalse();
        ops.RawWriteHandles.ShouldBeEmpty();
    }

    // ---------------- T6: fail-closed on advise failure ----------------

    /// <summary>When AdviseSupervisory fails (non-OK protocol status), the raw Write must NOT be issued and
    /// the writer returns Bad (which fires the #5 node revert). The handle must not linger in the supervised
    /// cache so the next write retries the advise.</summary>
    [Fact]
    public async Task WriteOne_AdviseFails_ReturnsBadCommunicationError_AndNeverIssuesRawWrite()
    {
        var ops = new FakeMxWriteOps
        {
            InvokeResponder = _ => new MxCommandReply
            {
                ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure, Message = "advise boom" },
            },
        };
        var writer = NewWriter();

        var result = await writer.WriteOneForTestAsync(
            ops, new WriteRequest("Tag.A", true), SecurityClassification.FreeAccess, CancellationToken.None);

        result.StatusCode.ShouldBe(StatusCodeMap.BadCommunicationError);
        ops.Invokes.Count(r => r.Command.Kind == MxCommandKind.AdviseSupervisory).ShouldBe(1);
        ops.RawWriteHandles.ShouldBeEmpty();               // the knowingly-lost raw write was NOT issued
        writer.CachedSupervisedHandleCount.ShouldBe(0);    // advise failure forgot the handle → next write retries
    }

    /// <summary>When advise succeeds, the raw Write proceeds and a clean reply returns Good (regression).</summary>
    [Fact]
    public async Task WriteOne_AdviseSucceeds_RawWriteProceeds()
    {
        var ops = new FakeMxWriteOps
        {
            InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } },
        };
        var writer = NewWriter();

        var result = await writer.WriteOneForTestAsync(
            ops, new WriteRequest("Tag.B", 42), SecurityClassification.FreeAccess, CancellationToken.None);

        result.StatusCode.ShouldBe(StatusCodeMap.Good);
        ops.Invokes.Count(r => r.Command.Kind == MxCommandKind.AdviseSupervisory).ShouldBe(1);
        ops.RawWriteHandles.ShouldHaveSingleItem();        // the raw write was issued after the successful advise
        writer.CachedSupervisedHandleCount.ShouldBe(1);
    }

    // ---------------- T7: counters ----------------

    /// <summary>A fail-closed advise increments galaxy.writes.advise_failed (and not unconfirmed).</summary>
    [Fact]
    public async Task WriteOne_AdviseFails_Increments_AdviseFailed_Meter()
    {
        using var advise = new MeterRecorder("galaxy.writes.advise_failed");
        using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
        var ops = new FakeMxWriteOps
        {
            InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure } },
        };
        var writer = NewWriter();

        await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.A", true), SecurityClassification.FreeAccess, CancellationToken.None);

        advise.Total.ShouldBe(1);
        unconfirmed.Total.ShouldBe(0);
    }

    /// <summary>An advised write whose reply carries an EMPTY statuses array returns Good but increments
    /// galaxy.writes.unconfirmed (the commit is unconfirmed pending gateway WriteComplete correlation).</summary>
    [Fact]
    public async Task WriteOne_EmptyStatuses_ReturnsGood_And_Increments_Unconfirmed_Meter()
    {
        using var advise = new MeterRecorder("galaxy.writes.advise_failed");
        using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
        var ops = new FakeMxWriteOps
        {
            InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } },
            RawWriteResponder = () => new MxCommandReply(),   // no ProtocolStatus, EMPTY statuses ⇒ Good
        };
        var writer = NewWriter();

        var result = await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.B", 1), SecurityClassification.FreeAccess, CancellationToken.None);

        result.StatusCode.ShouldBe(StatusCodeMap.Good);
        unconfirmed.Total.ShouldBe(1);
        advise.Total.ShouldBe(0);
    }

    /// <summary>A reply carrying an MX status ROW increments neither meter (not empty-statuses, advise OK).</summary>
    [Fact]
    public async Task WriteOne_StatusRowReply_Increments_Neither_Meter()
    {
        using var advise = new MeterRecorder("galaxy.writes.advise_failed");
        using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
        var okStatusReply = new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } };
        okStatusReply.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok });
        var ops = new FakeMxWriteOps
        {
            InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } },
            RawWriteResponder = () => okStatusReply,
        };
        var writer = NewWriter();

        var result = await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.C", 2), SecurityClassification.FreeAccess, CancellationToken.None);

        result.StatusCode.ShouldBe(StatusCodeMap.Good);   // Success/Ok status row ⇒ Good
        advise.Total.ShouldBe(0);
        unconfirmed.Total.ShouldBe(0);                     // NOT empty-statuses ⇒ not metered unconfirmed
    }

    // ---------------- fakes ----------------

    /// <summary>Listens to a single instrument by name on the Galaxy driver meter and sums the values.</summary>
    private sealed class MeterRecorder : IDisposable
    {
        private readonly string _name;
        private readonly System.Diagnostics.Metrics.MeterListener _listener;
        private long _total;
        private readonly object _gate = new();

        public MeterRecorder(string instrumentName)
        {
            _name = instrumentName;
            _listener = new System.Diagnostics.Metrics.MeterListener
            {
                InstrumentPublished = (instrument, listener) =>
                {
                    if (instrument.Meter.Name == EventPump.MeterName && instrument.Name == _name)
                        listener.EnableMeasurementEvents(instrument);
                }
            };
            _listener.SetMeasurementEventCallback<long>((_, value, _, _) => { lock (_gate) _total += value; });
            _listener.Start();
        }

        public long Total { get { lock (_gate) return _total; } }

        public void Dispose() => _listener.Dispose();
    }

    /// <summary>A fake <see cref="IMxWriteOps"/> that records every call and returns canned replies.</summary>
    private sealed class FakeMxWriteOps : IMxWriteOps
    {
        public string SessionId => "sess-1";
        public int ServerHandle => 100;
        public int NextHandle { get; init; } = 7;

        public List<string> AddItemRefs { get; } = new();
        public List<MxCommandRequest> Invokes { get; } = new();
        public List<int> RawWriteHandles { get; } = new();

        /// <summary>Responder for <see cref="InvokeAsync"/>; null ⇒ an OK (empty) reply.</summary>
        public Func<MxCommandRequest, MxCommandReply>? InvokeResponder { get; init; }
        /// <summary>Responder for <see cref="WriteRawAsync"/>; null ⇒ an empty (Good) reply.</summary>
        public Func<MxCommandReply>? RawWriteResponder { get; init; }

        public Task<int> AddItemAsync(string fullRef, CancellationToken ct)
        {
            AddItemRefs.Add(fullRef);
            return Task.FromResult(NextHandle);
        }

        public Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct)
        {
            Invokes.Add(request);
            return Task.FromResult(InvokeResponder?.Invoke(request) ?? new MxCommandReply());
        }

        public Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct)
        {
            RawWriteHandles.Add(itemHandle);
            return Task.FromResult(RawWriteResponder?.Invoke() ?? new MxCommandReply());
        }
    }
}
