using System.Runtime.CompilerServices;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

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

[Trait("Category", "Unit")]
public sealed class AbCipDriverDiscoveryTests
{
    [Fact]
    public async Task PreDeclared_tags_emit_as_variables_under_device_folder()
    {
        var builder = new RecordingBuilder();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", DeviceName: "Line1-PLC")],
            Tags =
            [
                new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt),
                new AbCipTagDefinition("Temperature", "ab://10.0.0.5/1,0", "T", AbCipDataType.Real, Writable: false),
            ],
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        builder.Folders.ShouldContain(f => f.BrowseName == "AbCip");
        builder.Folders.ShouldContain(f => f.BrowseName == "ab://10.0.0.5/1,0" && f.DisplayName == "Line1-PLC");
        builder.Variables.Count.ShouldBe(2);
        builder.Variables.Single(v => v.BrowseName == "Speed").Info.SecurityClass.ShouldBe(SecurityClassification.Operate);
        builder.Variables.Single(v => v.BrowseName == "Temperature").Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
    }

    [Fact]
    public async Task Device_folder_displayname_falls_back_to_host_address()
    {
        var builder = new RecordingBuilder();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], // no DeviceName
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        builder.Folders.ShouldContain(f => f.BrowseName == "ab://10.0.0.5/1,0"
            && f.DisplayName == "ab://10.0.0.5/1,0");
    }

    [Fact]
    public async Task PreDeclared_system_tags_are_filtered_out()
    {
        var builder = new RecordingBuilder();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            Tags =
            [
                new AbCipTagDefinition("__DEFVAL_X", "ab://10.0.0.5/1,0", "__DEFVAL_X", AbCipDataType.DInt),
                new AbCipTagDefinition("Routine:SomeRoutine", "ab://10.0.0.5/1,0", "R", AbCipDataType.DInt),
                new AbCipTagDefinition("UserTag", "ab://10.0.0.5/1,0", "U", AbCipDataType.DInt),
            ],
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        builder.Variables.Select(v => v.BrowseName).ShouldBe(["UserTag"]);
    }

    [Fact]
    public async Task Tags_for_mismatched_device_are_ignored()
    {
        var builder = new RecordingBuilder();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            Tags = [new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "O", AbCipDataType.DInt)],
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        builder.Variables.ShouldBeEmpty();
    }

    [Fact]
    public async Task Controller_enumeration_adds_tags_under_Discovered_folder()
    {
        var builder = new RecordingBuilder();
        var enumeratorFactory = new FakeEnumeratorFactory(
            new AbCipDiscoveredTag("Pressure", null, AbCipDataType.Real, ReadOnly: false),
            new AbCipDiscoveredTag("StepIndex", ProgramScope: "MainProgram", AbCipDataType.DInt, ReadOnly: false));

        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            EnableControllerBrowse = true,
        }, "drv-1", enumeratorFactory: enumeratorFactory);
        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("Pressure");
        builder.Variables.Select(v => v.Info.FullName).ShouldContain("Program:MainProgram.StepIndex");
    }

    [Fact]
    public async Task Controller_enumeration_honours_system_tag_hint_and_filter()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeEnumeratorFactory(
            new AbCipDiscoveredTag("HiddenByHint", null, AbCipDataType.DInt, ReadOnly: false, IsSystemTag: true),
            new AbCipDiscoveredTag("Routine:Foo", null, AbCipDataType.DInt, ReadOnly: false, IsSystemTag: false),
            new AbCipDiscoveredTag("KeepMe", null, AbCipDataType.DInt, ReadOnly: false));

        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            EnableControllerBrowse = true,
        }, "drv-1", enumeratorFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

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

    [Fact]
    public async Task Controller_enumeration_ReadOnly_surfaces_ViewOnly_classification()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeEnumeratorFactory(
            new AbCipDiscoveredTag("SafetyTag", null, AbCipDataType.DInt, ReadOnly: true));

        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            EnableControllerBrowse = true,
        }, "drv-1", enumeratorFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

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

    [Fact]
    public async Task Controller_enumeration_receives_correct_device_params()
    {
        var factory = new FakeEnumeratorFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5:44818/1,2,3", AbCipPlcFamily.ControlLogix)],
            Timeout = TimeSpan.FromSeconds(7),
            EnableControllerBrowse = true,
        }, "drv-1", enumeratorFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(new RecordingBuilder(), CancellationToken.None);

        var capturedParams = factory.LastDeviceParams.ShouldNotBeNull();
        capturedParams.Gateway.ShouldBe("10.0.0.5");
        capturedParams.Port.ShouldBe(44818);
        capturedParams.CipPath.ShouldBe("1,2,3");
        capturedParams.LibplctagPlcAttribute.ShouldBe("controllogix");
        capturedParams.TagName.ShouldBe("@tags");
        capturedParams.Timeout.ShouldBe(TimeSpan.FromSeconds(7));
    }

    [Fact]
    public void Default_enumerator_factory_is_used_when_not_injected()
    {
        // Sanity — absent enumerator factory does not crash discovery + uses EmptyAbCipTagEnumerator
        // (covered by the other tests which instantiate without injecting a factory).
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1");
        drv.ShouldNotBeNull();
    }

    [Theory]
    [InlineData("__DEFVAL_X", true)]
    [InlineData("__DEFAULT_Y", true)]
    [InlineData("Routine:Main", true)]
    [InlineData("Task:MainTask", true)]
    [InlineData("Local:1:I", true)]
    [InlineData("Map:Alias", true)]
    [InlineData("Axis:MoveX", true)]
    [InlineData("Cam:Profile1", true)]
    [InlineData("MotionGroup:MG0", true)]
    [InlineData("Motor1", false)]
    [InlineData("Program:Main.Step", false)]
    [InlineData("Recipe_2", false)]
    [InlineData("", true)]
    [InlineData("   ", true)]
    public void SystemTagFilter_rejects_infrastructure_names(string name, bool expected)
    {
        AbCipSystemTagFilter.IsSystemTag(name).ShouldBe(expected);
    }

    [Fact]
    public void TemplateCache_roundtrip_put_get()
    {
        var cache = new AbCipTemplateCache();
        var shape = new AbCipUdtShape("MyUdt", 32,
        [
            new AbCipUdtMember("A", 0, AbCipDataType.DInt, ArrayLength: 1),
            new AbCipUdtMember("B", 4, AbCipDataType.Real, ArrayLength: 1),
        ]);
        cache.Put("ab://10.0.0.5/1,0", 42, shape);

        cache.TryGet("ab://10.0.0.5/1,0", 42).ShouldBe(shape);
        cache.TryGet("ab://10.0.0.5/1,0", 99).ShouldBeNull();
        cache.Count.ShouldBe(1);

        cache.Clear();
        cache.Count.ShouldBe(0);
    }

    [Fact]
    public async Task FlushOptionalCachesAsync_clears_template_cache()
    {
        var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1");
        drv.TemplateCache.Put("dev", 1, new AbCipUdtShape("T", 4, []));
        drv.TemplateCache.Count.ShouldBe(1);

        await drv.FlushOptionalCachesAsync(CancellationToken.None);
        drv.TemplateCache.Count.ShouldBe(0);
    }

    // ---- helpers ----

    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
        public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();

        public IAddressSpaceBuilder Folder(string browseName, string displayName)
        { Folders.Add((browseName, displayName)); return this; }

        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
        { Variables.Add((browseName, info)); return new Handle(info.FullName); }

        public void AddProperty(string _, DriverDataType __, object? ___) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            public string FullReference => fullRef;
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }
        private sealed class NullSink : IAlarmConditionSink
        {
            public void OnTransition(AlarmEventArgs args) { }
        }
    }

    private sealed class FakeEnumeratorFactory : IAbCipTagEnumeratorFactory
    {
        private readonly AbCipDiscoveredTag[] _tags;
        public AbCipTagCreateParams? LastDeviceParams { get; private set; }
        public FakeEnumeratorFactory(params AbCipDiscoveredTag[] tags) => _tags = tags;
        public IAbCipTagEnumerator Create() => new FakeEnumerator(this);

        private sealed class FakeEnumerator(FakeEnumeratorFactory outer) : IAbCipTagEnumerator
        {
            public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
                AbCipTagCreateParams deviceParams,
                [EnumeratorCancellation] CancellationToken cancellationToken)
            {
                outer.LastDeviceParams = deviceParams;
                await Task.CompletedTask;
                foreach (var t in outer._tags) yield return t;
            }
            public void Dispose() { }
        }
    }
}
