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

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

[Trait("Category", "Unit")]
public sealed class DriverResilienceOptionsParserTests
{
    /// <summary>Verifies that null JSON returns pure tier defaults.</summary>
    [Fact]
    public void NullJson_ReturnsPureTierDefaults()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, null, out var diag);

        diag.ShouldBeNull();
        options.Tier.ShouldBe(DriverTier.A);
        options.Resolve(DriverCapability.Read).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
    }

    /// <summary>Verifies that whitespace JSON returns defaults.</summary>
    [Fact]
    public void WhitespaceJson_ReturnsDefaults()
    {
        DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, "   ", out var diag);
        diag.ShouldBeNull();
    }

    /// <summary>Verifies that malformed JSON falls back with diagnostic.</summary>
    [Fact]
    public void MalformedJson_FallsBack_WithDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, "{not json", out var diag);

        diag.ShouldNotBeNull();
        diag.ShouldContain("malformed");
        options.Tier.ShouldBe(DriverTier.A);
        options.Resolve(DriverCapability.Read).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
    }

    /// <summary>Verifies that empty object returns defaults.</summary>
    [Fact]
    public void EmptyObject_ReturnsDefaults()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, "{}", out var diag);

        diag.ShouldBeNull();
        options.Resolve(DriverCapability.Write).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
    }

    /// <summary>Verifies that Read override is merged into tier defaults.</summary>
    [Fact]
    public void ReadOverride_MergedIntoTierDefaults()
    {
        var json = """
            {
              "capabilityPolicies": {
                "Read": { "timeoutSeconds": 5, "retryCount": 7, "breakerFailureThreshold": 2 }
              }
            }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, json, out var diag);

        diag.ShouldBeNull();
        var read = options.Resolve(DriverCapability.Read);
        read.TimeoutSeconds.ShouldBe(5);
        read.RetryCount.ShouldBe(7);
        read.BreakerFailureThreshold.ShouldBe(2);

        // Other capabilities untouched
        options.Resolve(DriverCapability.Write).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
    }

    /// <summary>Verifies that partial policy fills missing fields from tier default.</summary>
    [Fact]
    public void PartialPolicy_FillsMissingFieldsFromTierDefault()
    {
        var json = """
            {
              "capabilityPolicies": {
                "Read": { "retryCount": 10 }
              }
            }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, json, out _);

        var read = options.Resolve(DriverCapability.Read);
        var tierDefault = DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read];
        read.RetryCount.ShouldBe(10);
        read.TimeoutSeconds.ShouldBe(tierDefault.TimeoutSeconds, "partial override; timeout falls back to tier default");
        read.BreakerFailureThreshold.ShouldBe(tierDefault.BreakerFailureThreshold);
    }

    /// <summary>
    ///     01/U-6: the deleted bulkhead knob's keys in stored JSON become ignored unknown keys — a
    ///     config carrying them parses clean with no diagnostic (migration-free contract).
    /// </summary>
    [Fact]
    public void BulkheadKeys_InStoredJson_AreIgnored()
    {
        var json = """
            { "bulkheadMaxConcurrent": 100, "bulkheadMaxQueue": 500 }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, json, out var diag);

        diag.ShouldBeNull();
        // Known capabilities untouched — the stale bulkhead keys are simply ignored.
        options.Resolve(DriverCapability.Read).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.B)[DriverCapability.Read]);
    }

    /// <summary>Verifies that unknown capability surfaces in diagnostic but does not fail.</summary>
    [Fact]
    public void UnknownCapability_Surfaces_InDiagnostic_ButDoesNotFail()
    {
        var json = """
            {
              "capabilityPolicies": {
                "InventedCapability": { "timeoutSeconds": 99 }
              }
            }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, json, out var diag);

        diag.ShouldNotBeNull();
        diag.ShouldContain("InventedCapability");
        // Known capabilities untouched.
        options.Resolve(DriverCapability.Read).ShouldBe(
            DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
    }

    /// <summary>Verifies that top-level property names are case insensitive.</summary>
    [Fact]
    public void PropertyNames_AreCaseInsensitive()
    {
        var json = """
            { "RECYCLEINTERVALSECONDS": 3600 }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.C, json, out var diag);

        diag.ShouldBeNull();
        options.RecycleIntervalSeconds.ShouldBe(3600);
    }

    /// <summary>Verifies that capability name is case insensitive.</summary>
    [Fact]
    public void CapabilityName_IsCaseInsensitive()
    {
        var json = """
            { "capabilityPolicies": { "read": { "retryCount": 99 } } }
            """;

        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, json, out var diag);

        diag.ShouldBeNull();
        options.Resolve(DriverCapability.Read).RetryCount.ShouldBe(99);
    }

    /// <summary>Verifies that every tier with empty JSON round-trips its defaults.</summary>
    /// <param name="tier">The driver tier to test.</param>
    [Theory]
    [InlineData(DriverTier.A)]
    [InlineData(DriverTier.B)]
    [InlineData(DriverTier.C)]
    public void EveryTier_WithEmptyJson_RoundTrips_Its_Defaults(DriverTier tier)
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(tier, "{}", out var diag);

        diag.ShouldBeNull();
        options.Tier.ShouldBe(tier);
        foreach (var cap in Enum.GetValues<DriverCapability>())
            options.Resolve(cap).ShouldBe(DriverResilienceOptions.GetTierDefaults(tier)[cap]);
    }

    /// <summary>Verifies that RecycleIntervalSeconds on Tier C with positive value parses and surfaces.</summary>
    [Fact]
    public void RecycleIntervalSeconds_TierC_PositiveValue_ParsesAndSurfaces()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.C, "{\"recycleIntervalSeconds\":3600}", out var diag);

        diag.ShouldBeNull();
        options.RecycleIntervalSeconds.ShouldBe(3600);
    }

    /// <summary>Verifies that RecycleIntervalSeconds when null defaults to null.</summary>
    [Fact]
    public void RecycleIntervalSeconds_Null_DefaultsToNull()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.C, "{}", out _);
        options.RecycleIntervalSeconds.ShouldBeNull();
    }

    /// <summary>Verifies that RecycleIntervalSeconds on Tier A or B is rejected with diagnostic.</summary>
    /// <param name="tier">The driver tier to test.</param>
    [Theory]
    [InlineData(DriverTier.A)]
    [InlineData(DriverTier.B)]
    public void RecycleIntervalSeconds_OnTierAorB_Rejected_With_Diagnostic(DriverTier tier)
    {
        // Decision #74 — in-process drivers must not scheduled-recycle because it would
        // tear down every OPC UA session. The parser surfaces a diagnostic rather than
        // silently honouring the value.
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            tier, "{\"recycleIntervalSeconds\":3600}", out var diag);

        options.RecycleIntervalSeconds.ShouldBeNull();
        diag.ShouldContain("Tier C only");
    }

    /// <summary>Verifies that RecycleIntervalSeconds with non-positive value is rejected with diagnostic.</summary>
    [Fact]
    public void RecycleIntervalSeconds_NonPositive_Rejected_With_Diagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.C, "{\"recycleIntervalSeconds\":0}", out var diag);

        options.RecycleIntervalSeconds.ShouldBeNull();
        diag.ShouldContain("must be positive");
    }

    // ---- 01/S-6: range clamps (clamp + diagnostic, never brick) ----

    /// <summary>A non-positive timeout is nonsense; it snaps to the capability's tier default with a diagnostic.</summary>
    [Fact]
    public void TimeoutSeconds_Zero_FallsBackToTierDefault_WithDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"timeoutSeconds\":0}}}", out var diag);

        var tierDefault = DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read];
        options.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(tierDefault.TimeoutSeconds);
        diag.ShouldNotBeNull();
        diag.ShouldContain("timeoutSeconds");
    }

    /// <summary>A timeout above Polly's ceiling clamps to the max with a diagnostic.</summary>
    [Fact]
    public void TimeoutSeconds_AbovePollyMax_Clamped()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"timeoutSeconds\":999999}}}", out var diag);

        options.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(ResiliencePolicyRanges.MaxTimeoutSeconds);
        diag.ShouldNotBeNull();
        diag.ShouldContain("timeoutSeconds");
    }

    /// <summary>A negative retry count clamps to zero with a diagnostic.</summary>
    [Fact]
    public void RetryCount_Negative_ClampedToZero()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":-1}}}", out var diag);

        options.Resolve(DriverCapability.Read).RetryCount.ShouldBe(0);
        diag.ShouldNotBeNull();
        diag.ShouldContain("retryCount");
    }

    /// <summary>An absurd retry count clamps to the operational cap with a diagnostic.</summary>
    [Fact]
    public void RetryCount_AboveCap_ClampedTo100()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":2000000000}}}", out var diag);

        options.Resolve(DriverCapability.Read).RetryCount.ShouldBe(ResiliencePolicyRanges.MaxRetryCount);
        diag.ShouldNotBeNull();
        diag.ShouldContain("retryCount");
    }

    /// <summary>A breaker threshold of 1 (below Polly's MinimumThroughput floor) raises to 2 with a diagnostic.</summary>
    [Fact]
    public void BreakerThreshold_One_ClampedToTwo()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"breakerFailureThreshold\":1}}}", out var diag);

        options.Resolve(DriverCapability.Read).BreakerFailureThreshold.ShouldBe(
            ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold);
        diag.ShouldNotBeNull();
        diag.ShouldContain("breakerFailureThreshold");
    }

    /// <summary>A negative breaker threshold disables the breaker (0) with a diagnostic.</summary>
    [Fact]
    public void BreakerThreshold_Negative_DisablesBreaker()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"breakerFailureThreshold\":-5}}}", out var diag);

        options.Resolve(DriverCapability.Read).BreakerFailureThreshold.ShouldBe(0);
        diag.ShouldNotBeNull();
        diag.ShouldContain("breakerFailureThreshold");
    }

    /// <summary>Multiple clamps in one policy all surface in the (appended) diagnostic string.</summary>
    [Fact]
    public void MultipleClamps_AllSurfaceInDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A,
            "{\"capabilityPolicies\":{\"Read\":{\"timeoutSeconds\":0,\"retryCount\":-1,\"breakerFailureThreshold\":1}}}",
            out var diag);

        diag.ShouldNotBeNull();
        diag.ShouldContain("timeoutSeconds");
        diag.ShouldContain("retryCount");
        diag.ShouldContain("breakerFailureThreshold");

        var read = options.Resolve(DriverCapability.Read);
        read.TimeoutSeconds.ShouldBe(DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read].TimeoutSeconds);
        read.RetryCount.ShouldBe(0);
        read.BreakerFailureThreshold.ShouldBe(ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold);
    }

    // ---- 01/S-8 = 03/S12: write-shaped capabilities never retry ----

    /// <summary>A Write retryCount override is forced to 0 with a diagnostic (writes are non-idempotent).</summary>
    [Fact]
    public void WriteRetryOverride_IsForcedToZero_WithDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Write\":{\"retryCount\":5}}}", out var diag);

        options.Resolve(DriverCapability.Write).RetryCount.ShouldBe(0);
        diag.ShouldNotBeNull();
        diag.ShouldContain("Write never retries");
    }

    /// <summary>An AlarmAcknowledge retryCount override is forced to 0 with a diagnostic.</summary>
    [Fact]
    public void AlarmAcknowledgeRetryOverride_IsForcedToZero_WithDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"AlarmAcknowledge\":{\"retryCount\":3}}}", out var diag);

        options.Resolve(DriverCapability.AlarmAcknowledge).RetryCount.ShouldBe(0);
        diag.ShouldNotBeNull();
        diag.ShouldContain("AlarmAcknowledge never retries");
    }

    /// <summary>A Read (idempotent) retryCount override is still honored — the invariant is write-shaped only.</summary>
    [Fact]
    public void ReadRetryOverride_StillHonored()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A, "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":5}}}", out var diag);

        options.Resolve(DriverCapability.Read).RetryCount.ShouldBe(5);
        diag.ShouldBeNull();
    }

    /// <summary>In-range overrides pass through untouched with no diagnostic.</summary>
    [Fact]
    public void InRangeOverrides_PassThrough_NoDiagnostic()
    {
        var options = DriverResilienceOptionsParser.ParseOrDefaults(
            DriverTier.A,
            "{\"capabilityPolicies\":{\"Read\":{\"timeoutSeconds\":5,\"retryCount\":3,\"breakerFailureThreshold\":4}}}",
            out var diag);

        diag.ShouldBeNull();
        var read = options.Resolve(DriverCapability.Read);
        read.TimeoutSeconds.ShouldBe(5);
        read.RetryCount.ShouldBe(3);
        read.BreakerFailureThreshold.ShouldBe(4);
    }
}
