using System.Collections.Concurrent;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests;

/// <summary>
///     In-memory <see cref="ITagUpstreamSource"/> for tests. Seed tag values via
///     <see cref="Set"/>, push changes via <see cref="Push"/>. Tracks subscriptions so
///     tests can assert the engine disposes them on reload / shutdown.
/// </summary>
public sealed class FakeUpstream : ITagUpstreamSource
{
    private readonly ConcurrentDictionary<string, DataValueSnapshot> _values = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, List<Action<string, DataValueSnapshot>>> _subs = new(StringComparer.Ordinal);

    /// <summary>Gets the number of active subscriptions.</summary>
    public int ActiveSubscriptionCount { get; private set; }

    /// <summary>Sets a tag value without notifying subscribers.</summary>
    /// <param name="path">The path to the tag.</param>
    /// <param name="value">The value to set.</param>
    /// <param name="statusCode">The OPC UA status code for the value.</param>
    public void Set(string path, object value, uint statusCode = 0u)
    {
        var now = DateTime.UtcNow;
        _values[path] = new DataValueSnapshot(value, statusCode, now, now);
    }

    /// <summary>Sets a tag value and notifies all subscribers.</summary>
    /// <param name="path">The path to the tag.</param>
    /// <param name="value">The value to set.</param>
    /// <param name="statusCode">The OPC UA status code for the value.</param>
    public void Push(string path, object value, uint statusCode = 0u)
    {
        Set(path, value, statusCode);
        if (_subs.TryGetValue(path, out var list))
        {
            Action<string, DataValueSnapshot>[] snap;
            lock (list) { snap = list.ToArray(); }
            foreach (var obs in snap) obs(path, _values[path]);
        }
    }

    /// <summary>Reads the current value of a tag.</summary>
    /// <param name="path">The path to the tag.</param>
    public DataValueSnapshot ReadTag(string path)
        => _values.TryGetValue(path, out var v)
            ? v
            : new DataValueSnapshot(null, 0x80340000u, null, DateTime.UtcNow);

    /// <summary>Subscribes to tag value changes.</summary>
    /// <param name="path">The path to the tag.</param>
    /// <param name="observer">The callback to invoke when the tag value changes.</param>
    public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
    {
        var list = _subs.GetOrAdd(path, _ => []);
        lock (list) { list.Add(observer); }
        ActiveSubscriptionCount++;
        return new Unsub(this, path, observer);
    }

    private sealed class Unsub : IDisposable
    {
        private readonly FakeUpstream _up;
        private readonly string _path;
        private readonly Action<string, DataValueSnapshot> _observer;

        /// <summary>Initializes a new subscription unsubscription handler.</summary>
        /// <param name="up">The parent FakeUpstream instance.</param>
        /// <param name="path">The tag path.</param>
        /// <param name="observer">The observer callback to unsubscribe.</param>
        public Unsub(FakeUpstream up, string path, Action<string, DataValueSnapshot> observer)
        {
            _up = up; _path = path; _observer = observer;
        }

        /// <summary>Unsubscribes from tag value changes.</summary>
        public void Dispose()
        {
            if (_up._subs.TryGetValue(_path, out var list))
            {
                lock (list)
                {
                    if (list.Remove(_observer))
                        _up.ActiveSubscriptionCount--;
                }
            }
        }
    }
}
