"""tests/test_phase3_adaptive_grading.py — Phase 3: Adaptive Grading TDD tests.

Blind spots: BS-07 (Hardcoded thresholds), BS-08 (Grade inflation risk),
             BS-15 (Flat grade distribution).

WATERFALL_PLAN §7 specification:
  - GradeInput dataclass with 6 fields
  - adaptive_score_to_grade() — three-factor weighted model
  - Path-adjusted thresholds: P1/P3 (0.75/0.50), P2 (0.82/0.60), P4 (0.85/0.65)
  - Weights: composite 0.50, kavaid 0.25, evidence_quality 0.25
  - AX56 ceiling: max grade = İlmelyakîn, NEVER Hakkalyakîn
  - Legacy score_to_grade() unchanged

Test map:
  Unit tests                    Empirical tests
  ──────────────────────────    ──────────────────────────────
  3.01  Legacy unchanged        3.E1  ≥2 distinct grades (Gate 3)
  3.02  High evidence → İlm     3.E2  Monotonicity: composite
  3.03  Low evidence → Tasdik   3.E3  Monotonicity: kavaid
  3.04  P2 harder than P1       3.E4  AX56 across 10 inputs
  3.05  P4 hardest              3.E5  Path ordering
  3.06  Kavaid affects grade
  3.07  Structured ratio bonus  Edge cases
  3.08  Never Hakkalyakîn       ──────────────────────────────
  3.09  dusun() integration     3.EC1 All-zero → Tasavvur
                                3.EC2 P0 → valid grade
                                3.EC3 Boundary values
"""

from __future__ import annotations

import unittest

import pytest

from bileshke.types import EpistemicGrade

# ── Legacy import (MUST work even in RED phase) ──
from fw_server.grade_map import score_to_grade, grade_to_string


# ═══════════════════════════════════════════════════════════════════════
#  UNIT TESTS  (3.01 – 3.09)
# ═══════════════════════════════════════════════════════════════════════


class TestLegacyUnchanged(unittest.TestCase):
    """3.01 — score_to_grade() returns identical results to v2.3.0."""

    def test_ilmelyakin_threshold(self):
        self.assertEqual(score_to_grade(0.92), EpistemicGrade.ILMELYAKIN)
        self.assertEqual(score_to_grade(0.86), EpistemicGrade.ILMELYAKIN)

    def test_tasdik_threshold(self):
        self.assertEqual(score_to_grade(0.85), EpistemicGrade.TASDIK)
        self.assertEqual(score_to_grade(0.70), EpistemicGrade.TASDIK)
        self.assertEqual(score_to_grade(0.61), EpistemicGrade.TASDIK)

    def test_tasavvur_threshold(self):
        self.assertEqual(score_to_grade(0.60), EpistemicGrade.TASAVVUR)
        self.assertEqual(score_to_grade(0.30), EpistemicGrade.TASAVVUR)
        self.assertEqual(score_to_grade(0.00), EpistemicGrade.TASAVVUR)

    def test_edge_clamp(self):
        self.assertEqual(score_to_grade(0.9999), EpistemicGrade.ILMELYAKIN)
        self.assertEqual(score_to_grade(1.5), EpistemicGrade.ILMELYAKIN)
        self.assertEqual(score_to_grade(-0.1), EpistemicGrade.TASAVVUR)

    def test_grade_to_string_unchanged(self):
        self.assertEqual(grade_to_string(EpistemicGrade.ILMELYAKIN), "İlmelyakîn")
        self.assertEqual(grade_to_string(EpistemicGrade.TASDIK), "Tasdik")
        self.assertEqual(grade_to_string(EpistemicGrade.TASAVVUR), "Tasavvur")


