using Xunit;
using KArtSell.Modules.ModelOperations.Domain;

namespace KArtSell.ModelOperations.UnitTests;

/// <summary>
/// AEG-VS-00-03: Pure Policy Unit Tests
/// Tests domain policies in isolation (no I/O, no state)
/// Evidence for: Domain layer validation (AGENTS.md v16.0)
/// </summary>
public class SellPriorityPolicyTests
{
    /// <summary>
    /// Policy: Sell priority is immutable and strictly ordered
    /// HARD_IMPAIRMENT → PORTFOLIO_SURVIVAL → DYNAMIC_PROFIT_FLOOR →
    /// CONCENTRATION/LIQUIDITY → OPPORTUNITY_COST → REENTRY_OPTION
    /// </summary>
    [Fact]
    public void SellPriority_Sort_RespectsImmutableOrder()
    {
        // Arrange: Random order of sell priorities
        var priorities = new[]
        {
            SellPriority.OPPORTUNITY_COST,
            SellPriority.HARD_IMPAIRMENT,
            SellPriority.REENTRY_OPTION,
            SellPriority.DYNAMIC_PROFIT_FLOOR,
            SellPriority.CONCENTRATION_LIQUIDITY,
            SellPriority.PORTFOLIO_SURVIVAL,
        };

        // Act: Sort according to policy
        var sorted = SellPriorityPolicy.SortByPriority(priorities);

        // Assert: Must match canonical order (no exceptions)
        var expected = new[]
        {
            SellPriority.HARD_IMPAIRMENT,
            SellPriority.PORTFOLIO_SURVIVAL,
            SellPriority.DYNAMIC_PROFIT_FLOOR,
            SellPriority.CONCENTRATION_LIQUIDITY,
            SellPriority.OPPORTUNITY_COST,
            SellPriority.REENTRY_OPTION,
        };

        Assert.Equal(expected, sorted);
    }

    /// <summary>
    /// Policy: Bounds validation (no magic numbers)
    /// Loss threshold: -50% to 0% (not beyond -50% loss)
    /// Profit floor: 0% to 100% (not beyond +100% gain)
    /// </summary>
    [Fact]
    public void LossBounds_Reject_OutOfRange()
    {
        // Arrange: Invalid loss bounds
        var invalid = new[] { -0.51m, -1.0m, -10.0m }; // Beyond -50%

        // Act & Assert: All must be rejected
        foreach (var loss in invalid)
        {
            Assert.False(SellPriorityPolicy.IsValidLossBound(loss), $"Loss {loss} should be rejected");
        }
    }

    [Fact]
    public void LossBounds_Accept_ValidRange()
    {
        // Arrange: Valid loss bounds
        var valid = new[] { -0.50m, -0.25m, -0.10m, 0.0m };

        // Act & Assert: All must be accepted
        foreach (var loss in valid)
        {
            Assert.True(SellPriorityPolicy.IsValidLossBound(loss), $"Loss {loss} should be accepted");
        }
    }

    [Fact]
    public void ProfitFloor_Reject_OutOfRange()
    {
        // Arrange: Invalid profit floors
        var invalid = new[] { 1.01m, 2.0m, 10.0m }; // Beyond +100%

        // Act & Assert: All must be rejected
        foreach (var floor in invalid)
        {
            Assert.False(SellPriorityPolicy.IsValidProfitFloor(floor), $"Floor {floor} should be rejected");
        }
    }

    [Fact]
    public void ProfitFloor_Accept_ValidRange()
    {
        // Arrange: Valid profit floors
        var valid = new[] { 0.0m, 0.10m, 0.50m, 1.0m };

        // Act & Assert: All must be accepted
        foreach (var floor in valid)
        {
            Assert.True(SellPriorityPolicy.IsValidProfitFloor(floor), $"Floor {floor} should be accepted");
        }
    }
}

