using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
///     E2E integration coverage for the <c>ReconnectDriver</c> command path through
///     <see cref="IAdminOperationsClient"/>.
///
///     <para><b>Scope note:</b> wiring a live <c>DriverInstanceActor</c> for the full
///     Healthy → Reconnecting → Healthy health-transition assertion requires a deployed
///     driver row in the config DB, a real fixture endpoint, and the
///     <c>DriverHostActor</c> to have registered the instance — substantially more
///     harness complexity than the two-node cluster setup alone provides. That deeper
///     fixture is tracked as a follow-up. This suite instead verifies the message
///     round-trip through the <c>AdminOperationsActor</c> singleton: the command is
///     accepted, persisted as a <c>ConfigEdit</c> audit row, and the reply carries
///     <c>Ok = true</c> with the matching <c>CorrelationId</c>. The DPS broadcast
///     that triggers the actor-side reconnect is exercised by the control-plane unit
///     tests that mock <c>IActorRef</c>.</para>
/// </summary>
[Trait("Category", "Integration")]
public sealed class DriverReconnectE2eTests
{
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    /// <summary>
    ///     Verifies that a <see cref="ReconnectDriver"/> message dispatched through
    ///     <see cref="IAdminOperationsClient.AskAsync{T}"/> returns a
    ///     <see cref="ReconnectDriverResult"/> with <c>Ok = true</c> and the matching
    ///     correlation ID, confirming the cluster-singleton round-trip works end-to-end.
    ///
    ///     <para>The instance ID used here ("reconnect-e2e-nonexistent") does not correspond
    ///     to a deployed driver, so no <c>DriverInstanceActor</c> will act on the DPS
    ///     broadcast — the test is validating the command ingestion and reply path only.</para>
    /// </summary>
    [Fact]
    public async Task Reconnect_RoundTrip_ReturnsOk()
    {
        await using var harness = await TwoNodeClusterHarness.StartAsync();
        await using var scope = harness.NodeA.Services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IAdminOperationsClient>();

        var correlationId = Guid.NewGuid();
        var msg = new ReconnectDriver(
            ClusterId: "cluster-e2e-test",
            DriverInstanceId: "reconnect-e2e-nonexistent",
            ActorByUserName: "e2e-test-runner",
            CorrelationId: correlationId);

        var result = await client.AskAsync<ReconnectDriverResult>(msg, Ct);

        result.CorrelationId.ShouldBe(correlationId);
        result.Ok.ShouldBeTrue($"ReconnectDriver round-trip failed: {result.Message}");
        result.Message.ShouldBeNull();
    }

    /// <summary>
    ///     Verifies that a second <see cref="ReconnectDriver"/> for the same instance ID
    ///     is also accepted (idempotent at the actor layer — the actor simply re-broadcasts
    ///     to DPS and writes another <c>ConfigEdit</c> row).
    /// </summary>
    [Fact]
    public async Task Reconnect_IsIdempotent_SecondCallAlsoReturnsOk()
    {
        await using var harness = await TwoNodeClusterHarness.StartAsync();
        await using var scope = harness.NodeA.Services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IAdminOperationsClient>();

        const string instanceId = "reconnect-idempotency-test";

        var first = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid());
        var second = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid());

        var r1 = await client.AskAsync<ReconnectDriverResult>(first, Ct);
        var r2 = await client.AskAsync<ReconnectDriverResult>(second, Ct);

        r1.Ok.ShouldBeTrue($"First call failed: {r1.Message}");
        r2.Ok.ShouldBeTrue($"Second call failed: {r2.Message}");
        r1.CorrelationId.ShouldBe(first.CorrelationId);
        r2.CorrelationId.ShouldBe(second.CorrelationId);
    }
}
