using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

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

[Trait("Category", "Unit")]
public sealed class TwinCATSymbolBrowserTests
{
    /// <summary>Verifies that discovery without EnableControllerBrowse only emits predeclared tags.</summary>
    [Fact]
    public async Task Discovery_without_EnableControllerBrowse_emits_only_predeclared()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Hidden", TwinCATDataType.DInt, false));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = false,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Variables.Select(v => v.BrowseName).ShouldBe(["Declared"]);
        builder.Folders.ShouldNotContain(f => f.BrowseName == "Discovered");
    }

    /// <summary>Verifies that discovery with browse enabled adds controller symbols under Discovered folder.</summary>
    [Fact]
    public async Task Discovery_with_browse_enabled_adds_controller_symbols_under_Discovered_folder()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Counter", TwinCATDataType.DInt, ReadOnly: false));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("GVL.Setpoint", TwinCATDataType.Real, ReadOnly: false));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Folders.ShouldContain(f => f.BrowseName == "Discovered");
        builder.Variables.Select(v => v.Info.FullName).ShouldContain("MAIN.Counter");
        builder.Variables.Select(v => v.Info.FullName).ShouldContain("GVL.Setpoint");
    }

    /// <summary>Verifies that browse filters out system symbols correctly.</summary>
    [Fact]
    public async Task Browse_filters_system_symbols()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("TwinCAT_SystemInfoVarList._AppInfo", TwinCATDataType.DInt, false));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("Constants.PI", TwinCATDataType.LReal, true));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("Mc_InternalState", TwinCATDataType.DInt, true));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("__CompilerGen", TwinCATDataType.DInt, true));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Real", TwinCATDataType.DInt, false));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Variables.Select(v => v.Info.FullName).ShouldBe(["MAIN.Real"]);
    }

    /// <summary>Verifies that browse skips symbols with null datatype.</summary>
    [Fact]
    public async Task Browse_skips_symbols_with_null_datatype()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Struct", DataType: null, ReadOnly: false));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Counter", TwinCATDataType.DInt, false));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Variables.Select(v => v.Info.FullName).ShouldBe(["MAIN.Counter"]);
    }

    /// <summary>Verifies that read-only symbols surface as ViewOnly.</summary>
    [Fact]
    public async Task ReadOnly_symbol_surfaces_ViewOnly()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Status", TwinCATDataType.DInt, ReadOnly: true));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Variables.Single().Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
    }

    /// <summary>Verifies that browse failure is non-fatal and predeclared tags still emit.</summary>
    [Fact]
    public async Task Browse_failure_is_non_fatal_predeclared_still_emits()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient { ThrowOnBrowse = true },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        builder.Variables.Select(v => v.BrowseName).ShouldContain("Declared");
    }

    /// <summary>Verifies that system symbol filter matches expected patterns.</summary>
    /// <param name="path">The symbol path to test.</param>
    /// <param name="expected">Whether the path is expected to be a system symbol.</param>
    [Theory]
    [InlineData("TwinCAT_SystemInfoVarList._AppInfo", true)]
    [InlineData("TwinCAT_RuntimeInfo.Something", true)]
    [InlineData("Constants.PI", true)]
    [InlineData("Mc_AxisState", true)]
    [InlineData("__hidden", true)]
    [InlineData("Global_Version", true)]
    [InlineData("MAIN.UserVar", false)]
    [InlineData("GVL.Counter", false)]
    [InlineData("MyFbInstance.State", false)]
    [InlineData("", true)]
    [InlineData("   ", true)]
    public void SystemSymbolFilter_matches_expected_patterns(string path, bool expected)
    {
        TwinCATSystemSymbolFilter.IsSystemSymbol(path).ShouldBe(expected);
    }

    // ---- helpers ----

    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        /// <summary>
        /// Gets the list of folders that were added to the address space.
        /// </summary>
        public List<(string BrowseName, string DisplayName)> Folders { get; } = new();

        /// <summary>
        /// Gets the list of variables that were added to the address space.
        /// </summary>
        public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();

        /// <summary>
        /// Adds a folder to the address space builder.
        /// </summary>
        /// <param name="browseName">The browse name of the folder.</param>
        /// <param name="displayName">The display name of the folder.</param>
        /// <returns>This builder for method chaining.</returns>
        public IAddressSpaceBuilder Folder(string browseName, string displayName)
        { Folders.Add((browseName, displayName)); return this; }

        /// <summary>
        /// Adds a variable to the address space builder.
        /// </summary>
        /// <param name="browseName">The browse name of the variable.</param>
        /// <param name="displayName">The display name of the variable.</param>
        /// <param name="info">The driver attribute information for the variable.</param>
        /// <returns>A variable handle for further configuration.</returns>
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
        { Variables.Add((browseName, info)); return new Handle(info.FullName); }

        /// <summary>
        /// Adds a property to the address space builder.
        /// </summary>
        /// <param name="_">The property name (unused).</param>
        /// <param name="__">The property data type (unused).</param>
        /// <param name="___">The property value (unused).</param>
        public void AddProperty(string _, DriverDataType __, object? ___) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            /// <summary>
            /// Gets the full reference of the variable.
            /// </summary>
            public string FullReference => fullRef;

            /// <summary>
            /// Marks the variable as an alarm condition.
            /// </summary>
            /// <param name="info">The alarm condition information.</param>
            /// <returns>An alarm condition sink for event transitions.</returns>
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }

        /// <summary>
        /// A null implementation of IAlarmConditionSink for test purposes.
        /// </summary>
        private sealed class NullSink : IAlarmConditionSink
        {
            /// <summary>
            /// Handles alarm transitions (no-op for tests).
            /// </summary>
            /// <param name="args">The alarm event arguments.</param>
            public void OnTransition(AlarmEventArgs args) { }
        }
    }
}
