using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;

/// <summary>
///     Regression coverage for 05/STAB-5 — the four per-device fixed-tree caches
///     (<c>LastFixedSnapshots</c>, <c>LastTimers</c>, <c>LastServoLoads</c>, <c>LastSpindleLoads</c>)
///     are written by the background fixed-tree loop while <c>ReadAsync</c> callers read them
///     concurrently. A plain <see cref="System.Collections.Generic.Dictionary{TKey,TValue}"/>
///     throws / corrupts when a resize races a read — undefined behaviour. They must be
///     <see cref="System.Collections.Concurrent.ConcurrentDictionary{TKey,TValue}"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasFixedTreeConcurrencyTests
{
    private static FocasDriver.DeviceState NewDeviceState() =>
        new(new FocasHostAddress("10.0.0.5", FocasHostAddress.DefaultPort),
            new FocasDeviceOptions("focas://10.0.0.5"));

    /// <summary>
    ///     Hammers concurrent writers (which force dictionary resizes) against concurrent readers on
    ///     all four caches. A plain <c>Dictionary</c> is not concurrency-safe: its structural-mutation
    ///     version check throws "operations that change non-concurrent collections must have exclusive
    ///     access to the collection", and its lookup collision guard throws "concurrent operations
    ///     are not supported" — deterministically under this load. A
    ///     <c>ConcurrentDictionary</c> never throws.
    /// </summary>
    /// <remarks>
    ///     Uses several writers, not one, so the fault is deterministic rather than timing-dependent
    ///     UB: the fixed-tree caches are shared mutable state with zero synchronization, so ANY
    ///     concurrent access is the defect — a driver that survives this survives the real
    ///     writer-loop-vs-reader-callers race the finding cites.
    /// </remarks>
    [Fact]
    public async Task Concurrent_access_to_fixed_tree_caches_never_throws()
    {
        var state = NewDeviceState();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var faults = new System.Collections.Concurrent.ConcurrentQueue<Exception>();
        const int rounds = 40;
        const int growth = 4_000;

        void Writer(int seed)
        {
            try
            {
                for (var round = 0; round < rounds && !cts.IsCancellationRequested; round++)
                {
                    state.LastFixedSnapshots.Clear();
                    state.LastServoLoads.Clear();
                    state.LastSpindleLoads.Clear();
                    for (var i = 0; i < growth; i++)
                    {
                        // Distinct keys force repeated dictionary growth (resize = the race window);
                        // the Clear() each round keeps the caches churning through resizes.
                        state.LastFixedSnapshots[$"Axes/A{seed}_{i}/AbsolutePosition"] = i;
                        state.LastServoLoads[$"Servo{seed}_{i}"] = i;
                        state.LastSpindleLoads[seed * growth + i] = i;
                        state.LastTimers[(FocasTimerKind)(i % 4)] = new FocasTimer((FocasTimerKind)(i % 4), i, i);
                    }
                }
            }
            catch (Exception ex) { faults.Enqueue(ex); }
        }

        void Reader()
        {
            try
            {
                while (!cts.IsCancellationRequested && faults.IsEmpty)
                {
                    for (var i = 0; i < growth; i++)
                    {
                        state.LastFixedSnapshots.TryGetValue($"Axes/A0_{i}/AbsolutePosition", out _);
                        state.LastServoLoads.TryGetValue($"Servo0_{i}", out _);
                        state.LastSpindleLoads.TryGetValue(i, out _);
                        state.LastTimers.TryGetValue((FocasTimerKind)(i % 4), out _);
                    }
                }
            }
            catch (Exception ex) { faults.Enqueue(ex); }
        }

        var tasks = new List<Task>
        {
            Task.Run(() => Writer(0)),
            Task.Run(() => Writer(1)),
            Task.Run(() => Writer(2)),
            Task.Run(Reader),
            Task.Run(Reader),
        };
        await Task.WhenAll(tasks);

        faults.ShouldBeEmpty(
            faults.TryPeek(out var first) ? $"concurrent cache access threw: {first}" : "");
    }
}
