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 DiscoveryDriverBrowserTests
{
    private static readonly TimeSpan ShortOpen = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan ShortShutdown = TimeSpan.FromMilliseconds(200);

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

    [Fact]
    public async Task OpenAsync_RunsInitializeDiscoverShutdown_InOrder()
    {
        var driver = new FakeDiscoveryDriver();
        var browser = Browser(FakeDriverFactory.Of(() => driver));

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

        session.ShouldNotBeNull();
        driver.Trace.ShouldBe(new[] { "init", "discover", "shutdown" });
        driver.InitializeCalls.ShouldBe(1);
        driver.ShutdownCalls.ShouldBe(1);
    }

    [Fact]
    public async Task OpenAsync_ShutdownRuns_EvenWhenDiscoverThrows()
    {
        var driver = new FakeDiscoveryDriver
        {
            OnDiscover = (_, _) => throw new InvalidOperationException("boom-discovery"),
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver));

        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => browser.OpenAsync("Fake", "{}", CancellationToken.None));
        ex.Message.ShouldBe("boom-discovery"); // discovery error surfaces, not a shutdown error
        driver.ShutdownCalls.ShouldBe(1);       // shutdown still ran
    }

    [Fact]
    public async Task OpenAsync_UnknownType_ThrowsCleanMessage()
    {
        var browser = Browser(new FakeDriverFactory((_, _, _) => null));
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => browser.OpenAsync("Ghost", "{}", CancellationToken.None));
        ex.Message.ShouldContain("Ghost");
    }

    [Fact]
    public async Task OpenAsync_NotDiscoveryCapable_Throws()
    {
        var browser = Browser(new FakeDriverFactory((_, _, _) => new FakeNonDiscoveryDriver()));
        await Should.ThrowAsync<InvalidOperationException>(
            () => browser.OpenAsync("Flat", "{}", CancellationToken.None));
    }

    [Fact]
    public async Task OpenAsync_FlagFalse_Throws()
    {
        var browser = Browser(FakeDriverFactory.Of(() => new FakeDiscoveryDriver { SupportsOnlineDiscovery = false }));
        await Should.ThrowAsync<InvalidOperationException>(
            () => browser.OpenAsync("Fake", "{}", CancellationToken.None));
    }

    [Fact]
    public void PatchForBrowse_MergesAbCipPatch()
    {
        var patched = DiscoveryDriverBrowser.PatchForBrowse("AbCip", """{ "Host": "1.2.3.4" }""");
        var obj = System.Text.Json.Nodes.JsonNode.Parse(patched)!.AsObject();
        ((string?)obj["Host"]).ShouldBe("1.2.3.4");
        ((bool?)obj["EnableControllerBrowse"]).ShouldBe(true);

        // Unknown type: unchanged.
        DiscoveryDriverBrowser.PatchForBrowse("Modbus", """{ "X": 1 }""").ShouldBe("""{ "X": 1 }""");
    }

    [Fact]
    public void PatchForBrowse_DeepMergesFocasFixedTree()
    {
        var patched = DiscoveryDriverBrowser.PatchForBrowse("FOCAS", """{ "FixedTree": { "Series": "30i" } }""");
        var fixedTree = System.Text.Json.Nodes.JsonNode.Parse(patched)!["FixedTree"]!.AsObject();
        ((string?)fixedTree["Series"]).ShouldBe("30i");   // kept
        ((bool?)fixedTree["Enabled"]).ShouldBe(true);      // added
    }

    [Fact]
    public void CanBrowse_TrueForDiscoveryCapable()
    {
        var browser = Browser(FakeDriverFactory.Of(() => new FakeDiscoveryDriver()));
        browser.CanBrowse("Fake", "{}").ShouldBeTrue();
    }

    [Fact]
    public void CanBrowse_FalseWhenTryCreateReturnsNull()
    {
        var browser = Browser(new FakeDriverFactory((_, _, _) => null));
        browser.CanBrowse("Ghost", "{}").ShouldBeFalse();
    }

    [Fact]
    public void CanBrowse_FalseWhenTryCreateThrows_NoExceptionEscapes()
    {
        var browser = Browser(new FakeDriverFactory((_, _, _) => throw new System.Text.Json.JsonException("bad json")));
        Should.NotThrow(() => browser.CanBrowse("Fake", "{ half-typed").ShouldBeFalse());
    }

    [Fact]
    public async Task CanBrowse_ShutsDownThrowawayInstance()
    {
        var driver = new FakeDiscoveryDriver { SupportsOnlineDiscovery = false };
        var factory = FakeDriverFactory.Of(() => driver);
        var browser = Browser(factory);

        browser.CanBrowse("Fake", "{}").ShouldBeFalse();
        // Teardown is fire-and-forget — await its completion.
        await driver.ShutdownDone.Task.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
        driver.ShutdownCalls.ShouldBe(1);
    }

    [Fact]
    public async Task BoundedShutdown_AbandonsWedgedDriver()
    {
        // Shutdown ignores its token and would never complete; the browser must abandon it
        // and still return the successfully-captured session within the shutdown bound.
        var driver = new FakeDiscoveryDriver
        {
            OnShutdown = _ => Task.Delay(Timeout.Infinite, CancellationToken.None),
        };
        var browser = Browser(FakeDriverFactory.Of(() => driver));

        var sw = System.Diagnostics.Stopwatch.StartNew();
        var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None);
        sw.Stop();

        session.ShouldNotBeNull();                       // discovery succeeded → usable session
        driver.ShutdownCalls.ShouldBe(1);                // shutdown was attempted
        sw.Elapsed.ShouldBeLessThan(ShortOpen);          // returned on the shutdown bound, not the open timeout
    }
}
