"""Edge case tests for the grading system.

Verifies structural guarantees:
- Score never negative
- Score floor at 15 for non-critical paths
- Penalty stacking doesn't produce unreasonable results
- Handles degenerate inputs gracefully
"""

from __future__ import annotations

import numpy as np
import pytest

from orbit.analyzer.success_predictor import DatasetReadinessGrader


def _make_signal_health(n_dead=0, n_total=14, n_clipping=0):
    return {
        "dead_dimensions": list(range(n_dead)),
        "total_joints": n_total,
        "blockers": [],
        "warnings": [],
        "clipping_joints": list(range(n_clipping)),
    }


def _make_episode_ranking(total=50, remove=0, review=0):
    return {"total": total, "remove_count": remove, "review_count": review}


class TestScoreFloor:
    """Score should never go below 15 via penalty accumulation."""

    def test_worst_case_penalties(self):
        """Even with every penalty triggered, score >= 15."""
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=20.0,
            episode_consistency=0.10,
            signal_health=_make_signal_health(n_dead=2, n_total=14, n_clipping=8),
            policy_fit_score=0.30,
            episode_ranking=_make_episode_ranking(total=50, remove=15, review=30),
            action_divergence=0.50,
            episode_count=30,
            policy_type="act",
        )
        assert result.score >= 15
        assert result.grade in {"A", "B", "C", "D", "F"}

    def test_score_never_negative(self):
        """Score is always >= 0 regardless of input."""
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=0.0,
            episode_consistency=0.0,
            signal_health=_make_signal_health(n_dead=5, n_total=14, n_clipping=10),
            policy_fit_score=0.0,
            episode_ranking=_make_episode_ranking(total=50, remove=25, review=25),
            action_divergence=0.90,
            episode_count=30,
            policy_type="act",
        )
        assert result.score >= 0


class TestCriticalFailures:
    """Critical failures produce F grades but with appropriate scores."""

    def test_too_many_dead_joints(self):
        """Majority dead joints (>60%) → F(0)."""
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=80.0,
            episode_consistency=0.90,
            signal_health=_make_signal_health(n_dead=5, n_total=7),
            policy_fit_score=0.90,
            episode_ranking=_make_episode_ranking(),
            action_divergence=0.05,
            episode_count=50,
            policy_type="act",
        )
        assert result.grade == "F"
        assert result.score == 0

    def test_minority_dead_joints_not_critical(self):
        """3/7 dead joints (43%) should NOT be critical failure."""
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=80.0,
            episode_consistency=0.90,
            signal_health=_make_signal_health(n_dead=3, n_total=7),
            policy_fit_score=0.90,
            episode_ranking=_make_episode_ranking(),
            action_divergence=0.05,
            episode_count=50,
            policy_type="act",
        )
        # Should get a penalty but NOT critical failure
        assert result.grade != "F" or result.score > 0
        assert result.score >= 15

    def test_too_few_episodes(self):
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=80.0,
            episode_consistency=0.90,
            signal_health=_make_signal_health(),
            policy_fit_score=0.90,
            episode_ranking=_make_episode_ranking(total=5),
            action_divergence=0.05,
            episode_count=5,
            policy_type="act",
        )
        assert result.grade == "F"


class TestDiminishingReturns:
    """Penalty stacking should produce reasonable results."""

    def test_many_moderate_problems(self):
        """Dataset with many moderate issues should get D, not F."""
        grader = DatasetReadinessGrader()
        result = grader.grade(
            quality_score=50.0,
            episode_consistency=0.30,
            signal_health=_make_signal_health(n_dead=1, n_clipping=4),
            policy_fit_score=0.55,
            episode_ranking=_make_episode_ranking(total=50, remove=8, review=10),
            action_divergence=0.35,
            episode_count=30,
            policy_type="act",
        )
        # Many problems but each is moderate — should be D, not F
        assert result.score >= 15
        assert result.grade in {"C", "D"}

    def test_single_severe_problem(self):
        """Single severe problem should cost less than many moderate ones."""
        grader = DatasetReadinessGrader()

        # Single problem: very high divergence
        single = grader.grade(
            quality_score=80.0,
            episode_consistency=0.80,
            signal_health=_make_signal_health(),
            policy_fit_score=0.85,
            episode_ranking=_make_episode_ranking(),
            action_divergence=0.50,
            episode_count=50,
            policy_type="act",
        )

        # Many problems: moderate divergence + moderate everything
        many = grader.grade(
            quality_score=50.0,
            episode_consistency=0.30,
            signal_health=_make_signal_health(n_dead=2, n_clipping=4),
            policy_fit_score=0.55,
            episode_ranking=_make_episode_ranking(total=50, remove=8),
            action_divergence=0.25,
            episode_count=30,
            policy_type="act",
        )

        # Many moderate problems should score lower than single severe
        assert many.score <= single.score


class TestDivergencePenaltyContinuous:
    """Divergence penalty should scale smoothly, not have cliffs."""

    def test_monotonically_increasing(self):
        """Higher divergence should always give same or lower score."""
        grader = DatasetReadinessGrader()
        scores = []
        for div in [0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40]:
            result = grader.grade(
                quality_score=90.0,
                episode_consistency=0.90,
                signal_health=_make_signal_health(),
                policy_fit_score=0.90,
                episode_ranking=_make_episode_ranking(),
                action_divergence=div,
                episode_count=100,
                policy_type="act",
            )
            scores.append(result.score)

        for i in range(1, len(scores)):
            assert scores[i] <= scores[i - 1], (
                f"Score increased from div={0.05*i:.2f} ({scores[i-1]}) "
                f"to div={0.05*(i+1):.2f} ({scores[i]})"
            )

    def test_no_large_jumps(self):
        """Adjacent divergence values should not differ by more than 4 points."""
        grader = DatasetReadinessGrader()
        prev_score = None
        for div_x10 in range(0, 45):  # 0.00 to 0.44 in steps of 0.01
            div = div_x10 / 100.0
            result = grader.grade(
                quality_score=90.0,
                episode_consistency=0.90,
                signal_health=_make_signal_health(),
                policy_fit_score=0.90,
                episode_ranking=_make_episode_ranking(),
                action_divergence=div,
                episode_count=100,
                policy_type="act",
            )
            if prev_score is not None:
                jump = abs(result.score - prev_score)
                assert jump <= 4, (
                    f"Jump of {jump} points at div={div:.2f} "
                    f"(prev={prev_score}, curr={result.score})"
                )
            prev_score = result.score


class TestGradeValidity:
    """All outputs should be valid grade/score combinations."""

    def test_grade_score_consistency(self):
        """Grade letter must match score range."""
        grader = DatasetReadinessGrader()
        for div in [0.0, 0.15, 0.30, 0.50]:
            for cons in [0.2, 0.5, 0.9]:
                result = grader.grade(
                    quality_score=70.0,
                    episode_consistency=cons,
                    signal_health=_make_signal_health(),
                    policy_fit_score=0.80,
                    episode_ranking=_make_episode_ranking(),
                    action_divergence=div,
                    episode_count=50,
                    policy_type="act",
                )
                if result.score >= 85:
                    assert result.grade == "A"
                elif result.score >= 72:
                    assert result.grade == "B"
                elif result.score >= 58:
                    assert result.grade == "C"
                elif result.score >= 40:
                    assert result.grade == "D"
                else:
                    assert result.grade == "F"
