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

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

[Trait("Category", "Unit")]
public sealed class WedgeDetectorTests
{
    private static readonly DateTime Now = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc);
    private static readonly TimeSpan Threshold = TimeSpan.FromSeconds(120);

    /// <summary>Verifies that thresholds below 60 seconds are clamped to 60 seconds.</summary>
    [Fact]
    public void SubSixtySecondThreshold_ClampsToSixty()
    {
        var detector = new WedgeDetector(TimeSpan.FromSeconds(10));
        detector.Threshold.ShouldBe(TimeSpan.FromSeconds(60));
    }

    /// <summary>Verifies that unhealthy drivers always return NotApplicable verdict.</summary>
    [Fact]
    public void Unhealthy_Driver_AlwaysNotApplicable()
    {
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 5, ActiveMonitoredItems: 10, QueuedHistoryReads: 0, LastProgressUtc: Now.AddMinutes(-10));

        detector.Classify(DriverState.Faulted, demand, Now).ShouldBe(WedgeVerdict.NotApplicable);
        detector.Classify(DriverState.Degraded, demand, Now).ShouldBe(WedgeVerdict.NotApplicable);
        detector.Classify(DriverState.Initializing, demand, Now).ShouldBe(WedgeVerdict.NotApplicable);
    }

    /// <summary>Verifies that an idle subscription-only driver stays Idle.</summary>
    [Fact]
    public void Idle_Subscription_Only_StaysIdle()
    {
        // Idle driver: bulkhead 0, monitored items 0, no history reads queued.
        // Even if LastProgressUtc is ancient, the verdict is Idle, not Faulted.
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(0, 0, 0, Now.AddHours(-12));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Idle);
    }

    /// <summary>Verifies that pending work with recent progress stays Healthy.</summary>
    [Fact]
    public void PendingWork_WithRecentProgress_StaysHealthy()
    {
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 2, ActiveMonitoredItems: 0, QueuedHistoryReads: 0, LastProgressUtc: Now.AddSeconds(-30));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Healthy);
    }

    /// <summary>Verifies that pending work with stale progress is detected as Faulted.</summary>
    [Fact]
    public void PendingWork_WithStaleProgress_IsFaulted()
    {
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 2, ActiveMonitoredItems: 0, QueuedHistoryReads: 0, LastProgressUtc: Now.AddMinutes(-5));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Faulted);
    }

    /// <summary>Verifies that active monitored items without recent publishes are detected as Faulted.</summary>
    [Fact]
    public void MonitoredItems_Active_ButNoRecentPublish_IsFaulted()
    {
        // Subscription-only driver with live MonitoredItems but no publish progress within threshold
        // is a real wedge — this is the case the previous "no successful Read" formulation used
        // to miss (no reads ever happen).
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 0, ActiveMonitoredItems: 5, QueuedHistoryReads: 0, LastProgressUtc: Now.AddMinutes(-10));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Faulted);
    }

    /// <summary>Verifies that active monitored items with fresh publishes stay Healthy.</summary>
    [Fact]
    public void MonitoredItems_Active_WithFreshPublish_StaysHealthy()
    {
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 0, ActiveMonitoredItems: 5, QueuedHistoryReads: 0, LastProgressUtc: Now.AddSeconds(-10));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Healthy);
    }

    /// <summary>Verifies that slow history backfill with progress stays Healthy.</summary>
    [Fact]
    public void HistoryBackfill_SlowButMakingProgress_StaysHealthy()
    {
        // Slow historian backfill — QueuedHistoryReads > 0 but progress advances within threshold.
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(BulkheadDepth: 0, ActiveMonitoredItems: 0, QueuedHistoryReads: 50, LastProgressUtc: Now.AddSeconds(-60));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Healthy);
    }

    /// <summary>Verifies that write-only burst stays Idle when the bulkhead is empty.</summary>
    [Fact]
    public void WriteOnlyBurst_StaysIdle_WhenBulkheadEmpty()
    {
        // A write-only driver that just finished a burst: bulkhead drained, no subscriptions, no
        // history reads. Idle — the previous formulation would have faulted here because no
        // reads were succeeding even though the driver is perfectly healthy.
        var detector = new WedgeDetector(Threshold);
        var demand = new DemandSignal(0, 0, 0, Now.AddMinutes(-30));

        detector.Classify(DriverState.Healthy, demand, Now).ShouldBe(WedgeVerdict.Idle);
    }

    /// <summary>Verifies that DemandSignal.HasPendingWork is true for any non-zero counter.</summary>
    [Fact]
    public void DemandSignal_HasPendingWork_TrueForAnyNonZeroCounter()
    {
        new DemandSignal(1, 0, 0, Now).HasPendingWork.ShouldBeTrue();
        new DemandSignal(0, 1, 0, Now).HasPendingWork.ShouldBeTrue();
        new DemandSignal(0, 0, 1, Now).HasPendingWork.ShouldBeTrue();
        new DemandSignal(0, 0, 0, Now).HasPendingWork.ShouldBeFalse();
    }

    /// <summary>
    ///     Core-012 regression: the XML <c>&lt;summary&gt;</c> on the <see cref="WedgeDetector"/>
    ///     constructor must accurately describe what the constructor does (take + clamp the
    ///     threshold). The previous text — "Whether the driver reported itself Healthy at
    ///     construction" — referenced behaviour the constructor doesn't perform.
    /// </summary>
    [Fact]
    public void Doc_Constructor_Summary_Describes_Threshold_Clamp()
    {
        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 ctorSummary = ExtractSummaryFor(content,
            "M:ZB.MOM.WW.OtOpcUa.Core.Stability.WedgeDetector.#ctor(System.TimeSpan)");

        ctorSummary.ShouldNotBeNullOrWhiteSpace();
        ctorSummary.ShouldNotContain("reported itself");
        ctorSummary.ShouldContain("threshold");
    }

    private static string ExtractSummaryFor(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 sumStart = slice.IndexOf("<summary>", StringComparison.Ordinal);
        if (sumStart < 0) return string.Empty;
        var sumEnd = slice.IndexOf("</summary>", sumStart, StringComparison.Ordinal);
        return sumEnd < 0 ? string.Empty : slice.Substring(sumStart, sumEnd - sumStart);
    }
}
