using System.Runtime.CompilerServices;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

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

internal class FakeTwinCATClient : ITwinCATClient
{
    public bool IsConnected { get; private set; }
    public int ConnectCount { get; private set; }
    public int DisposeCount { get; private set; }
    public bool ThrowOnConnect { get; set; }
    public bool ThrowOnRead { get; set; }
    public bool ThrowOnWrite { get; set; }
    public bool ThrowOnProbe { get; set; }
    public Exception? Exception { get; set; }
    public Dictionary<string, object?> Values { get; } = new(StringComparer.OrdinalIgnoreCase);
    public Dictionary<string, uint> ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
    public Dictionary<string, uint> WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
    public List<(string symbol, TwinCATDataType type, int? bit, object? value)> WriteLog { get; } = new();
    public bool ProbeResult { get; set; } = true;

    public virtual Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken ct)
    {
        ConnectCount++;
        if (ThrowOnConnect) throw Exception ?? new InvalidOperationException();
        IsConnected = true;
        return Task.CompletedTask;
    }

    public virtual Task<(object? value, uint status)> ReadValueAsync(
        string symbolPath, TwinCATDataType type, int? bitIndex, CancellationToken ct)
    {
        if (ThrowOnRead) throw Exception ?? new InvalidOperationException();
        var status = ReadStatuses.TryGetValue(symbolPath, out var s) ? s : TwinCATStatusMapper.Good;
        var value = Values.TryGetValue(symbolPath, out var v) ? v : null;
        return Task.FromResult((value, status));
    }

    public virtual Task<uint> WriteValueAsync(
        string symbolPath, TwinCATDataType type, int? bitIndex, object? value, CancellationToken ct)
    {
        if (ThrowOnWrite) throw Exception ?? new InvalidOperationException();
        WriteLog.Add((symbolPath, type, bitIndex, value));
        Values[symbolPath] = value;
        var status = WriteStatuses.TryGetValue(symbolPath, out var s) ? s : TwinCATStatusMapper.Good;
        return Task.FromResult(status);
    }

    public virtual Task<bool> ProbeAsync(CancellationToken ct)
    {
        if (ThrowOnProbe) return Task.FromResult(false);
        return Task.FromResult(ProbeResult);
    }

    public virtual void Dispose()
    {
        DisposeCount++;
        IsConnected = false;
    }

    // ---- notification fake ----

    public List<FakeNotification> Notifications { get; } = new();
    public bool ThrowOnAddNotification { get; set; }

    public virtual Task<ITwinCATNotificationHandle> AddNotificationAsync(
        string symbolPath, TwinCATDataType type, int? bitIndex, TimeSpan cycleTime,
        Action<string, object?> onChange, CancellationToken cancellationToken)
    {
        if (ThrowOnAddNotification)
            throw Exception ?? new InvalidOperationException("fake AddNotification failure");

        var reg = new FakeNotification(symbolPath, type, bitIndex, onChange, this);
        Notifications.Add(reg);
        return Task.FromResult<ITwinCATNotificationHandle>(reg);
    }

    /// <summary>Fire a change event through the registered callback for <paramref name="symbolPath"/>.</summary>
    public void FireNotification(string symbolPath, object? value)
    {
        foreach (var n in Notifications)
            if (!n.Disposed && string.Equals(n.SymbolPath, symbolPath, StringComparison.OrdinalIgnoreCase))
                n.OnChange(symbolPath, value);
    }

    // ---- symbol browser fake ----

    public List<TwinCATDiscoveredSymbol> BrowseResults { get; } = new();
    public bool ThrowOnBrowse { get; set; }

    public virtual async IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        if (ThrowOnBrowse) throw Exception ?? new InvalidOperationException("fake browse failure");
        await Task.CompletedTask;
        foreach (var sym in BrowseResults)
        {
            if (cancellationToken.IsCancellationRequested) yield break;
            yield return sym;
        }
    }

    public sealed class FakeNotification(
        string symbolPath, TwinCATDataType type, int? bitIndex,
        Action<string, object?> onChange, FakeTwinCATClient owner) : ITwinCATNotificationHandle
    {
        public string SymbolPath { get; } = symbolPath;
        public TwinCATDataType Type { get; } = type;
        public int? BitIndex { get; } = bitIndex;
        public Action<string, object?> OnChange { get; } = onChange;
        public bool Disposed { get; private set; }

        public void Dispose()
        {
            Disposed = true;
            owner.Notifications.Remove(this);
        }
    }
}

internal sealed class FakeTwinCATClientFactory : ITwinCATClientFactory
{
    public List<FakeTwinCATClient> Clients { get; } = new();
    public Func<FakeTwinCATClient>? Customise { get; set; }

    public ITwinCATClient Create()
    {
        var client = Customise?.Invoke() ?? new FakeTwinCATClient();
        Clients.Add(client);
        return client;
    }
}
