using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;

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

/// <summary>
///     Unit tests for <see cref="MxAccessValueCache"/>. The cache is consumed by
///     <see cref="MxAccessSession.ReadBulk"/> to satisfy "current value"
///     requests for already-advised tags without touching the existing
///     subscription, so its contract is exercised in isolation here before any
///     STA / COM plumbing gets layered on top.
/// </summary>
public sealed class MxAccessValueCacheTests
{
    [Fact]
    public void Set_ThenTryGet_ReturnsLastValueWithIncrementingVersion()
    {
        MxAccessValueCache cache = new();
        Timestamp sourceTimestamp = Timestamp.FromDateTime(new(2026, 5, 19, 9, 0, 0, DateTimeKind.Utc));

        cache.Set(serverHandle: 7, itemHandle: 21, BuildEvent(serverHandle: 7, itemHandle: 21, intValue: 100, quality: 192, sourceTimestamp));

        Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue first));
        Assert.Equal(1UL, first.Version);
        Assert.Equal(100, first.Value.Int32Value);
        Assert.Equal(192, first.Quality);
        Assert.Equal(sourceTimestamp, first.SourceTimestamp);

        // A second Set on the same key bumps the version and overwrites the
        // payload. Different keys remain isolated.
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 200, quality: 192, sourceTimestamp));
        cache.Set(7, 22, BuildEvent(7, 22, intValue: 999, quality: 192, sourceTimestamp));

        Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue second));
        Assert.Equal(2UL, second.Version);
        Assert.Equal(200, second.Value.Int32Value);

        Assert.True(cache.TryGet(7, 22, out MxAccessValueCache.CachedValue other));
        Assert.Equal(1UL, other.Version);
        Assert.Equal(999, other.Value.Int32Value);
    }

    [Fact]
    public void TryGet_WithUnknownHandle_ReturnsFalse()
    {
        MxAccessValueCache cache = new();

        Assert.False(cache.TryGet(serverHandle: 7, itemHandle: 21, out _));
    }

    [Fact]
    public void Remove_DropsEntryAndResetsVersion()
    {
        MxAccessValueCache cache = new();
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 1, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 2, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));

        cache.Remove(7, 21);
        Assert.False(cache.TryGet(7, 21, out _));

        // After Remove, a subsequent Set restarts the per-handle version from 1
        // — the cache must not serve a stale "version 3" entry that would race
        // against a reused MXAccess item handle.
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 3, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
        Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue reset));
        Assert.Equal(1UL, reset.Version);
    }

    [Fact]
    public void CurrentVersion_ReturnsZeroForUnknown_AndLatestForKnown()
    {
        MxAccessValueCache cache = new();
        Assert.Equal(0UL, cache.CurrentVersion(7, 21));

        cache.Set(7, 21, BuildEvent(7, 21, intValue: 1, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 2, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));

        Assert.Equal(2UL, cache.CurrentVersion(7, 21));
    }

    /// <summary>
    ///     Worker.Tests-020: pins the contract that <c>TryWaitForUpdate</c>
    ///     returns <c>false</c> when the deadline has elapsed with no
    ///     <c>Set</c>, yields a default <c>CachedValue</c>, and invokes
    ///     <c>pumpStep</c> at least once so MXAccess Windows messages can
    ///     be dispatched. Earlier revisions of this test asserted both an
    ///     elapsed-time floor (<c>stopwatch.ElapsedMilliseconds &gt;= 60</c>)
    ///     and <c>pumpCalls &gt; 1</c> — the same wall-clock-floor race
    ///     pattern Worker.Tests-003/004/013 corrected. To eliminate the
    ///     timing dependency entirely (the equivalent of a manual time
    ///     source for a <c>DateTime.UtcNow</c>-based deadline), the test
    ///     now supplies a deadline already in the past: the loop pumps
    ///     once, observes the passed deadline, and returns false
    ///     deterministically without any <c>Thread.Sleep</c>. The
    ///     deadline-honouring contract is what this test exists to pin;
    ///     elapsed time and pump-iteration count are incidental.
    /// </summary>
    [Fact]
    public void TryWaitForUpdate_ReturnsFalseAfterDeadline_WhenNoSetOccurs()
    {
        MxAccessValueCache cache = new();
        int pumpCalls = 0;

        // Deadline already in the past — eliminates the wall-clock-floor
        // race. The loop must pump once (so MXAccess messages can dispatch
        // on the calling thread even when the deadline has just expired)
        // and then immediately observe the passed deadline.
        DateTime expiredDeadlineUtc = DateTime.UtcNow.AddMilliseconds(-1);

        bool result = cache.TryWaitForUpdate(
            serverHandle: 7,
            itemHandle: 21,
            sinceVersion: 0,
            deadlineUtc: expiredDeadlineUtc,
            pumpStep: () => Interlocked.Increment(ref pumpCalls),
            out MxAccessValueCache.CachedValue value,
            pollIntervalMs: 5);

        Assert.False(result);
        Assert.Equal(default, value.Value);
        Assert.Equal(1, pumpCalls);
    }

    [Fact]
    public async Task TryWaitForUpdate_ReturnsTrue_WhenSetFiresAfterBaselineVersion()
    {
        MxAccessValueCache cache = new();
        Timestamp sourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow);
        // Baseline is "no entry yet" → wait for the first Set to land.
        Task<(bool ok, MxAccessValueCache.CachedValue value)> waitTask = Task.Run(() =>
        {
            bool ok = cache.TryWaitForUpdate(
                serverHandle: 7,
                itemHandle: 21,
                sinceVersion: 0,
                deadlineUtc: DateTime.UtcNow.AddSeconds(2),
                pumpStep: () => { },
                out MxAccessValueCache.CachedValue v,
                pollIntervalMs: 5);
            return (ok, v);
        });

        // Race a Set against the wait loop. The cache's lock guarantees the
        // wait observes the new version before TryGet returns it.
        await Task.Delay(20);
        cache.Set(7, 21, BuildEvent(7, 21, intValue: 4242, quality: 192, sourceTimestamp));

        (bool ok, MxAccessValueCache.CachedValue value) = await waitTask;
        Assert.True(ok);
        Assert.Equal(4242, value.Value.Int32Value);
        Assert.Equal(1UL, value.Version);
    }

    private static MxEvent BuildEvent(
        int serverHandle,
        int itemHandle,
        int intValue,
        int quality,
        Timestamp sourceTimestamp)
    {
        MxEvent mxEvent = new()
        {
            Family = MxEventFamily.OnDataChange,
            ServerHandle = serverHandle,
            ItemHandle = itemHandle,
            Quality = quality,
            SourceTimestamp = sourceTimestamp,
            Value = new MxValue
            {
                DataType = MxDataType.Integer,
                VariantType = "VT_I4",
                Int32Value = intValue,
            },
            OnDataChange = new OnDataChangeEvent(),
        };
        mxEvent.Statuses.Add(new MxStatusProxy
        {
            Category = MxStatusCategory.Ok,
        });
        return mxEvent;
    }
}
