using Shouldly;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Browsing;

public class CapturedTreeBrowseSessionTests
{
    private static DriverAttributeInfo Attr(string fullName, bool isAlarm = false) =>
        new(fullName, DriverDataType.Float64, false, null, SecurityClassification.Operate, false, IsAlarm: isAlarm);

    // A small captured graph: a folder Plc1 with a Speed leaf and an alarm leaf, plus a root-level leaf.
    // truncated=true builds with a tiny cap so recording stops short and Truncated flips.
    private static CapturedTree SampleTree(bool truncated = false)
    {
        var b = new CapturingAddressSpaceBuilder(nodeCap: truncated ? 2 : 100);
        var plc = b.Folder("Plc1", "PLC 1");
        plc.Variable("Speed", "Speed", Attr("Plc1.Speed"));
        plc.Variable("Alarm", "Alarm", Attr("Plc1.Alarm")).MarkAsAlarmCondition(
            new AlarmConditionInfo("Alarm", AlarmSeverity.High, null));
        b.Variable("Direct", "Direct", Attr("Root.Direct"));
        return b.Build();
    }

    [Fact]
    public async Task RootReturnsTopLevelNodes()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree());
        var root = await s.RootAsync(CancellationToken.None);

        var folder = root.Single(n => n.NodeId == "/Plc1");
        folder.Kind.ShouldBe(BrowseNodeKind.Folder);
        folder.HasChildrenHint.ShouldBeTrue();

        var leaf = root.Single(n => n.NodeId == "Root.Direct");
        leaf.Kind.ShouldBe(BrowseNodeKind.Leaf);
        leaf.NodeId.ShouldBe("Root.Direct"); // leaf NodeId IS the driver FullName
    }

    [Fact]
    public async Task ExpandReturnsCapturedChildren()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree());
        var kids = await s.ExpandAsync("/Plc1", CancellationToken.None);
        kids.Select(n => n.NodeId).ShouldBe(new[] { "Plc1.Speed", "Plc1.Alarm" });
    }

    [Fact]
    public async Task ExpandUnknownNodeReturnsEmpty()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree());
        (await s.ExpandAsync("/nope", CancellationToken.None)).ShouldBeEmpty();
    }

    [Fact]
    public async Task AttributesForLeafMapsDriverAttributeInfo()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree());
        var attrs = await s.AttributesAsync("Plc1.Speed", CancellationToken.None);
        var a = attrs.Single();
        a.Name.ShouldBe("Speed");
        a.DriverDataType.ShouldBe(DriverDataType.Float64.ToString());
        a.IsArray.ShouldBeFalse();
        a.SecurityClass.ShouldBe(SecurityClassification.Operate.ToString());
        a.IsAlarm.ShouldBeFalse();

        // Alarm marked during discovery surfaces IsAlarm=true even though attr.IsAlarm is false.
        var alarmAttrs = await s.AttributesAsync("Plc1.Alarm", CancellationToken.None);
        alarmAttrs.Single().IsAlarm.ShouldBeTrue();
    }

    [Fact]
    public async Task TruncatedTreeAppendsMarkerFolderAtRoot()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree(truncated: true));
        var root = await s.RootAsync(CancellationToken.None);
        var marker = root[^1];
        marker.Kind.ShouldBe(BrowseNodeKind.Folder);
        marker.HasChildrenHint.ShouldBeFalse();
        marker.DisplayName.ToLowerInvariant().ShouldContain("truncated");
    }

    [Fact]
    public async Task CallsRefreshLastUsedUtc()
    {
        await using var s = new CapturedTreeBrowseSession(SampleTree());
        var before = s.LastUsedUtc;
        await Task.Delay(5, TestContext.Current.CancellationToken);
        await s.RootAsync(CancellationToken.None);
        s.LastUsedUtc.ShouldBeGreaterThan(before);
    }

    [Fact]
    public async Task DisposedSessionThrowsObjectDisposed()
    {
        var s = new CapturedTreeBrowseSession(SampleTree());
        await s.DisposeAsync();
        await Should.ThrowAsync<ObjectDisposedException>(() => s.RootAsync(CancellationToken.None));
    }

    [Fact]
    public async Task TwoSessionsOverSameTreeAreIndependent()
    {
        var tree = SampleTree();
        var s1 = new CapturedTreeBrowseSession(tree);
        var s2 = new CapturedTreeBrowseSession(tree);
        s1.Token.ShouldNotBe(s2.Token);

        await s1.DisposeAsync();
        // Disposing s1 must not affect s2 — the tree is immutable and shared.
        (await s2.RootAsync(CancellationToken.None)).ShouldNotBeEmpty();
        await s2.DisposeAsync();
    }
}
