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

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

[Trait("Category", "Unit")]
public sealed class OpcUaClientFailoverTests
{
    /// <summary>Verifies that ResolveEndpointCandidates prefers EndpointUrls when provided.</summary>
    [Fact]
    public void ResolveEndpointCandidates_prefers_EndpointUrls_when_provided()
    {
        var opts = new OpcUaClientDriverOptions
        {
            EndpointUrl = "opc.tcp://fallback:4840",
            EndpointUrls = ["opc.tcp://primary:4840", "opc.tcp://backup:4841"],
        };
        var list = OpcUaClientDriver.ResolveEndpointCandidates(opts);
        list.Count.ShouldBe(2);
        list[0].ShouldBe("opc.tcp://primary:4840");
        list[1].ShouldBe("opc.tcp://backup:4841");
    }

    /// <summary>Verifies that ResolveEndpointCandidates falls back to single EndpointUrl when list is empty.</summary>
    [Fact]
    public void ResolveEndpointCandidates_falls_back_to_single_EndpointUrl_when_list_empty()
    {
        var opts = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://only:4840" };
        var list = OpcUaClientDriver.ResolveEndpointCandidates(opts);
        list.Count.ShouldBe(1);
        list[0].ShouldBe("opc.tcp://only:4840");
    }

    /// <summary>Verifies that an empty EndpointUrls list is treated as a fallback to EndpointUrl.</summary>
    [Fact]
    public void ResolveEndpointCandidates_empty_list_treated_as_fallback_to_EndpointUrl()
    {
        // Explicit empty list should still fall back to the single-URL shortcut rather than
        // producing a zero-candidate sweep that would immediately throw with no URLs tried.
        var opts = new OpcUaClientDriverOptions
        {
            EndpointUrl = "opc.tcp://single:4840",
            EndpointUrls = [],
        };
        OpcUaClientDriver.ResolveEndpointCandidates(opts).Count.ShouldBe(1);
    }

    /// <summary>Verifies that HostName uses the first candidate before connection.</summary>
    [Fact]
    public void HostName_uses_first_candidate_before_connect()
    {
        var opts = new OpcUaClientDriverOptions
        {
            EndpointUrls = ["opc.tcp://primary:4840", "opc.tcp://backup:4841"],
        };
        using var drv = new OpcUaClientDriver(opts, "opcua-host");
        drv.HostName.ShouldBe("opc.tcp://primary:4840",
            "pre-connect the dashboard should show the first candidate URL so operators can link back");
    }

    /// <summary>Verifies that initializing against all unreachable endpoints throws AggregateException listing each.</summary>
    [Fact]
    public async Task Initialize_against_all_unreachable_endpoints_throws_AggregateException_listing_each()
    {
        // Port 1 + port 2 + port 3 on loopback are all guaranteed closed (TCP RST immediate).
        // Failover sweep should attempt all three and throw AggregateException naming each URL
        // so operators see exactly which candidates were tried.
        var opts = new OpcUaClientDriverOptions
        {
            EndpointUrls = ["opc.tcp://127.0.0.1:1", "opc.tcp://127.0.0.1:2", "opc.tcp://127.0.0.1:3"],
            PerEndpointConnectTimeout = TimeSpan.FromMilliseconds(500),
            Timeout = TimeSpan.FromMilliseconds(500),
            AutoAcceptCertificates = true,
            // SystemPlatform kind needs no UNS mapping table — keeps this failover test
            // focused on the endpoint sweep rather than §8 namespace validation.
            TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
        };
        using var drv = new OpcUaClientDriver(opts, "opcua-failover");

        var ex = await Should.ThrowAsync<AggregateException>(async () =>
            await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));

        ex.Message.ShouldContain("127.0.0.1:1");
        ex.Message.ShouldContain("127.0.0.1:2");
        ex.Message.ShouldContain("127.0.0.1:3");
        drv.GetHealth().State.ShouldBe(DriverState.Faulted);
    }
}