class TestAdaptiveHighEvidence(unittest.TestCase):
    """3.02 — High composite + high evidence → İlmelyakîn."""

    def test_high_evidence_p1(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=0.90,
            kavaid_pass_ratio=1.0,
            structured_ratio=0.9,
            lens_agreement=0.9,
            evidence_density=4.0,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertEqual(grade, EpistemicGrade.ILMELYAKIN)


class TestAdaptiveLowEvidence(unittest.TestCase):
    """3.03 — High composite but low evidence → NOT İlmelyakîn."""

    def test_low_evidence_degrades_grade(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=0.88,
            kavaid_pass_ratio=0.625,  # 5/8 kavaid
            structured_ratio=0.0,
            lens_agreement=0.2,
            evidence_density=0.3,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertNotEqual(
            grade,
            EpistemicGrade.ILMELYAKIN,
            "High composite + low evidence should NOT reach İlmelyakîn",
        )
        # But should still be at least Tasavvur (not an error state)
        self.assertIn(
            grade,
            [EpistemicGrade.TASAVVUR, EpistemicGrade.TASDIK],
        )


class TestP2HigherThreshold(unittest.TestCase):
    """3.04 — Same score, P2 gets lower grade than P1."""

    def test_p2_harder_than_p1(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.85,
            kavaid_pass_ratio=0.875,  # 7/8
            structured_ratio=0.7,
            lens_agreement=0.75,
            evidence_density=2.5,
        )
        grade_p1 = adaptive_score_to_grade(GradeInput(path="P1", **base))
        grade_p2 = adaptive_score_to_grade(GradeInput(path="P2", **base))

        self.assertGreaterEqual(
            grade_p1.rank,
            grade_p2.rank,
            f"P2 should be harder: P1={grade_p1}, P2={grade_p2}",
        )
        # Specifically: at these values, P1 should reach İlm but P2 should not
        self.assertEqual(grade_p1, EpistemicGrade.ILMELYAKIN)
        self.assertNotEqual(grade_p2, EpistemicGrade.ILMELYAKIN)


class TestP4HighestThreshold(unittest.TestCase):
    """3.05 — P4 requires most evidence; grade ≤ P1 at same score."""

    def test_p4_hardest(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.85,
            kavaid_pass_ratio=0.875,
            structured_ratio=0.7,
            lens_agreement=0.75,
            evidence_density=2.5,
        )
        grade_p1 = adaptive_score_to_grade(GradeInput(path="P1", **base))
        grade_p4 = adaptive_score_to_grade(GradeInput(path="P4", **base))

        self.assertGreaterEqual(
            grade_p1.rank,
            grade_p4.rank,
            f"P4 should be ≤ P1: P4={grade_p4}, P1={grade_p1}",
        )


class TestKavaidComplianceAffectsGrade(unittest.TestCase):
    """3.06 — 8/8 KV pass → higher/equal grade than 5/8."""

    def test_full_kavaid_vs_partial(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.78,
            structured_ratio=0.5,
            lens_agreement=0.6,
            evidence_density=1.5,
            path="P1",
        )
        grade_full = adaptive_score_to_grade(
            GradeInput(kavaid_pass_ratio=1.0, **base)
        )
        grade_partial = adaptive_score_to_grade(
            GradeInput(kavaid_pass_ratio=0.625, **base)
        )

        self.assertGreaterEqual(
            grade_full.rank,
            grade_partial.rank,
            f"Full kavaid should be ≥ partial: full={grade_full}, partial={grade_partial}",
        )


class TestStructuredRatioBonus(unittest.TestCase):
    """3.07 — More structured lenses → easier to reach higher grade."""

    def test_structured_vs_unstructured(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.78,
            kavaid_pass_ratio=0.875,
            lens_agreement=0.6,
            evidence_density=1.5,
            path="P1",
        )
        grade_high_sr = adaptive_score_to_grade(
            GradeInput(structured_ratio=0.9, **base)
        )
        grade_low_sr = adaptive_score_to_grade(
            GradeInput(structured_ratio=0.1, **base)
        )

        self.assertGreaterEqual(
            grade_high_sr.rank,
            grade_low_sr.rank,
            f"Higher structured_ratio should help: high={grade_high_sr}, low={grade_low_sr}",
        )


class TestGradeNeverExceedsIlmelyakin(unittest.TestCase):
    """3.08 — Perfect input → max İlmelyakîn; NEVER Hakkalyakîn (AX56)."""

    def test_perfect_input_all_paths(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        for path in ["P0", "P1", "P2", "P3", "P4"]:
            gi = GradeInput(
                composite_score=0.9999,
                kavaid_pass_ratio=1.0,
                structured_ratio=1.0,
                lens_agreement=1.0,
                evidence_density=10.0,
                path=path,
            )
            grade = adaptive_score_to_grade(gi)
            self.assertNotEqual(
                grade,
                EpistemicGrade.HAKKALYAKIN,
                f"AX56 violation on {path}: got Hakkalyakîn",
            )
            self.assertLessEqual(
                grade.rank,
                EpistemicGrade.ILMELYAKIN.rank,
                f"Grade exceeds İlmelyakîn on {path}: {grade}",
            )


class TestDusunUsesAdaptiveGrade(unittest.TestCase):
    """3.09 — dusun() result includes adaptive grading metadata."""

    def test_grade_input_in_result(self):
        from fw_server.grade_map import GradeInput  # noqa: F401
        from fw_server.dusun import dusun

        result = dusun(
            "The ontological structure demonstrates holographic architecture "
            "with constitutive dependency creating functional interfaces."
        )

        # Phase 3 adds grade_input to the result dict
        self.assertIn(
            "grade_input",
            result,
            "dusun() should include 'grade_input' dict after Phase 3 wiring",
        )
        gi = result["grade_input"]
        # Verify all GradeInput fields are present
        for field in [
            "composite_score",
            "kavaid_pass_ratio",
            "lens_agreement",
            "structured_ratio",
            "evidence_density",
            "path",
        ]:
            self.assertIn(field, gi, f"grade_input missing field: {field}")

        # Verify values are in expected ranges
        self.assertGreaterEqual(gi["composite_score"], 0.0)
        self.assertLess(gi["composite_score"], 1.0)
        self.assertGreaterEqual(gi["kavaid_pass_ratio"], 0.0)
        self.assertLessEqual(gi["kavaid_pass_ratio"], 1.0)
        self.assertIn(gi["path"], ["P0", "P1", "P2", "P3", "P4"])


# ═══════════════════════════════════════════════════════════════════════
#  EMPIRICAL TESTS  (3.E1 – 3.E5)
# ═══════════════════════════════════════════════════════════════════════


class TestEmpiricalGradeDistribution(unittest.TestCase):
    """3.E1 — Gate 3: ≥2 distinct grades across 5 diverse inputs."""

    def test_diverse_inputs_produce_multiple_grades(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        inputs = [
            GradeInput(0.95, 1.0, 0.9, 0.9, 4.0, "P1"),     # Top-tier
            GradeInput(0.78, 0.875, 0.6, 0.7, 2.0, "P1"),    # Upper-mid
            GradeInput(0.55, 0.625, 0.3, 0.4, 0.8, "P1"),    # Lower-mid
            GradeInput(0.30, 0.25, 0.1, 0.1, 0.2, "P1"),     # Low
            GradeInput(0.10, 0.125, 0.0, 0.1, 0.1, "P1"),    # Very low
        ]
        grades = {adaptive_score_to_grade(gi) for gi in inputs}
        self.assertGreaterEqual(
            len(grades),
            2,
            f"Gate 3: Grade distribution must have ≥2 distinct grades, got {grades}",
        )


_MONOTONICITY_COMPOSITE_INPUTS = [0.20, 0.40, 0.60, 0.80, 0.95]


class TestEmpiricalMonotonicityComposite(unittest.TestCase):
    """3.E2 — Higher composite (all else equal) → grade non-decreasing."""

    def test_monotonic_on_composite(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            kavaid_pass_ratio=0.75,
            structured_ratio=0.5,
            lens_agreement=0.6,
            evidence_density=1.5,
            path="P1",
        )
        composites = _MONOTONICITY_COMPOSITE_INPUTS
        grades = [
            adaptive_score_to_grade(GradeInput(composite_score=c, **base))
            for c in composites
        ]
        for i in range(len(grades) - 1):
            self.assertLessEqual(
                grades[i].rank,
                grades[i + 1].rank,
                f"Monotonicity: composite {composites[i]}→{grades[i]} "
                f"> {composites[i+1]}→{grades[i+1]}",
            )


_MONOTONICITY_KAVAID_INPUTS = [0.0, 0.25, 0.50, 0.75, 1.0]


class TestEmpiricalMonotonicityKavaid(unittest.TestCase):
    """3.E3 — Higher kavaid (all else equal) → grade non-decreasing."""

    def test_monotonic_on_kavaid(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.78,
            structured_ratio=0.5,
            lens_agreement=0.6,
            evidence_density=1.5,
            path="P1",
        )
        ratios = _MONOTONICITY_KAVAID_INPUTS
        grades = [
            adaptive_score_to_grade(GradeInput(kavaid_pass_ratio=r, **base))
            for r in ratios
        ]
        for i in range(len(grades) - 1):
            self.assertLessEqual(
                grades[i].rank,
                grades[i + 1].rank,
                f"Monotonicity: kavaid {ratios[i]}→{grades[i]} "
                f"> {ratios[i+1]}→{grades[i+1]}",
            )


# Parametrized AX56 sweep across diverse inputs
_AX56_SWEEP_PARAMS = [
    (0.90, 1.0, 0.9, 0.9, 4.0, "P1"),
    (0.60, 0.75, 0.5, 0.5, 1.0, "P1"),
    (0.30, 0.375, 0.1, 0.2, 0.3, "P1"),
    (0.90, 1.0, 0.9, 0.9, 4.0, "P4"),
    (0.60, 0.75, 0.5, 0.5, 1.0, "P4"),
    (0.30, 0.375, 0.1, 0.2, 0.3, "P4"),
    (0.85, 0.875, 0.7, 0.75, 2.5, "P2"),
    (0.75, 0.625, 0.3, 0.4, 0.8, "P3"),
    (0.50, 0.5, 0.2, 0.3, 0.5, "P1"),
    (0.9999, 1.0, 1.0, 1.0, 10.0, "P1"),
]


@pytest.mark.parametrize(
    "composite,kavaid,sr,la,ed,path",
    _AX56_SWEEP_PARAMS,
    ids=[
        "hi_p1",
        "mid_p1",
        "lo_p1",
        "hi_p4",
        "mid_p4",
        "lo_p4",
        "border_p2",
        "mid_p3",
        "below_p1",
        "perfect_p1",
    ],
)
def test_empirical_ax56_sweep(composite, kavaid, sr, la, ed, path):
    """3.E4 — AX56: No input EVER produces Hakkalyakîn."""
    from fw_server.grade_map import GradeInput, adaptive_score_to_grade

    gi = GradeInput(
        composite_score=composite,
        kavaid_pass_ratio=kavaid,
        structured_ratio=sr,
        lens_agreement=la,
        evidence_density=ed,
        path=path,
    )
    grade = adaptive_score_to_grade(gi)
    assert grade != EpistemicGrade.HAKKALYAKIN, (
        f"AX56 violation: Hakkalyakîn with {gi}"
    )
    assert grade.is_accessible, f"Grade must be accessible: {grade}"


class TestEmpiricalPathOrdering(unittest.TestCase):
    """3.E5 — At same inputs, P4 ≤ P2 ≤ P1/P3 in grade rank."""

    def test_path_ordering(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        base = dict(
            composite_score=0.80,
            kavaid_pass_ratio=0.875,
            structured_ratio=0.6,
            lens_agreement=0.7,
            evidence_density=2.0,
        )
        g_p1 = adaptive_score_to_grade(GradeInput(path="P1", **base))
        g_p2 = adaptive_score_to_grade(GradeInput(path="P2", **base))
        g_p3 = adaptive_score_to_grade(GradeInput(path="P3", **base))
        g_p4 = adaptive_score_to_grade(GradeInput(path="P4", **base))

        self.assertLessEqual(
            g_p4.rank, g_p2.rank, f"P4 ≤ P2: {g_p4} vs {g_p2}"
        )
        self.assertLessEqual(
            g_p2.rank, g_p1.rank, f"P2 ≤ P1: {g_p2} vs {g_p1}"
        )
        # P3 uses same thresholds as P1
        self.assertEqual(
            g_p3.rank, g_p1.rank, f"P3 == P1: {g_p3} vs {g_p1}"
        )


# ═══════════════════════════════════════════════════════════════════════
#  EDGE CASE TESTS  (3.EC1 – 3.EC3)
# ═══════════════════════════════════════════════════════════════════════


class TestEdgeCaseAllZero(unittest.TestCase):
    """3.EC1 — All signals at zero → Tasavvur."""

    def test_zero_input(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=0.0,
            kavaid_pass_ratio=0.0,
            structured_ratio=0.0,
            lens_agreement=0.0,
            evidence_density=0.0,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertEqual(grade, EpistemicGrade.TASAVVUR)


class TestEdgeCaseP0Path(unittest.TestCase):
    """3.EC2 — P0 path still returns a valid accessible grade."""

    def test_p0_returns_valid_grade(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=0.70,
            kavaid_pass_ratio=0.75,
            structured_ratio=0.5,
            lens_agreement=0.6,
            evidence_density=1.5,
            path="P0",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertIn(
            grade,
            [
                EpistemicGrade.TASAVVUR,
                EpistemicGrade.TASDIK,
                EpistemicGrade.ILMELYAKIN,
            ],
        )
        self.assertNotEqual(grade, EpistemicGrade.HAKKALYAKIN)


class TestEdgeCaseBoundaryValues(unittest.TestCase):
    """3.EC3 — Boundary values don't crash or produce unexpected grades."""

    def test_maximal_composite(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=0.9999,
            kavaid_pass_ratio=1.0,
            structured_ratio=1.0,
            lens_agreement=1.0,
            evidence_density=100.0,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertEqual(grade, EpistemicGrade.ILMELYAKIN)

    def test_negative_composite_clamped(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=-0.5,
            kavaid_pass_ratio=0.0,
            structured_ratio=0.0,
            lens_agreement=0.0,
            evidence_density=0.0,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        self.assertEqual(grade, EpistemicGrade.TASAVVUR)

    def test_over_one_composite_clamped(self):
        from fw_server.grade_map import GradeInput, adaptive_score_to_grade

        gi = GradeInput(
            composite_score=1.5,
            kavaid_pass_ratio=1.0,
            structured_ratio=1.0,
            lens_agreement=1.0,
            evidence_density=5.0,
            path="P1",
        )
        grade = adaptive_score_to_grade(gi)
        # Even with clamped score, max is İlmelyakîn
        self.assertLessEqual(grade.rank, EpistemicGrade.ILMELYAKIN.rank)
        self.assertNotEqual(grade, EpistemicGrade.HAKKALYAKIN)


if __name__ == "__main__":
    unittest.main()
