using Microsoft.Extensions.Logging.Abstractions;
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 DiscoveryDriverBrowserSettleTests
{
    private static readonly TimeSpan ShortShutdown = TimeSpan.FromMilliseconds(200);

    private static DiscoveryDriverBrowser Browser(IDriverFactory factory, TimeSpan openTimeout) =>
        new(factory, NullLogger<DiscoveryDriverBrowser>.Instance, openTimeout, ShortShutdown);

    private static void StreamN(IAddressSpaceBuilder builder, int n)
    {
        for (var i = 0; i < n; i++)
            builder.Variable($"T{i}", $"T{i}",
                new DriverAttributeInfo($"Dev.T{i}", DriverDataType.Float64, false, null,
                    SecurityClassification.Operate, false));
    }

    [Fact]
    public async Task UntilStable_RerunsUntilNonEmptyAndStable()
    {
        // Pass 1 streams nothing (FixedTree cache still filling); passes 2+ stream 3 nodes.
        var driver = new FakeDiscoveryDriver
        {
            RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable,
            OnDiscover = (b, idx) => { if (idx >= 2) StreamN(b, 3); return Task.CompletedTask; },
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(30));

        var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None);
        var root = await session.RootAsync(CancellationToken.None);

        root.Count.ShouldBe(3);
        driver.DiscoverCalls.ShouldBeGreaterThanOrEqualTo(3); // empty -> 3 -> 3-stable
    }

    [Fact]
    public async Task UntilStable_NeverStable_ReturnsLastNonEmptyAtTimeout()
    {
        // Node count grows every pass — never stabilises; on open-timeout return the last capture.
        var driver = new FakeDiscoveryDriver
        {
            RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable,
            OnDiscover = (b, idx) => { StreamN(b, idx); return Task.CompletedTask; },
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(3));

        var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None);
        var root = await session.RootAsync(CancellationToken.None);

        root.ShouldNotBeEmpty(); // last non-empty capture, not an exception
    }

    [Fact]
    public async Task UntilStable_NeverAnyNodes_FailsAtOpenTimeout()
    {
        // Every pass empty — no non-empty capture to fall back to, so the timeout surfaces.
        var driver = new FakeDiscoveryDriver
        {
            RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable,
            OnDiscover = (_, _) => Task.CompletedTask,
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(2));

        await Should.ThrowAsync<OperationCanceledException>(
            () => browser.OpenAsync("Fake", "{}", CancellationToken.None));
    }

    [Fact]
    public async Task OncePolicy_SingleDiscoverPass()
    {
        var driver = new FakeDiscoveryDriver
        {
            RediscoverPolicy = DiscoveryRediscoverPolicy.Once,
            OnDiscover = (b, _) => { StreamN(b, 3); return Task.CompletedTask; },
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(30));

        await browser.OpenAsync("Fake", "{}", CancellationToken.None);
        driver.DiscoverCalls.ShouldBe(1);
    }
}