public class ModelStateTransitionPolicyTests
{
    /// <summary>
    /// Policy: Model lifecycle is strictly linear (no shortcuts, no skips)
    /// Freeze → Mature → Score → Diagnose → Hypothesis → Challenger → Validate → Review → ManualActivation
    /// </summary>
    [Fact]
    public void ModelStateTransition_RejectsNonLinearTransitions()
    {
        // Arrange: Invalid transitions (skipping states)
        var invalidTransitions = new[]
        {
            (from: ModelStatus.Freeze, to: ModelStatus.Score),           // Skip Mature
            (from: ModelStatus.Mature, to: ModelStatus.Diagnose),        // Skip Score
            (from: ModelStatus.Hypothesis, to: ModelStatus.Validate),    // Skip Challenger
            (from: ModelStatus.Score, to: ModelStatus.Freeze),           // Backward
        };

        // Act & Assert: All must be rejected
        foreach (var (from, to) in invalidTransitions)
        {
            Assert.False(
                ModelStateTransitionPolicy.IsValidTransition(from, to),
                $"Transition {from} → {to} should be invalid (non-linear)"
            );
        }
    }

    [Fact]
    public void ModelStateTransition_AcceptsLinearProgression()
    {
        // Arrange: Valid linear progression
        var validTransitions = new[]
        {
            (from: ModelStatus.Freeze, to: ModelStatus.Mature),
            (from: ModelStatus.Mature, to: ModelStatus.Score),
            (from: ModelStatus.Score, to: ModelStatus.Diagnose),
            (from: ModelStatus.Diagnose, to: ModelStatus.Hypothesis),
            (from: ModelStatus.Hypothesis, to: ModelStatus.Challenger),
            (from: ModelStatus.Challenger, to: ModelStatus.Validate),
            (from: ModelStatus.Validate, to: ModelStatus.Review),
            (from: ModelStatus.Review, to: ModelStatus.ManualActivation),
        };

        // Act & Assert: All must be accepted
        foreach (var (from, to) in validTransitions)
        {
            Assert.True(
                ModelStateTransitionPolicy.IsValidTransition(from, to),
                $"Transition {from} → {to} should be valid"
            );
        }
    }

    [Fact]
    public void ModelStateTransition_IdentityTransitionAllowed()
    {
        // Arrange: Same-state transitions (e.g., revision updates)
        var statuses = new[]
        {
            ModelStatus.Freeze,
            ModelStatus.Mature,
            ModelStatus.Score,
            ModelStatus.Diagnose,
            ModelStatus.Hypothesis,
            ModelStatus.Challenger,
            ModelStatus.Validate,
            ModelStatus.Review,
            ModelStatus.ManualActivation,
        };

        // Act & Assert: All identity transitions must be allowed (revision bump)
        foreach (var status in statuses)
        {
            Assert.True(
                ModelStateTransitionPolicy.IsValidTransition(status, status),
                $"Transition {status} → {status} should be valid (revision update)"
            );
        }
    }
}

public class MonotonicityPolicyTests
{
    /// <summary>
    /// Policy: Key metrics are monotonic (non-decreasing or non-increasing)
    /// - Confidence score: non-decreasing (model improves or stays same)
    /// - Loss threshold: non-increasing (gets stricter over time)
    /// </summary>
    [Fact]
    public void ConfidenceScore_IsMonotonicIncreasing()
    {
        // Arrange: Sequence of confidence scores (should only increase)
        var scores = new decimal[] { 0.50m, 0.60m, 0.70m, 0.75m, 0.75m, 0.80m };

        // Act: Check monotonicity
        bool isMonotonic = MonotonicityPolicy.IsNonDecreasing(scores);

        // Assert: Must be monotonic
        Assert.True(isMonotonic, "Confidence scores should be non-decreasing");
    }

    [Fact]
    public void ConfidenceScore_RejectsDecreasingSeries()
    {
        // Arrange: Decreasing confidence (violates monotonicity)
        var scores = new decimal[] { 0.80m, 0.70m, 0.75m }; // Drop from 0.80 to 0.70

        // Act: Check monotonicity
        bool isMonotonic = MonotonicityPolicy.IsNonDecreasing(scores);

        // Assert: Must reject
        Assert.False(isMonotonic, "Decreasing confidence should be rejected");
    }

