using System.Text.Json;
using MQTTnet;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     The operator-visible half of the refused-CONNACK defect. <see cref="MqttConnectionTests"/>
///     pins that <c>MqttConnection.ConnectAsync</c> raises the rejection; this pins the thing an
///     operator actually sees — that a driver whose broker refused the CONNECT does <b>not</b> seal
///     green.
/// </summary>
/// <remarks>
///     Worth its own suite because the two failures are independent. The connection could classify a
///     rejection perfectly and the driver could still report <c>Healthy</c>, simply by not letting the
///     failure reach <c>InitializeCoreAsync</c>'s catch — which is exactly the shape the Task-13 live
///     fixture found (a wrong broker password produced <c>DriverState.Healthy</c> /
///     <c>HostState.Running</c> on a session that never authenticated).
/// </remarks>
[Trait("Category", "Unit")]
public sealed class MqttDriverConnectRejectionTests
{
    /// <summary>
    ///     THE regression pin. A configured-wrong password must fail the deploy loudly, not produce a
    ///     healthy driver that will never receive a single value.
    /// </summary>
    [Fact]
    public async Task InitializeAsync_BrokerRefusesTheCredentials_IsNotHealthy()
    {
        using var broker = new MiniBroker { ConnAckReturnCode = MiniBroker.ReturnCodeNotAuthorized };
        var options = BrokerOptions(broker.Port);
        await using var driver = new MqttDriver(options, "d", null);

        var ex = await Should.ThrowAsync<MqttConnectRejectedException>(
            async () => await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken));

        ex.ResultCode.ShouldBe(MqttClientConnectResultCode.NotAuthorized);

        var health = driver.GetHealth();
        health.State.ShouldNotBe(DriverState.Healthy, "a driver the broker refused must never seal green");
        health.State.ShouldBe(DriverState.Faulted);
        health.LastError.ShouldNotBeNullOrWhiteSpace();
        health.LastError!.ShouldContain("rejected the credentials");
    }

    /// <summary>
    ///     The transient counterpart: still a failed initialize (the driver has no session), but it must
    ///     not be reported as the same unrecoverable shape — a broker restarting is not a misconfigured
    ///     deployment.
    /// </summary>
    [Fact]
    public async Task InitializeAsync_BrokerTemporarilyUnavailable_FailsAsTransient()
    {
        using var broker = new MiniBroker { ConnAckReturnCode = MiniBroker.ReturnCodeServerUnavailable };
        var options = BrokerOptions(broker.Port);
        await using var driver = new MqttDriver(options, "d", null);

        var ex = await Should.ThrowAsync<MqttConnectRejectedException>(
            async () => await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken));

        ex.IsUnrecoverable.ShouldBeFalse();
        driver.GetHealth().State.ShouldNotBe(DriverState.Healthy);
    }

    /// <summary>
    ///     The control. Without it, every assertion above would still hold if the driver simply never
    ///     went healthy against this fake broker — the suite would be green and prove nothing.
    /// </summary>
    [Fact]
    public async Task InitializeAsync_BrokerAcceptsTheConnect_IsHealthy()
    {
        using var broker = new MiniBroker();
        var options = BrokerOptions(broker.Port);
        await using var driver = new MqttDriver(options, "d", null);

        await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken);

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    private static MqttDriverOptions BrokerOptions(int port) => new()
    {
        Mode = MqttMode.Plain,
        Host = "127.0.0.1",
        Port = port,
        UseTls = false,
        ProtocolVersion = MqttProtocolVersion.V311,
        Username = "svc",
        Password = "pw",
        KeepAliveSeconds = 300,
        ConnectTimeoutSeconds = 5,
        ReconnectMinBackoffSeconds = 1,
        ReconnectMaxBackoffSeconds = 2,
    };

    private static string Json(MqttDriverOptions options) => JsonSerializer.Serialize(options, MqttJson.Options);
}
