using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     Connect / recreate orchestration tests for <see cref="GalaxyMxSession"/>. The SDK
///     session/client types are sealed with internal ctors and cannot be faked, so these
///     drive the open body through the <c>OpenAndRegisterOverrideForTests</c> seam. The
///     core regression guard is <see cref="Recreate_after_connect_opens_a_fresh_session"/>:
///     a stale-session reconnect must rebuild rather than no-op (the gateway-restart bug).
/// </summary>
public sealed class GalaxyMxSessionReconnectTests
{
    private static GalaxyMxAccessOptions MinimalOptions() => new(ClientName: "OtOpcUaTest");

    private static GalaxyMxSession NewSession() => new(MinimalOptions());

    /// <summary>A second <c>ConnectAsync</c> while connected must be a no-op (idempotent guard).</summary>
    [Fact]
    public async Task Connect_then_connect_again_is_a_noop()
    {
        var session = NewSession();
        var openCount = 0;
        session.OpenAndRegisterOverrideForTests = _ =>
        {
            openCount++;
            return Task.CompletedTask;
        };

        await session.ConnectAsync(null!, CancellationToken.None);
        openCount.ShouldBe(1);

        await session.ConnectAsync(null!, CancellationToken.None);
        openCount.ShouldBe(1);
    }

    /// <summary>
    ///     The regression guard for the gateway-restart bug: <c>RecreateAsync</c> must bypass
    ///     the no-op guard and re-run the open body even though a (stale) session is present.
    /// </summary>
    [Fact]
    public async Task Recreate_after_connect_opens_a_fresh_session()
    {
        var session = NewSession();
        var openCount = 0;
        session.OpenAndRegisterOverrideForTests = _ =>
        {
            openCount++;
            return Task.CompletedTask;
        };

        await session.ConnectAsync(null!, CancellationToken.None);
        openCount.ShouldBe(1);

        await session.RecreateAsync(null!, CancellationToken.None);
        openCount.ShouldBe(2);
    }

    /// <summary><c>RecreateAsync</c> on a never-connected session still opens (teardown is a no-op first).</summary>
    [Fact]
    public async Task Recreate_when_never_connected_still_opens()
    {
        var session = NewSession();
        var openCount = 0;
        session.OpenAndRegisterOverrideForTests = _ =>
        {
            openCount++;
            return Task.CompletedTask;
        };

        await session.RecreateAsync(null!, CancellationToken.None);
        openCount.ShouldBe(1);
    }

    /// <summary>
    ///     A failed first connect must not leave <c>_connected</c> set — the next attempt has
    ///     to reach the open body again (partial-open teardown).
    /// </summary>
    [Fact]
    public async Task Connect_failure_is_not_left_half_open()
    {
        var session = NewSession();
        var openCount = 0;
        session.OpenAndRegisterOverrideForTests = _ =>
        {
            // Throw on the first attempt, succeed on the second.
            if (openCount++ == 0)
            {
                throw new InvalidOperationException("simulated open failure");
            }

            return Task.CompletedTask;
        };

        await Should.ThrowAsync<InvalidOperationException>(
            async () => await session.ConnectAsync(null!, CancellationToken.None));
        openCount.ShouldBe(1);
        session.IsConnected.ShouldBeFalse(); // _connected must NOT be latched by the failed attempt.

        // The failed first attempt must not have latched _connected — the retry reaches the body.
        await session.ConnectAsync(null!, CancellationToken.None);
        openCount.ShouldBe(2);
    }

    /// <summary>
    ///     <see cref="GalaxyMxSession.IsConnected"/> tracks the <c>_connected</c> guard across the
    ///     full lifecycle: false when fresh, true after connect, still true after a recreate, and
    ///     false again after dispose.
    /// </summary>
    [Fact]
    public async Task IsConnected_reflects_connect_recreate_and_dispose()
    {
        var session = NewSession();
        var openCount = 0;
        session.OpenAndRegisterOverrideForTests = _ =>
        {
            openCount++;
            return Task.CompletedTask;
        };

        session.IsConnected.ShouldBeFalse();

        await session.ConnectAsync(null!, CancellationToken.None);
        session.IsConnected.ShouldBeTrue();

        await session.RecreateAsync(null!, CancellationToken.None);
        session.IsConnected.ShouldBeTrue();

        await session.DisposeAsync();
        session.IsConnected.ShouldBeFalse();
    }
}
