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

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

/// <summary>
///     Driver.TwinCAT.Cli-006: covers the prefix / max filtering and the RO/RW classification
///     logic inside <see cref="BrowseCommand"/>. The collecting builder + the filter pipeline
///     are pure — no ADS contact required — so they unit-test cleanly.
/// </summary>
[Trait("Category", "Unit")]
public sealed class BrowseCommandFilterTests
{
    private static DriverAttributeInfo Info(SecurityClassification cls, DriverDataType dt = DriverDataType.Int32)
        => new(
            FullName: "ignored",
            DriverDataType: dt,
            IsArray: false,
            ArrayDim: null,
            SecurityClass: cls,
            IsHistorized: false);

    /// <summary>Verifies that the collector records variables in call order.</summary>
    [Fact]
    public void Collector_records_each_variable_in_call_order()
    {
        var builder = new BrowseCommand.CollectingAddressSpaceBuilder();
        builder.Variable("GVL.A", "GVL.A", Info(SecurityClassification.Operate));
        builder.Variable("GVL.B", "GVL.B", Info(SecurityClassification.ViewOnly));

        builder.Variables.Count.ShouldBe(2);
        builder.Variables[0].BrowseName.ShouldBe("GVL.A");
        builder.Variables[1].BrowseName.ShouldBe("GVL.B");
    }

    /// <summary>Verifies that folder returns the same builder for flattening.</summary>
    [Fact]
    public void Folder_returns_same_builder_so_nested_variables_land_in_one_flat_list()
    {
        // BrowseCommand expects a flat list — TwinCAT's flat-mode symbol walk doesn't nest
        // into sub-folders, but DiscoverAsync may still call Folder() before Variable().
        var builder = new BrowseCommand.CollectingAddressSpaceBuilder();
        var nested = builder.Folder("Discovered", "Discovered");
        nested.Variable("GVL.X", "GVL.X", Info(SecurityClassification.Operate));

        builder.Variables.Count.ShouldBe(1);
        builder.Variables[0].BrowseName.ShouldBe("GVL.X");
    }

    /// <summary>Verifies that empty prefix returns all symbols up to max.</summary>
    [Fact]
    public void FilterAndLimit_empty_prefix_returns_everything_up_to_max()
    {
        var symbols = new List<(string BrowseName, DriverAttributeInfo Info)>
        {
            ("GVL_A.x", Info(SecurityClassification.Operate)),
            ("GVL_B.y", Info(SecurityClassification.ViewOnly)),
            ("MAIN.z",  Info(SecurityClassification.Operate)),
        };

        var matched = BrowseCommand.FilterByPrefix(symbols, prefix: null);
        matched.Count.ShouldBe(3);
    }

    /// <summary>Verifies that prefix filtering is case-sensitive.</summary>
    [Fact]
    public void FilterAndLimit_prefix_is_case_sensitive()
    {
        var symbols = new List<(string BrowseName, DriverAttributeInfo Info)>
        {
            ("GVL_Fixture.x", Info(SecurityClassification.Operate)),
            ("gvl_fixture.y", Info(SecurityClassification.Operate)),
            ("MAIN.z",        Info(SecurityClassification.Operate)),
        };

        var matched = BrowseCommand.FilterByPrefix(symbols, prefix: "GVL_Fixture");

        matched.Count.ShouldBe(1);
        matched[0].BrowseName.ShouldBe("GVL_Fixture.x");
    }

    /// <summary>Verifies that zero max limit means unbounded.</summary>
    [Fact]
    public void FilterAndLimit_zero_max_means_unbounded()
    {
        var symbols = Enumerable.Range(0, 10)
            .Select(i => ($"GVL.S{i}", Info(SecurityClassification.Operate)))
            .ToList();

        var limit = BrowseCommand.PrintLimit(symbols.Count, max: 0);
        limit.ShouldBe(10);
    }

    /// <summary>Verifies that output is capped to max when more symbols match.</summary>
    [Fact]
    public void FilterAndLimit_caps_to_max_when_more_matched()
    {
        BrowseCommand.PrintLimit(matchedCount: 1000, max: 50).ShouldBe(50);
    }

    /// <summary>Verifies that output is not padded when fewer symbols match.</summary>
    [Fact]
    public void FilterAndLimit_does_not_pad_to_max_when_fewer_matched()
    {
        BrowseCommand.PrintLimit(matchedCount: 3, max: 50).ShouldBe(3);
    }

    /// <summary>Verifies that ViewOnly attribute returns RO access tag.</summary>
    [Fact]
    public void AccessTag_returns_RO_for_ViewOnly_attribute()
    {
        BrowseCommand.AccessTag(Info(SecurityClassification.ViewOnly)).ShouldBe("RO");
    }

    /// <summary>Verifies that all classifications except ViewOnly return RW access tag.</summary>
    /// <param name="cls">The security classification to test.</param>
    [Theory]
    [InlineData(SecurityClassification.FreeAccess)]
    [InlineData(SecurityClassification.Operate)]
    [InlineData(SecurityClassification.SecuredWrite)]
    [InlineData(SecurityClassification.VerifiedWrite)]
    [InlineData(SecurityClassification.Tune)]
    [InlineData(SecurityClassification.Configure)]
    public void AccessTag_returns_RW_for_anything_except_ViewOnly(SecurityClassification cls)
    {
        // BrowseCommand's display logic flips ViewOnly = RO, everything else = RW. The real
        // ACL is enforced server-side from the SecurityClassification — the CLI label is just
        // a coarse "is this writable from any tier" indicator.
        BrowseCommand.AccessTag(Info(cls)).ShouldBe("RW");
    }
}
