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

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

[Trait("Category", "Unit")]
public sealed class AbCipPlcFamilyTests
{
    // ---- ControlLogix ----

    /// <summary>Verifies that the ControlLogix profile defaults match the large forward open baseline.</summary>
    [Fact]
    public void ControlLogix_profile_defaults_match_large_forward_open_baseline()
    {
        var p = AbCipPlcFamilyProfile.ControlLogix;
        p.LibplctagPlcAttribute.ShouldBe("controllogix");
        p.DefaultConnectionSize.ShouldBe(4002); // LFO — FW20+
        p.DefaultCipPath.ShouldBe("1,0");
        p.SupportsRequestPacking.ShouldBeTrue();
        p.SupportsConnectedMessaging.ShouldBeTrue();
        p.MaxFragmentBytes.ShouldBe(4000);
    }

    /// <summary>Verifies that a ControlLogix device initializes with the correct profile.</summary>
    [Fact]
    public async Task ControlLogix_device_initialises_with_correct_profile()
    {
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

        drv.GetDeviceState("ab://10.0.0.5/1,0")!.Profile.LibplctagPlcAttribute.ShouldBe("controllogix");
    }

    // ---- CompactLogix ----

    /// <summary>Verifies that the CompactLogix profile uses a narrower connection size than ControlLogix.</summary>
    [Fact]
    public void CompactLogix_profile_uses_narrower_connection_size()
    {
        var p = AbCipPlcFamilyProfile.CompactLogix;
        p.LibplctagPlcAttribute.ShouldBe("compactlogix");
        p.DefaultConnectionSize.ShouldBe(504); // 5069-L3x narrow-window safety
        p.DefaultCipPath.ShouldBe("1,0");
        p.SupportsRequestPacking.ShouldBeTrue();
        p.SupportsConnectedMessaging.ShouldBeTrue();
        p.MaxFragmentBytes.ShouldBe(500);
    }

    /// <summary>Verifies that a CompactLogix device initializes with a narrow connection size.</summary>
    [Fact]
    public async Task CompactLogix_device_initialises_with_narrow_ConnectionSize()
    {
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://192.168.1.10/1,0", AbCipPlcFamily.CompactLogix)],
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

        var profile = drv.GetDeviceState("ab://192.168.1.10/1,0")!.Profile;
        profile.DefaultConnectionSize.ShouldBeLessThan(AbCipPlcFamilyProfile.ControlLogix.DefaultConnectionSize);
        profile.MaxFragmentBytes.ShouldBeLessThan(AbCipPlcFamilyProfile.ControlLogix.MaxFragmentBytes);
    }

    // ---- Micro800 ----

    /// <summary>Verifies that the Micro800 profile is unconnected only and supports an empty CIP path.</summary>
    [Fact]
    public void Micro800_profile_is_unconnected_only_with_empty_path()
    {
        var p = AbCipPlcFamilyProfile.Micro800;
        p.LibplctagPlcAttribute.ShouldBe("micro800");
        p.DefaultConnectionSize.ShouldBe(488);
        p.DefaultCipPath.ShouldBe(""); // no backplane routing
        p.SupportsRequestPacking.ShouldBeFalse();
        p.SupportsConnectedMessaging.ShouldBeFalse();
        p.MaxFragmentBytes.ShouldBe(484);
    }

    /// <summary>Verifies that a Micro800 device with an empty CIP path parses correctly.</summary>
    [Fact]
    public async Task Micro800_device_with_empty_cip_path_parses_correctly()
    {
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://192.168.1.20/", AbCipPlcFamily.Micro800)],
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

        var state = drv.GetDeviceState("ab://192.168.1.20/")!;
        state.ParsedAddress.CipPath.ShouldBe("");
        state.Profile.SupportsRequestPacking.ShouldBeFalse();
        state.Profile.SupportsConnectedMessaging.ShouldBeFalse();
    }

