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

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Observability;

[Trait("Category", "Unit")]
public sealed class DriverHealthReportTests
{
    /// <summary>Verifies that an empty fleet is healthy.</summary>
    [Fact]
    public void EmptyFleet_IsHealthy()
    {
        DriverHealthReport.Aggregate([]).ShouldBe(ReadinessVerdict.Healthy);
    }

    /// <summary>Verifies that a fleet with all healthy drivers is healthy.</summary>
    [Fact]
    public void AllHealthy_Fleet_IsHealthy()
    {
        var verdict = DriverHealthReport.Aggregate([
            new DriverHealthSnapshot("a", DriverState.Healthy),
            new DriverHealthSnapshot("b", DriverState.Healthy),
        ]);
        verdict.ShouldBe(ReadinessVerdict.Healthy);
    }

    /// <summary>Verifies that any faulted driver trumps other states.</summary>
    [Fact]
    public void AnyFaulted_TrumpsEverything()
    {
        var verdict = DriverHealthReport.Aggregate([
            new DriverHealthSnapshot("a", DriverState.Healthy),
            new DriverHealthSnapshot("b", DriverState.Degraded),
            new DriverHealthSnapshot("c", DriverState.Faulted),
            new DriverHealthSnapshot("d", DriverState.Initializing),
        ]);
        verdict.ShouldBe(ReadinessVerdict.Faulted);
    }

    /// <summary>Verifies that any not-ready driver without faults results in NotReady verdict.</summary>
    /// <param name="initializingState">The driver state representing a not-ready condition.</param>
    [Theory]
    [InlineData(DriverState.Unknown)]
    [InlineData(DriverState.Initializing)]
    public void Any_NotReady_WithoutFaulted_IsNotReady(DriverState initializingState)
    {
        var verdict = DriverHealthReport.Aggregate([
            new DriverHealthSnapshot("a", DriverState.Healthy),
            new DriverHealthSnapshot("b", initializingState),
        ]);
        verdict.ShouldBe(ReadinessVerdict.NotReady);
    }

    /// <summary>Verifies that any degraded driver without faults or not-ready results in Degraded verdict.</summary>
    [Fact]
    public void Any_Degraded_WithoutFaultedOrNotReady_IsDegraded()
    {
        var verdict = DriverHealthReport.Aggregate([
            new DriverHealthSnapshot("a", DriverState.Healthy),
            new DriverHealthSnapshot("b", DriverState.Degraded),
        ]);
        verdict.ShouldBe(ReadinessVerdict.Degraded);
    }

    /// <summary>Verifies that HTTP status codes match the readiness verdict state matrix.</summary>
    /// <param name="verdict">The readiness verdict to test.</param>
    /// <param name="expected">The expected HTTP status code.</param>
    [Theory]
    [InlineData(ReadinessVerdict.Healthy, 200)]
    [InlineData(ReadinessVerdict.Degraded, 200)]
    [InlineData(ReadinessVerdict.NotReady, 503)]
    [InlineData(ReadinessVerdict.Faulted, 503)]
    public void HttpStatus_MatchesStateMatrix(ReadinessVerdict verdict, int expected)
    {
        DriverHealthReport.HttpStatus(verdict).ShouldBe(expected);
    }

    /// <summary>
    ///     Core-012 regression: <see cref="DriverState.Reconnecting"/> must aggregate to
    ///     <see cref="ReadinessVerdict.Degraded"/> — the doc remarks state matrix lists this
    ///     mapping (after the Core-012 doc fix that added the Reconnecting row).
    /// </summary>
    [Fact]
    public void Any_Reconnecting_WithoutFaultedOrNotReady_IsDegraded()
    {
        var verdict = DriverHealthReport.Aggregate([
            new DriverHealthSnapshot("a", DriverState.Healthy),
            new DriverHealthSnapshot("b", DriverState.Reconnecting),
        ]);
        verdict.ShouldBe(ReadinessVerdict.Degraded,
            "Reconnecting = driver alive but not serving live data → /readyz stays 200 while operators see the affected driver in the body");
    }

    /// <summary>
    ///     Core-012 regression: assert the XML <c>&lt;remarks&gt;</c> on
    ///     <see cref="DriverHealthReport"/> names <see cref="DriverState.Reconnecting"/> in its
    ///     state matrix. Catches a future doc-drift if someone re-aliases Reconnecting without
    ///     updating the matrix.
    /// </summary>
    [Fact]
    public void Doc_State_Matrix_Includes_Reconnecting()
    {
        var xmlPath = Path.Combine(
            AppContext.BaseDirectory,
            "ZB.MOM.WW.OtOpcUa.Core.xml");
        File.Exists(xmlPath).ShouldBeTrue($"expected XML doc file at {xmlPath}");

        var content = File.ReadAllText(xmlPath);
        var driverHealthReportRemarks = ExtractRemarksFor(content, "T:ZB.MOM.WW.OtOpcUa.Core.Observability.DriverHealthReport");

        driverHealthReportRemarks.ShouldContain("Reconnecting");
    }

    private static string ExtractRemarksFor(string xml, string member)
    {
        var memberStart = xml.IndexOf($"<member name=\"{member}\"", StringComparison.Ordinal);
        if (memberStart < 0) return string.Empty;
        var memberEnd = xml.IndexOf("</member>", memberStart, StringComparison.Ordinal);
        if (memberEnd < 0) return string.Empty;
        var slice = xml.Substring(memberStart, memberEnd - memberStart);
        var remarksStart = slice.IndexOf("<remarks>", StringComparison.Ordinal);
        if (remarksStart < 0) return string.Empty;
        var remarksEnd = slice.IndexOf("</remarks>", remarksStart, StringComparison.Ordinal);
        return remarksEnd < 0 ? string.Empty : slice.Substring(remarksStart, remarksEnd - remarksStart);
    }
}
