using System;
using ArchestrA.MxAccess;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
using ComMxDataType = ArchestrA.MxAccess.MxDataType;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;

/// <summary>
/// Integrated tests for <see cref="MxAccessBaseEventSink"/>: drive an MXAccess COM
/// event through the real sink → <see cref="MxAccessEventMapper"/> →
/// <see cref="MxAccessEventQueue"/> pipeline and assert a correctly-converted
/// protobuf <see cref="WorkerEvent"/> lands in the queue.
/// </summary>
/// <remarks>
/// Boundary: the COM-side <c>+=</c> subscription performed in
/// <see cref="MxAccessBaseEventSink.Attach"/> casts the supplied object to the
/// sealed <c>LMXProxyServerClass</c> RCW and cannot run without a live MXAccess COM
/// object, so <c>Attach</c>/<c>Detach</c> are not exercised here. The event
/// handlers themselves (<c>OnDataChange</c>, <c>OnWriteComplete</c>,
/// <c>OperationComplete</c>, <c>OnBufferedDataChange</c>) are the exact delegate
/// targets the COM runtime invokes; calling them directly reproduces an STA-thread
/// COM callback and exercises the genuine conversion + enqueue path. The
/// <c>sessionId</c> normally set by <c>Attach</c> defaults to empty here, which the
/// assertions account for. The COM-event-conversion fault branch is left to
/// <see cref="MxAccessEventMapperTests"/> and the queue's own fault tests.
/// </remarks>
public sealed class MxAccessBaseEventSinkTests
{
    /// <summary>
    /// Verifies that an OnDataChange COM callback converts to a protobuf event and lands in the queue.
    /// </summary>
    [Fact]
    public void OnDataChange_ComCallback_ConvertedEventLandsInQueue()
    {
        MxAccessEventQueue queue = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper());
        DateTime timestamp = new(2026, 5, 18, 9, 15, 0, DateTimeKind.Utc);
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OnDataChange(
            hLMXServerHandle: 7,
            phItemHandle: 21,
            pvItemValue: 1234,
            pwItemQuality: 192,
            pftItemTimeStamp: timestamp,
            ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.Equal(1UL, queue.LastEventSequence);
        Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
        Assert.NotNull(workerEvent);

        MxEvent mxEvent = workerEvent!.Event;
        Assert.Equal(MxEventFamily.OnDataChange, mxEvent.Family);
        Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, mxEvent.BodyCase);
        Assert.Equal(7, mxEvent.ServerHandle);
        Assert.Equal(21, mxEvent.ItemHandle);
        Assert.Equal(1234, mxEvent.Value.Int32Value);
        Assert.Equal(192, mxEvent.Quality);
        Assert.Equal(timestamp, mxEvent.SourceTimestamp.ToDateTime());
        Assert.Equal(1UL, mxEvent.WorkerSequence);
        Assert.NotNull(mxEvent.WorkerTimestamp);
    }

    /// <summary>
    /// Verifies that an OnDataChange COM callback also writes the value into the
    /// per-session value cache, so a later <c>ReadBulk</c> on an already-advised
    /// tag can serve the cached value without re-advising. The cache update must
    /// fire after the event has cleared the outbound queue — verified here by
    /// checking the cache only after the queue confirms the enqueue succeeded.
    /// </summary>
    [Fact]
    public void OnDataChange_ComCallback_PopulatesValueCache()
    {
        MxAccessEventQueue queue = new();
        MxAccessValueCache cache = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), cache);
        DateTime timestamp = new(2026, 5, 18, 9, 15, 0, DateTimeKind.Utc);
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OnDataChange(
            hLMXServerHandle: 7,
            phItemHandle: 21,
            pvItemValue: 1234,
            pwItemQuality: 192,
            pftItemTimeStamp: timestamp,
            ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue cached));
        Assert.Equal(1UL, cached.Version);
        Assert.Equal(1234, cached.Value.Int32Value);
        Assert.Equal(192, cached.Quality);
        Assert.Equal(timestamp, cached.SourceTimestamp.ToDateTime());
    }

    /// <summary>
    /// Verifies that the sink-bound <c>ValueCache</c> is exposed for sharing with
    /// the owning <see cref="MxAccessSession"/> so writes and reads see the same
    /// instance.
    /// </summary>
    [Fact]
    public void ValueCache_ReturnsTheInstanceBoundAtConstruction()
    {
        MxAccessEventQueue queue = new();
        MxAccessValueCache cache = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), cache);

        Assert.Same(cache, sink.ValueCache);
    }

    /// <summary>
    /// Verifies that an OnWriteComplete COM callback records the completion into
    /// the per-session write-completion cache for the executor's bounded reply
    /// wait AND still enqueues the event unchanged for the outbound stream —
    /// correlation observes the event, it never consumes it. The cache update
    /// fires only after the event has cleared the queue (post-publish rule).
    /// </summary>
    [Fact]
    public void OnWriteComplete_ComCallback_RecordsCompletionAndStillEnqueuesEvent()
    {
        MxAccessEventQueue queue = new();
        MxAccessWriteCompletionCache completionCache = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), new MxAccessValueCache(), completionCache);
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OnWriteComplete(hLMXServerHandle: 7, phItemHandle: 21, ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
        MxEvent mxEvent = workerEvent!.Event;
        Assert.Equal(MxEventFamily.OnWriteComplete, mxEvent.Family);
        Assert.Equal(7, mxEvent.ServerHandle);
        Assert.Equal(21, mxEvent.ItemHandle);
        Assert.Equal(1UL, completionCache.CurrentVersion(7, 21));
    }

    /// <summary>
    /// Verifies that the sink-bound write-completion cache is exposed for sharing
    /// with the owning <see cref="MxAccessSession"/> so the sink's recordings and
    /// the write executor's waits see the same instance.
    /// </summary>
    [Fact]
    public void WriteCompletionCache_ReturnsTheInstanceBoundAtConstruction()
    {
        MxAccessEventQueue queue = new();
        MxAccessWriteCompletionCache completionCache = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), new MxAccessValueCache(), completionCache);

        Assert.Same(completionCache, sink.WriteCompletionCache);
    }

    /// <summary>
    /// Verifies that consecutive OnDataChange callbacks land in the queue with monotonic sequences.
    /// </summary>
    [Fact]
    public void OnDataChange_MultipleComCallbacks_QueueAssignsMonotonicSequences()
    {
        MxAccessEventQueue queue = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper());
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OnDataChange(1, 10, 100, 192, DateTime.UtcNow, ref statuses);
        sink.OnDataChange(1, 11, 200, 192, DateTime.UtcNow, ref statuses);
        sink.OnDataChange(1, 12, 300, 192, DateTime.UtcNow, ref statuses);

        Assert.Equal(3, queue.Count);
        Assert.Equal(3UL, queue.LastEventSequence);

        Assert.True(queue.TryDequeue(out WorkerEvent? first));
        Assert.True(queue.TryDequeue(out WorkerEvent? second));
        Assert.True(queue.TryDequeue(out WorkerEvent? third));
        Assert.Equal(1UL, first!.Event.WorkerSequence);
        Assert.Equal(2UL, second!.Event.WorkerSequence);
        Assert.Equal(3UL, third!.Event.WorkerSequence);
        Assert.Equal(10, first.Event.ItemHandle);
        Assert.Equal(12, third.Event.ItemHandle);
    }

    /// <summary>
    /// Verifies that an OnWriteComplete COM callback lands in the queue with the correct family.
    /// </summary>
    [Fact]
    public void OnWriteComplete_ComCallback_ConvertedEventLandsInQueue()
    {
        MxAccessEventQueue queue = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper());
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OnWriteComplete(hLMXServerHandle: 3, phItemHandle: 9, ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
        MxEvent mxEvent = workerEvent!.Event;
        Assert.Equal(MxEventFamily.OnWriteComplete, mxEvent.Family);
        Assert.Equal(MxEvent.BodyOneofCase.OnWriteComplete, mxEvent.BodyCase);
        Assert.Equal(3, mxEvent.ServerHandle);
        Assert.Equal(9, mxEvent.ItemHandle);
        Assert.Equal(1UL, mxEvent.WorkerSequence);
    }

    /// <summary>
    /// Verifies that an OperationComplete COM callback lands in the queue with the correct family.
    /// </summary>
    [Fact]
    public void OperationComplete_ComCallback_ConvertedEventLandsInQueue()
    {
        MxAccessEventQueue queue = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper());
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        sink.OperationComplete(hLMXServerHandle: 4, phItemHandle: 8, ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
        MxEvent mxEvent = workerEvent!.Event;
        Assert.Equal(MxEventFamily.OperationComplete, mxEvent.Family);
        Assert.Equal(MxEvent.BodyOneofCase.OperationComplete, mxEvent.BodyCase);
        Assert.Equal(4, mxEvent.ServerHandle);
        Assert.Equal(8, mxEvent.ItemHandle);
    }

    /// <summary>
    /// Verifies that an OnBufferedDataChange COM callback converts the value and lands in the queue.
    /// </summary>
    [Fact]
    public void OnBufferedDataChange_ComCallback_ConvertedEventLandsInQueue()
    {
        MxAccessEventQueue queue = new();
        MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper());
        MXSTATUS_PROXY[] statuses = Array.Empty<MXSTATUS_PROXY>();

        // Raw MXAccess data-type code 2 == Integer (see MxAccessEventMapper.MapMxDataType).
        const int integerDataTypeCode = 2;

        sink.OnBufferedDataChange(
            hLMXServerHandle: 5,
            phItemHandle: 13,
            dtDataType: (ComMxDataType)integerDataTypeCode,
            pvItemValue: 77,
            pwItemQuality: 192,
            pftItemTimeStamp: DateTime.UtcNow,
            ref statuses);

        Assert.Equal(1, queue.Count);
        Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
        MxEvent mxEvent = workerEvent!.Event;
        Assert.Equal(MxEventFamily.OnBufferedDataChange, mxEvent.Family);
        Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, mxEvent.BodyCase);
        Assert.Equal(5, mxEvent.ServerHandle);
        Assert.Equal(13, mxEvent.ItemHandle);
        Assert.Equal(integerDataTypeCode, mxEvent.OnBufferedDataChange.RawDataType);
    }
}
