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

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

/// <summary>
///     An <see cref="IAddressSpaceBuilder"/> that records the tree <c>DiscoverAsync</c> streams into
///     it, so discovery against a live Agent can be asserted on shape (which leaf landed under which
///     folder, with which driver-side metadata) rather than on a flat list.
/// </summary>
/// <remarks>
///     A near-twin of the MTConnect unit suite's <c>CapturingBuilder</c>, restated here rather than
///     shared: that type is <c>internal</c> to a different test assembly, and the two production
///     capturing builders (<c>Commons.Browsing</c> / <c>Runtime.Drivers</c>) would drag the server
///     stack into a driver integration project that deliberately references only the driver.
/// </remarks>
internal sealed class DiscoveryCapture : IAddressSpaceBuilder
{
    private readonly State _state;
    private readonly string _path;

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

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

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

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

    /// <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 DiscoveryCapture(_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 Handle(captured);
    }

    /// <inheritdoc/>
    public void AddProperty(string browseName, DriverDataType dataType, object? value)
    {
        // Recorded nowhere: the MTConnect driver streams no node properties, and a capture that
        // threw here would fail a driver that legitimately started to.
    }

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

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

    private sealed class Handle(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)
            {
                // No test here drives an alarm transition through discovery.
            }
        }
    }
}

/// <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.</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; } = [];
}
