using System;
using System.Collections.Generic;
using System.Linq;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;

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

/// <summary>
///     Unit-test coverage for <see cref="LmxSubtagAlarmSource"/>'s advise/write
///     sequencing and its <c>OnDataChange</c> normalization. The actual
///     <c>LMXProxyServerClass</c> COM event subscription cannot be exercised
///     without a live MXAccess install, so these tests drive the source through
///     its internal <see cref="IMxAccessServer"/> seam and call
///     <c>HandleDataChange</c> directly to simulate a COM callback — exactly the
///     boundary <c>MxAccessBaseEventSink.OnDataChange</c> uses for the
///     per-session pipeline. End-to-end COM delivery is covered by the
///     Skip-gated alarm live smoke tests.
/// </summary>
public sealed class LmxSubtagAlarmSourceTests
{
    private const int FakeServerHandle = 7;

    /// <summary>Verifies the production constructor rejects a null factory.</summary>
    [Fact]
    public void Constructor_NullFactory_Throws()
    {
        Assert.Throws<ArgumentNullException>(() => new LmxSubtagAlarmSource(factory: null!));
    }

    /// <summary>
    ///     Verifies <see cref="LmxSubtagAlarmSource.Advise"/> calls AddItem then
    ///     Advise once per distinct address, and is idempotent on a repeated
    ///     address.
    /// </summary>
    [Fact]
    public void Advise_AddsAndAdvisesEachAddressOnce()
    {
        var server = new RecordingMxAccessServer();
        using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Advise(new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" });
        // Re-advising an already-advised address is a no-op.
        source.Advise(new[] { "Tank1.Alarm.Subtag" });

        Assert.Equal(
            new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" },
            server.AddedItems);
        Assert.Equal(2, server.AdviseCount);
        // Every advise targeted the supplied server handle.
        Assert.All(server.AdvisedServerHandles, h => Assert.Equal(FakeServerHandle, h));
    }

    /// <summary>
    ///     Verifies a simulated <c>OnDataChange</c> for an advised item handle
    ///     raises <see cref="LmxSubtagAlarmSource.ValueChanged"/> with the
    ///     address that was advised and the delivered value.
    /// </summary>
    [Fact]
    public void HandleDataChange_RaisesValueChangedWithAdvisedAddress()
    {
        var server = new RecordingMxAccessServer();
        using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Advise(new[] { "Tank1.Alarm.Subtag" });
        int itemHandle = server.LastItemHandleFor("Tank1.Alarm.Subtag");

        SubtagValueChange? received = null;
        source.ValueChanged += (_, change) => received = change;

        source.HandleDataChange(itemHandle, pvItemValue: 42, pftItemTimeStamp: null);

        Assert.NotNull(received);
        Assert.Equal("Tank1.Alarm.Subtag", received!.ItemAddress);
        Assert.Equal(42, received.Value);
        Assert.Equal(DateTimeKind.Utc, received.TimestampUtc.Kind);
    }

    /// <summary>
    ///     Verifies <c>OnDataChange</c> for an unknown item handle is ignored
    ///     (no <see cref="LmxSubtagAlarmSource.ValueChanged"/> raised).
    /// </summary>
    [Fact]
    public void HandleDataChange_UnknownHandle_DoesNotRaise()
    {
        var server = new RecordingMxAccessServer();
        using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        bool raised = false;
        source.ValueChanged += (_, _) => raised = true;

        source.HandleDataChange(phItemHandle: 999, pvItemValue: 1, pftItemTimeStamp: null);

        Assert.False(raised);
    }

    /// <summary>
    ///     Verifies <see cref="LmxSubtagAlarmSource.Write"/> adds the item when
    ///     it was not previously advised and writes with user id 0.
    /// </summary>
    [Fact]
    public void Write_AddsItemWhenUnknownAndWrites()
    {
        var server = new RecordingMxAccessServer();
        using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Write("Tank1.Alarm.AckComment", "acknowledged");

        Assert.Contains("Tank1.Alarm.AckComment", server.AddedItems);
        Assert.Single(server.Writes);
        RecordingMxAccessServer.WriteRecord write = server.Writes[0];
        Assert.Equal(FakeServerHandle, write.ServerHandle);
        Assert.Equal("acknowledged", write.Value);
        Assert.Equal(0, write.UserId);
    }

    /// <summary>
    ///     Verifies <see cref="LmxSubtagAlarmSource.Write"/> reuses an existing
    ///     item handle (no duplicate AddItem) when the address was already
    ///     advised.
    /// </summary>
    [Fact]
    public void Write_ReusesHandleForAdvisedAddress()
    {
        var server = new RecordingMxAccessServer();
        using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Advise(new[] { "Tank1.Alarm.Subtag" });
        int adviseAddCount = server.AddedItems.Count;

        source.Write("Tank1.Alarm.Subtag", true);

        // No second AddItem for the already-advised address.
        Assert.Equal(adviseAddCount, server.AddedItems.Count);
        Assert.Single(server.Writes);
    }

    /// <summary>
    ///     Verifies <see cref="LmxSubtagAlarmSource.Dispose"/> UnAdvises only the
    ///     handles that were actually advised — a write-only item (added by
    ///     <see cref="LmxSubtagAlarmSource.Write"/> but never advised) is removed
    ///     but not unadvised — and unregisters the server exactly once.
    /// </summary>
    [Fact]
    public void Dispose_UnAdvisesOnlyAdvisedHandles_RemovesAll_AndUnregistersOnce()
    {
        var server = new RecordingMxAccessServer();
        var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Advise(new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" });
        // A write-only subtag: added by Write, never advised.
        source.Write("Tank1.Alarm.AckComment", "acknowledged");

        int advised1 = server.LastItemHandleFor("Tank1.Alarm.Subtag");
        int advised2 = server.LastItemHandleFor("Tank2.Alarm.Subtag");
        int writeOnly = server.LastItemHandleFor("Tank1.Alarm.AckComment");

        source.Dispose();

        // Only the two advised handles are unadvised — never the write-only one.
        Assert.Equal(new[] { advised1, advised2 }, server.UnAdvisedItemHandles);
        Assert.DoesNotContain(writeOnly, server.UnAdvisedItemHandles);
        // Every added item (advised + write-only) is removed.
        Assert.Equal(
            new[] { advised1, advised2, writeOnly }.OrderBy(h => h),
            server.RemovedItemHandles.OrderBy(h => h));
        Assert.Equal(1, server.UnregisterCount);
    }

    /// <summary>
    ///     Verifies <see cref="LmxSubtagAlarmSource.Dispose"/> is idempotent: a
    ///     second call performs no further teardown.
    /// </summary>
    [Fact]
    public void Dispose_IsIdempotent()
    {
        var server = new RecordingMxAccessServer();
        var source = new LmxSubtagAlarmSource(server, FakeServerHandle);

        source.Advise(new[] { "Tank1.Alarm.Subtag" });

        source.Dispose();
        int unadviseAfterFirst = server.UnAdvisedItemHandles.Count;
        int unregisterAfterFirst = server.UnregisterCount;

        source.Dispose();

        Assert.Equal(unadviseAfterFirst, server.UnAdvisedItemHandles.Count);
        Assert.Equal(unregisterAfterFirst, server.UnregisterCount);
    }

    /// <summary>
    ///     Recording <see cref="IMxAccessServer"/> test double that captures the
    ///     AddItem/Advise/Write/UnAdvise/RemoveItem/Unregister calls
    ///     <see cref="LmxSubtagAlarmSource"/> makes and hands out monotonically
    ///     increasing item handles.
    /// </summary>
    private sealed class RecordingMxAccessServer : IMxAccessServer
    {
        private readonly Dictionary<string, int> handlesByAddress = new(StringComparer.Ordinal);
        private int nextItemHandle = 100;

        /// <summary>Item definitions passed to <see cref="AddItem"/> or <see cref="AddItem2"/>, in call order.</summary>
        public List<string> AddedItems { get; } = new();

        /// <summary>Number of times <see cref="Advise"/> has been called.</summary>
        public int AdviseCount { get; private set; }

        /// <summary>Server handles passed to <see cref="Advise"/>, in call order.</summary>
        public List<int> AdvisedServerHandles { get; } = new();

        /// <summary>Recorded <see cref="Write"/> calls, in call order.</summary>
        public List<WriteRecord> Writes { get; } = new();

        /// <summary>Item handles passed to <see cref="UnAdvise"/>, in call order.</summary>
        public List<int> UnAdvisedItemHandles { get; } = new();

        /// <summary>Item handles passed to <see cref="RemoveItem"/>, in call order.</summary>
        public List<int> RemovedItemHandles { get; } = new();

        /// <summary>Number of times <see cref="Unregister"/> has been called.</summary>
        public int UnregisterCount { get; private set; }

        /// <summary>Returns the item handle most recently assigned to the given item address.</summary>
        /// <param name="itemAddress">The item definition/address to look up.</param>
        /// <returns>The item handle assigned by <see cref="AddItem"/>.</returns>
        public int LastItemHandleFor(string itemAddress) => handlesByAddress[itemAddress];

        /// <inheritdoc />
        public int Register(string clientName) => FakeServerHandle;

        /// <inheritdoc />
        public void Unregister(int serverHandle) => UnregisterCount++;

        /// <inheritdoc />
        public int AddItem(int serverHandle, string itemDefinition)
        {
            AddedItems.Add(itemDefinition);
            int handle = nextItemHandle++;
            handlesByAddress[itemDefinition] = handle;
            return handle;
        }

        /// <inheritdoc />
        public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
            => AddItem(serverHandle, itemDefinition);

        /// <inheritdoc />
        public void RemoveItem(int serverHandle, int itemHandle) => RemovedItemHandles.Add(itemHandle);

        /// <inheritdoc />
        public void Advise(int serverHandle, int itemHandle)
        {
            AdviseCount++;
            AdvisedServerHandles.Add(serverHandle);
        }

        /// <inheritdoc />
        public void UnAdvise(int serverHandle, int itemHandle) => UnAdvisedItemHandles.Add(itemHandle);

        /// <inheritdoc />
        public void AdviseSupervisory(int serverHandle, int itemHandle)
        {
        }

        /// <inheritdoc />
        public void Write(int serverHandle, int itemHandle, object? value, int userId)
            => Writes.Add(new WriteRecord(serverHandle, itemHandle, value, userId));

        /// <inheritdoc />
        public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
        {
        }

        /// <inheritdoc />
        public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
        {
        }

        /// <inheritdoc />
        public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
        {
        }

        /// <inheritdoc />
        public object Suspend(int serverHandle, int itemHandle) => new object();

        /// <inheritdoc />
        public object Activate(int serverHandle, int itemHandle) => new object();

        /// <inheritdoc />
        public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;

        /// <inheritdoc />
        public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;

        /// <inheritdoc />
        public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
            => AddItem(serverHandle, itemDefinition);

        /// <inheritdoc />
        public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
        {
        }

        /// <summary>Immutable record of a single <see cref="Write"/> call captured by <see cref="Writes"/>.</summary>
        internal sealed class WriteRecord
        {
            /// <summary>Initializes a new instance of the <see cref="WriteRecord"/> class from a recorded write.</summary>
            /// <param name="serverHandle">The server handle the write targeted.</param>
            /// <param name="itemHandle">The item handle the write targeted.</param>
            /// <param name="value">The written value.</param>
            /// <param name="userId">The writing user's identifier.</param>
            public WriteRecord(int serverHandle, int itemHandle, object? value, int userId)
            {
                ServerHandle = serverHandle;
                ItemHandle = itemHandle;
                Value = value;
                UserId = userId;
            }

            /// <summary>The server handle the write targeted.</summary>
            public int ServerHandle { get; }

            /// <summary>The item handle the write targeted.</summary>
            public int ItemHandle { get; }

            /// <summary>The written value.</summary>
            public object? Value { get; }

            /// <summary>The writing user's identifier.</summary>
            public int UserId { get; }
        }
    }
}
