using ZB.MOM.WW.Overview.Polling;

namespace ZB.MOM.WW.Overview.Tests;

/// <summary>
/// Status derivation and two-strike flap damping. These rules decide what an operator sees, so
/// every branch is pinned: the difference between Down and Unreachable sends someone to a
/// different place entirely, and damping that fires the wrong way either hides a real outage or
/// cries wolf on every dropped packet.
/// </summary>
public class StatusDerivationTests
{
    private static HealthProbeResult Reachable(string reportStatus, int statusCode) =>
        new(true, statusCode, new ZbHealthReport { Status = reportStatus }, TimeSpan.FromMilliseconds(5), null);

    private static HealthProbeResult Unreachable(string error) =>
        new(false, null, null, TimeSpan.FromMilliseconds(5), error);

    [Theory]
    [InlineData("Healthy", 200, InstanceStatus.Up)]
    [InlineData("Degraded", 200, InstanceStatus.Degraded)]
    [InlineData("Unhealthy", 503, InstanceStatus.Down)]
    public void Observe_MapsCanonicalStatuses(string reportStatus, int code, InstanceStatus expected)
    {
        Assert.Equal(expected, StatusDerivation.Observe(Reachable(reportStatus, code)));
    }

    [Fact]
    public void Observe_TransportFailure_IsUnreachable_NotDown()
    {
        // The distinction is the whole point: Down means "the app answered and said it is sick",
        // Unreachable means "nothing answered" — usually VPN, firewall or a registry typo.
        Assert.Equal(InstanceStatus.Unreachable, StatusDerivation.Observe(Unreachable("Connection refused")));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("Weird")]
    public void Observe_UnrecognisedStatus_IsUnreachable(string? reportStatus)
    {
        // Parsed as JSON but not this contract — something else is on that port.
        Assert.Equal(InstanceStatus.Unreachable, StatusDerivation.Observe(Reachable(reportStatus!, 200)));
    }

    [Theory]
    [InlineData(false, 200, ActiveState.NotApplicable)]
    [InlineData(false, 503, ActiveState.NotApplicable)]
    public void ObserveActive_WithoutActiveRole_IsNotApplicable(bool hasRole, int code, ActiveState expected)
    {
        Assert.Equal(expected, StatusDerivation.ObserveActive(hasRole, Reachable("Healthy", code)));
    }

    [Theory]
    [InlineData(200, ActiveState.Active)]
    [InlineData(503, ActiveState.Standby)]
    [InlineData(404, ActiveState.Unknown)]
    public void ObserveActive_MapsStatusCodes(int code, ActiveState expected)
    {
        Assert.Equal(expected, StatusDerivation.ObserveActive(true, Reachable("Healthy", code)));
    }

    [Fact]
    public void ObserveActive_NoAnswer_IsUnknown_NotStandby()
    {
        // Guessing Standby on a failed probe would let a pair render with zero Active nodes during
        // a blip, or — worse, if guessed the other way — with two.
        Assert.Equal(ActiveState.Unknown, StatusDerivation.ObserveActive(true, Unreachable("timed out")));
        Assert.Equal(ActiveState.Unknown, StatusDerivation.ObserveActive(true, probe: null));
    }

    [Fact]
    public void Damp_FirstFailureFromGoodState_HoldsThePreviousStatus()
    {
        var (status, failures) = StatusDerivation.Damp(InstanceStatus.Up, 0, InstanceStatus.Unreachable);

        Assert.Equal(InstanceStatus.Up, status);
        Assert.Equal(1, failures);
    }

    [Fact]
    public void Damp_SecondConsecutiveFailure_Flips()
    {
        var (status, failures) = StatusDerivation.Damp(InstanceStatus.Up, 1, InstanceStatus.Unreachable);

        Assert.Equal(InstanceStatus.Unreachable, status);
        Assert.Equal(2, failures);
    }

    [Fact]
    public void Damp_RecoveryIsImmediate_EvenAfterManyFailures()
    {
        var (status, failures) = StatusDerivation.Damp(InstanceStatus.Down, 27, InstanceStatus.Up);

        Assert.Equal(InstanceStatus.Up, status);
        Assert.Equal(0, failures);
    }

    [Fact]
    public void Damp_FirstEverPollThatFails_IsAdoptedImmediately()
    {
        // No previous state means nothing to protect and nothing better to show. Holding "Up" here
        // would be inventing a healthy reading that was never observed.
        var (status, failures) = StatusDerivation.Damp(previous: null, 0, InstanceStatus.Unreachable);

        Assert.Equal(InstanceStatus.Unreachable, status);
        Assert.Equal(1, failures);
    }

    [Fact]
    public void Damp_AlternatingFailures_NeverFlip()
    {
        // The flapping link this exists for: fail, recover, fail, recover. The counter must reset
        // on every good poll, so the card stays Up instead of oscillating.
        InstanceStatus? status = InstanceStatus.Up;
        var failures = 0;

        foreach (var observed in new[]
                 {
                     InstanceStatus.Unreachable, InstanceStatus.Up,
                     InstanceStatus.Unreachable, InstanceStatus.Up,
                     InstanceStatus.Unreachable, InstanceStatus.Up,
                 })
        {
            (var next, failures) = StatusDerivation.Damp(status, failures, observed);
            status = next;
        }

        Assert.Equal(InstanceStatus.Up, status);
        Assert.Equal(0, failures);
    }

    [Fact]
    public void Damp_DegradedIsNotAFailure_AndIsAdoptedImmediately()
    {
        // Degraded is a real answer from a live app, not a failed probe: it must show at once, and
        // must not accumulate strikes toward Down.
        var (status, failures) = StatusDerivation.Damp(InstanceStatus.Up, 1, InstanceStatus.Degraded);

        Assert.Equal(InstanceStatus.Degraded, status);
        Assert.Equal(0, failures);
    }

    [Fact]
    public void Damp_TwoDifferentFailureKindsInARow_StillFlips()
    {
        // Down then Unreachable is two consecutive failures, not one of each kind restarting the
        // count — the instance is not answering healthily either way.
        var (first, failures) = StatusDerivation.Damp(InstanceStatus.Up, 0, InstanceStatus.Down);
        Assert.Equal(InstanceStatus.Up, first);

        var (second, _) = StatusDerivation.Damp(first, failures, InstanceStatus.Unreachable);
        Assert.Equal(InstanceStatus.Unreachable, second);
    }
}