    /// <summary>Verifies that Micro800 read operations forward the empty path to tag creation parameters.</summary>
    [Fact]
    public async Task Micro800_read_forwards_empty_path_to_tag_create_params()
    {
        var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { Value = 123 } };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://192.168.1.20/", AbCipPlcFamily.Micro800)],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("X", "ab://192.168.1.20/", "X", AbCipDataType.DInt)),
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.ReadAsync(["X"], CancellationToken.None);
        factory.Tags["X"].CreationParams.CipPath.ShouldBe("");
        factory.Tags["X"].CreationParams.LibplctagPlcAttribute.ShouldBe("micro800");
    }

    // ---- GuardLogix ----

    /// <summary>Verifies that the GuardLogix profile wire protocol mirrors ControlLogix.</summary>
    [Fact]
    public void GuardLogix_profile_wire_protocol_mirrors_ControlLogix()
    {
        var p = AbCipPlcFamilyProfile.GuardLogix;
        // Wire protocol is identical to ControlLogix — only the safety-partition semantics differ,
        // which is a per-tag concern surfaced via AbCipTagDefinition.SafetyTag.
        p.LibplctagPlcAttribute.ShouldBe("controllogix");
        p.DefaultConnectionSize.ShouldBe(AbCipPlcFamilyProfile.ControlLogix.DefaultConnectionSize);
        p.DefaultCipPath.ShouldBe(AbCipPlcFamilyProfile.ControlLogix.DefaultCipPath);
    }

    /// <summary>Verifies that GuardLogix safety tags surface as ViewOnly in discovery.</summary>
    [Fact]
    public async Task GuardLogix_safety_tag_surfaces_as_ViewOnly_in_discovery()
    {
        var builder = new RecordingBuilder();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.GuardLogix)],
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition("NormalTag", "ab://10.0.0.5/1,0", "N", AbCipDataType.DInt),
                new AbCipTagDefinition("SafetyTag", "ab://10.0.0.5/1,0", "S", AbCipDataType.DInt,
                    Writable: true, SafetyTag: true))
            ,
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        builder.Variables.Single(v => v.BrowseName == "NormalTag").Info.SecurityClass
            .ShouldBe(SecurityClassification.Operate);
        builder.Variables.Single(v => v.BrowseName == "SafetyTag").Info.SecurityClass
            .ShouldBe(SecurityClassification.ViewOnly);
    }

    /// <summary>Verifies that GuardLogix safety tag writes are rejected even when the tag is marked Writable.</summary>
    [Fact]
    public async Task GuardLogix_safety_tag_writes_rejected_even_when_Writable_is_true()
    {
        var factory = new FakeAbCipTagFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.GuardLogix)],
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition("SafetySet", "ab://10.0.0.5/1,0", "S", AbCipDataType.DInt,
                    Writable: true, SafetyTag: true))
            ,
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("SafetySet", 42)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNotWritable);
    }

    // ---- ForFamily dispatch ----

    /// <summary>Verifies that ForFamily dispatches to the correct profile for each PLC family.</summary>
    /// <param name="family">The AB CIP PLC family to test.</param>
    /// <param name="expectedAttribute">The expected libplctag PLC attribute string.</param>
    [Theory]
    [InlineData(AbCipPlcFamily.ControlLogix, "controllogix")]
    [InlineData(AbCipPlcFamily.CompactLogix, "compactlogix")]
    [InlineData(AbCipPlcFamily.Micro800, "micro800")]
    [InlineData(AbCipPlcFamily.GuardLogix, "controllogix")]
    public void ForFamily_dispatches_to_correct_profile(AbCipPlcFamily family, string expectedAttribute)
    {
        AbCipPlcFamilyProfile.ForFamily(family).LibplctagPlcAttribute.ShouldBe(expectedAttribute);
    }

    // ---- helpers ----

    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        /// <summary>Gets the list of folders recorded by this builder.</summary>
        public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
        /// <summary>Gets the list of variables recorded by this builder.</summary>
        public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();

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

        /// <summary>Adds a variable to the recorded list and returns a handle.</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.</param>
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
        { Variables.Add((browseName, info)); return new Handle(info.FullName); }

        /// <summary>No-op property adding operation for test compatibility.</summary>
        /// <param name="_">The property name.</param>
        /// <param name="__">The property data type.</param>
        /// <param name="___">The property value.</param>
        public void AddProperty(string _, DriverDataType __, object? ___) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            /// <summary>Gets the full reference for this variable handle.</summary>
            public string FullReference => fullRef;
            /// <summary>Marks this variable as an alarm condition and returns a null sink.</summary>
            /// <param name="info">The alarm condition information.</param>
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }
        private sealed class NullSink : IAlarmConditionSink
        {
            /// <summary>Called when an alarm state transitions.</summary>
            /// <param name="args">The alarm event arguments.</param>
            public void OnTransition(AlarmEventArgs args) { }
        }
    }
}
