using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     An <see cref="IAddressSpaceBuilder"/> that records the tree a driver streams into it, so
///     <c>DiscoverAsync</c> can be asserted on <b>shape</b> (which node landed under which folder)
///     and not merely on a flat list of leaves.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why this exists rather than a reference to the shipped capturing builder.</b> Two
///         production capturing builders exist — <c>Commons.Browsing.CapturingAddressSpaceBuilder</c>
///         (universal browser) and <c>Runtime.Drivers.CapturingAddressSpaceBuilder</c> (deploy-time
///         discovery) — but this suite deliberately references only the driver and its
///         <c>.Contracts</c>, so neither is reachable without pulling the whole server stack into a
///         driver unit-test project. Both also flatten differently from what these tests must prove:
///         the browser's node id for a folder is a synthetic path while a leaf's id is its
///         <c>FullName</c>, which is exactly the conflation a "did the device-level data item land in
///         the device folder?" assertion needs to see through.
///     </para>
///     <para>
///         <b>Deliberately not thread-safe.</b> A driver streams discovery on one caller; recording
///         behind a lock would hide a driver that fanned out onto other threads.
///     </para>
/// </remarks>
internal sealed class CapturingBuilder : IAddressSpaceBuilder
{
    private readonly State _state;
    private readonly string _path;

    /// <summary>Creates a root builder — the scope a driver's <c>DiscoverAsync</c> is handed.</summary>
    public CapturingBuilder()
    {
        _state = new State();
        _path = string.Empty;
    }

    private CapturingBuilder(State state, string path)
    {
        _state = state;
        _path = path;
    }

    /// <summary>Every folder streamed, in call order, with the slash-joined path it landed at.</summary>
    public IReadOnlyList<CapturedFolder> Folders => _state.Folders;

    /// <summary>Every variable streamed, in call order, with the folder path it landed under.</summary>
    public IReadOnlyList<CapturedVariable> Variables => _state.Variables;

    /// <summary>Every property streamed, with the scope path it was added to.</summary>
    public IReadOnlyList<CapturedProperty> Properties => _state.Properties;

    /// <inheritdoc/>
    public IAddressSpaceBuilder Folder(string browseName, string displayName)
    {
        var path = _path.Length == 0 ? browseName : $"{_path}/{browseName}";
        _state.Folders.Add(new CapturedFolder(path, _path, browseName, displayName));

        return new CapturingBuilder(_state, path);
    }

    /// <inheritdoc/>
    public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
    {
        var captured = new CapturedVariable(_path, browseName, displayName, attributeInfo);
        _state.Variables.Add(captured);

        return new CapturedVariableHandle(captured);
    }

    /// <inheritdoc/>
    public void AddProperty(string browseName, DriverDataType dataType, object? value) =>
        _state.Properties.Add(new CapturedProperty(_path, browseName, dataType, value));

    private sealed class State
    {
        public List<CapturedFolder> Folders { get; } = [];

        public List<CapturedVariable> Variables { get; } = [];

        public List<CapturedProperty> Properties { get; } = [];
    }

    private sealed class CapturedVariableHandle(CapturedVariable variable) : IVariableHandle
    {
        public string FullReference => variable.Attr.FullName;

        public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
        {
            variable.AlarmConditions.Add(info);

            return new NullSink();
        }

        private sealed class NullSink : IAlarmConditionSink
        {
            public void OnTransition(AlarmEventArgs args)
            {
                // Nothing to record: no test in this suite drives an alarm transition through
                // discovery, and a sink that threw would fail a driver that legitimately never
                // pushes one.
            }
        }
    }
}

/// <summary>One folder captured from a discovery stream.</summary>
/// <param name="Path">Slash-joined path of the folder itself.</param>
/// <param name="ParentPath">Slash-joined path of the scope it was added to (empty at the root).</param>
/// <param name="BrowseName">The browse name the driver supplied.</param>
/// <param name="DisplayName">The display name the driver supplied.</param>
internal sealed record CapturedFolder(string Path, string ParentPath, string BrowseName, string DisplayName);

/// <summary>One variable captured from a discovery stream.</summary>
/// <param name="ParentPath">Slash-joined path of the folder it landed under (empty at the root).</param>
/// <param name="BrowseName">The browse name the driver supplied.</param>
/// <param name="DisplayName">The display name the driver supplied.</param>
/// <param name="Attr">The driver-side attribute metadata the driver stamped on it.</param>
internal sealed record CapturedVariable(
    string ParentPath, string BrowseName, string DisplayName, DriverAttributeInfo Attr)
{
    /// <summary>Alarm conditions the driver marked this variable with, if any.</summary>
    public List<AlarmConditionInfo> AlarmConditions { get; } = [];
}

/// <summary>One property captured from a discovery stream.</summary>
/// <param name="ScopePath">Slash-joined path of the node it was added to.</param>
/// <param name="BrowseName">The property browse name.</param>
/// <param name="DataType">The property data type.</param>
/// <param name="Value">The property value.</param>
internal sealed record CapturedProperty(string ScopePath, string BrowseName, DriverDataType DataType, object? Value);