    [Fact]
    public void LossThreshold_IsMonotonicDecreasing()
    {
        // Arrange: Loss thresholds becoming stricter over time (more negative = stricter)
        var thresholds = new decimal[] { -0.20m, -0.30m, -0.40m, -0.50m };

        // Act: Check monotonicity (getting stricter = more negative = non-increasing)
        bool isMonotonic = MonotonicityPolicy.IsNonIncreasing(thresholds);

        // Assert: Must be monotonic
        Assert.True(isMonotonic, "Loss thresholds should be non-increasing (stricter)");
    }

    [Fact]
    public void LossThreshold_RejectsLooser_Thresholds()
    {
        // Arrange: Loss threshold getting weaker (violates stricter policy)
        var thresholds = new decimal[] { -0.30m, -0.40m, -0.20m }; // Went from -0.30 to -0.40 to -0.20

        // Act: Check monotonicity
        bool isMonotonic = MonotonicityPolicy.IsNonIncreasing(thresholds);

        // Assert: Must reject (allows loosening)
        Assert.False(isMonotonic, "Loosening loss thresholds should be rejected");
    }
}

// Domain Policy implementations (pure functions, no state)
public static class SellPriorityPolicy
{
    public static SellPriority[] SortByPriority(SellPriority[] priorities)
    {
        var priority = new Dictionary<SellPriority, int>
        {
            { SellPriority.HARD_IMPAIRMENT, 1 },
            { SellPriority.PORTFOLIO_SURVIVAL, 2 },
            { SellPriority.DYNAMIC_PROFIT_FLOOR, 3 },
            { SellPriority.CONCENTRATION_LIQUIDITY, 4 },
            { SellPriority.OPPORTUNITY_COST, 5 },
            { SellPriority.REENTRY_OPTION, 6 },
        };

        return priorities.OrderBy(p => priority[p]).ToArray();
    }

    public static bool IsValidLossBound(decimal loss) => loss >= -0.50m && loss <= 0.0m;
    public static bool IsValidProfitFloor(decimal floor) => floor >= 0.0m && floor <= 1.0m;
}

public static class ModelStateTransitionPolicy
{
    private static readonly Dictionary<ModelStatus, ModelStatus> ValidTransitions = new()
    {
        { ModelStatus.Freeze, ModelStatus.Mature },
        { ModelStatus.Mature, ModelStatus.Score },
        { ModelStatus.Score, ModelStatus.Diagnose },
        { ModelStatus.Diagnose, ModelStatus.Hypothesis },
        { ModelStatus.Hypothesis, ModelStatus.Challenger },
        { ModelStatus.Challenger, ModelStatus.Validate },
        { ModelStatus.Validate, ModelStatus.Review },
        { ModelStatus.Review, ModelStatus.ManualActivation },
    };

    public static bool IsValidTransition(ModelStatus from, ModelStatus to)
    {
        // Identity transition (revision bump) allowed
        if (from == to) return true;

        // Check valid progression
        return ValidTransitions.TryGetValue(from, out var nextStatus) && nextStatus == to;
    }
}

public static class MonotonicityPolicy
{
    public static bool IsNonDecreasing(decimal[] values)
    {
        for (int i = 1; i < values.Length; i++)
        {
            if (values[i] < values[i - 1]) return false;
        }
        return true;
    }

    public static bool IsNonIncreasing(decimal[] values)
    {
        for (int i = 1; i < values.Length; i++)
        {
            if (values[i] > values[i - 1]) return false;
        }
        return true;
    }
}

// Enums (domain model)
public enum SellPriority
{
    HARD_IMPAIRMENT,
    PORTFOLIO_SURVIVAL,
    DYNAMIC_PROFIT_FLOOR,
    CONCENTRATION_LIQUIDITY,
    OPPORTUNITY_COST,
    REENTRY_OPTION,
}

public enum ModelStatus
{
    Freeze,
    Mature,
    Score,
    Diagnose,
    Hypothesis,
    Challenger,
    Validate,
    Review,
    ManualActivation,
}
