"""
tests/test_bileshke.py — Comprehensive tests for the Bileshke (Composite Convergence) Engine.

Phase D: 7-lens composite convergence, quality framework (Q-1..Q-4), coverage gates.

Test categories:
  1. Enums & Constants            — Latife, Ortam, EpistemicGrade, LensId, mappings
  2. LatifeVektor                 — construction, AX53, gate_score, from_active_lenses
  3. OrtamVektor                  — construction, gate_score, full, empty
  4. LensResult                   — construction, AX56, clamped_score, to_dict
  5. QualityReport                — properties, to_dict
  6. Engine core                  — bileshke(), validate_weights, DEFAULT_WEIGHTS
  7. Coverage & grade             — compute_latife_vektor, compute_coverage_gate,
                                    compute_aggregate_grade, detect_kv4_warning
  8. Full pipeline                — run_bileshke, white_light_summary
  9. Quality framework (Q-1..Q-4) — compute_q1..q4, build_quality_report,
                                    framework_summary
 10. Structural constraints       — T6, T17, KV₄, KV₇, AX52, AX53, AX55, AX56
"""

import unittest
import math
from typing import Dict, List

from bileshke.types import (
    clamp_score,
    Latife,
    Ortam,
    EpistemicGrade,
    LensId,
    TESCIL,
    LENS_TO_LATIFE,
    LENS_PACKAGE,
    LENS_ORDER,
    ALL_LATIFELER,
    ALL_ORTAM,
    NUM_LATIFELER,
    NUM_ORTAM,
    NUM_LENSES,
    MAX_ACCESSIBLE_LATIFE,
    MAX_COMPLETENESS_RATIO,
    LatifeVektor,
    OrtamVektor,
    LensResult,
    QualityReport,
)

from bileshke.engine import (
    DEFAULT_WEIGHTS,
    validate_weights,
    bileshke,
    compute_latife_vektor,
    compute_coverage_gate,
    detect_kv4_warning,
    compute_aggregate_grade,
    run_bileshke,
    white_light_summary,
)

from bileshke.quality import (
    ALL_KAVAID,
    KAVAID_DESCRIPTIONS,
    compute_q1_coverage,
    compute_q2_epistemic,
    compute_q3_kavaid,
    compute_q4_completeness,
    build_quality_report,
    framework_summary,
)


# ========================================================================
# Helpers
# ========================================================================

def make_result(
    lens: LensId,
    score: float = 0.5,
    grade: EpistemicGrade = EpistemicGrade.TASDIK,
    checks_passed: int = 5,
    checks_total: int = 10,
) -> LensResult:
    """Helper to create a LensResult with defaults."""
    return LensResult(
        lens_id=lens,
        score=score,
        grade=grade,
        checks_passed=checks_passed,
        checks_total=checks_total,
    )


def make_all_results(score: float = 0.5) -> List[LensResult]:
    """Create results for all 7 lenses with uniform score."""
    return [make_result(lens, score) for lens in LENS_ORDER]


# ========================================================================
# 1. Enums & Constants
# ========================================================================

class TestLatife(unittest.TestCase):
    """AX49: 7 subtle faculties."""

    def test_member_count(self):
        self.assertEqual(len(Latife), 7)

    def test_members(self):
        expected = {"Akıl", "Kalb", "Nefs", "Ruh", "Sır", "Hafî", "Ahfâ"}
        actual = {l.value for l in Latife}
        self.assertEqual(actual, expected)

    def test_ahfa_exists(self):
        """AX53: Ahfâ must exist as a member even though permanently unmapped."""
        self.assertIn(Latife.AHFA, list(Latife))

    def test_ahfa_is_last(self):
        """By convention, Ahfâ has index 6."""
        all_list = list(Latife)
        self.assertEqual(all_list[6], Latife.AHFA)


class TestOrtam(unittest.TestCase):
    """§5.2: 3 epistemic media."""

    def test_member_count(self):
        self.assertEqual(len(Ortam), 3)

    def test_members(self):
        expected = {"Nesim", "Ziya", "Ab-ı Hayat"}
        actual = {o.value for o in Ortam}
        self.assertEqual(actual, expected)


class TestEpistemicGrade(unittest.TestCase):
    """AX56: 4 grades, ascending order, Hakkalyakîn inaccessible."""

    def test_member_count(self):
        self.assertEqual(len(EpistemicGrade), 4)

    def test_rank_ordering(self):
        self.assertEqual(EpistemicGrade.TASAVVUR.rank, 0)
        self.assertEqual(EpistemicGrade.TASDIK.rank, 1)
        self.assertEqual(EpistemicGrade.ILMELYAKIN.rank, 2)
        self.assertEqual(EpistemicGrade.HAKKALYAKIN.rank, 3)

    def test_comparison_lt(self):
        self.assertTrue(EpistemicGrade.TASAVVUR < EpistemicGrade.TASDIK)
        self.assertTrue(EpistemicGrade.TASDIK < EpistemicGrade.ILMELYAKIN)
        self.assertTrue(EpistemicGrade.ILMELYAKIN < EpistemicGrade.HAKKALYAKIN)

    def test_comparison_le(self):
        self.assertTrue(EpistemicGrade.TASAVVUR <= EpistemicGrade.TASAVVUR)
        self.assertTrue(EpistemicGrade.TASDIK <= EpistemicGrade.ILMELYAKIN)

    def test_comparison_gt(self):
        self.assertTrue(EpistemicGrade.HAKKALYAKIN > EpistemicGrade.ILMELYAKIN)
        self.assertFalse(EpistemicGrade.TASAVVUR > EpistemicGrade.TASDIK)

    def test_comparison_ge(self):
        self.assertTrue(EpistemicGrade.ILMELYAKIN >= EpistemicGrade.ILMELYAKIN)
        self.assertTrue(EpistemicGrade.ILMELYAKIN >= EpistemicGrade.TASAVVUR)

    def test_is_accessible(self):
        self.assertTrue(EpistemicGrade.TASAVVUR.is_accessible)
        self.assertTrue(EpistemicGrade.TASDIK.is_accessible)
        self.assertTrue(EpistemicGrade.ILMELYAKIN.is_accessible)
        self.assertFalse(EpistemicGrade.HAKKALYAKIN.is_accessible)


class TestLensId(unittest.TestCase):
    """§4.1: 7 formal lenses."""

    def test_member_count(self):
        self.assertEqual(len(LensId), 7)

    def test_all_lens_values(self):
        expected = {
            "Ontoloji", "Mereoloji", "FOL", "Bayes",
            "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik",
        }
        actual = {l.value for l in LensId}
        self.assertEqual(actual, expected)


class TestConstants(unittest.TestCase):
    """Cross-reference mappings and numeric constants."""

    def test_num_latifeler(self):
        self.assertEqual(NUM_LATIFELER, 7)

    def test_num_ortam(self):
        self.assertEqual(NUM_ORTAM, 3)

    def test_num_lenses(self):
        self.assertEqual(NUM_LENSES, 7)

    def test_max_accessible_latife(self):
        """T17: Ahfâ permanently unmapped → max = 6."""
        self.assertEqual(MAX_ACCESSIBLE_LATIFE, 6)

    def test_max_completeness_ratio(self):
        """T17: 6/7 ≈ 0.857."""
        self.assertAlmostEqual(MAX_COMPLETENESS_RATIO, 6 / 7)

    def test_all_latifeler(self):
        self.assertEqual(len(ALL_LATIFELER), 7)

    def test_all_ortam(self):
        self.assertEqual(len(ALL_ORTAM), 3)

    def test_lens_order_count(self):
        self.assertEqual(len(LENS_ORDER), 7)

    def test_lens_order_uniqueness(self):
        self.assertEqual(len(set(LENS_ORDER)), 7)


class TestTescil(unittest.TestCase):
    """Appendix C.5: Tescil partial bijection."""

    def test_all_latifeler_have_mapping(self):
        """Every latife has an entry."""
        for l in Latife:
            self.assertIn(l, TESCIL)

    def test_ahfa_maps_to_none(self):
        """AX53: Ahfâ ↦ ∅."""
        self.assertIsNone(TESCIL[Latife.AHFA])

    def test_non_ahfa_map_to_lens(self):
        """All non-Ahfâ latifeler map to a LensId."""
        for l in Latife:
            if l != Latife.AHFA:
                self.assertIsNotNone(TESCIL[l])
                self.assertIsInstance(TESCIL[l], LensId)

    def test_specific_mappings(self):
        """§4.1 Tescil table."""
        self.assertEqual(TESCIL[Latife.AKIL], LensId.ONTOLOJI)
        self.assertEqual(TESCIL[Latife.KALB], LensId.BAYES)
        self.assertEqual(TESCIL[Latife.NEFS], LensId.OYUN_TEORISI)
        self.assertEqual(TESCIL[Latife.RUH], LensId.TOPOLOJI_HOLOGRAFIK)
        self.assertEqual(TESCIL[Latife.SIR], LensId.KATEGORI_TEORISI)
        self.assertEqual(TESCIL[Latife.HAFI], LensId.TOPOLOJI_HOLOGRAFIK)


class TestLensToLatife(unittest.TestCase):
    """Reverse Tescil: LensId → Latife tuple."""

    def test_all_lenses_have_entry(self):
        for lid in LensId:
            self.assertIn(lid, LENS_TO_LATIFE)

    def test_mereoloji_empty(self):
        """Mereoloji has no latife mapping (formal instrument)."""
        self.assertEqual(LENS_TO_LATIFE[LensId.MEREOLOJI], ())

    def test_fol_empty(self):
        """FOL has no latife mapping (formal instrument)."""
        self.assertEqual(LENS_TO_LATIFE[LensId.FOL], ())

    def test_holografik_dual(self):
        """Topoloji+Holografik maps to Ruh AND Hafî."""
        latifeler = LENS_TO_LATIFE[LensId.TOPOLOJI_HOLOGRAFIK]
        self.assertEqual(len(latifeler), 2)
        self.assertIn(Latife.RUH, latifeler)
        self.assertIn(Latife.HAFI, latifeler)

    def test_ontoloji_akil(self):
        self.assertEqual(LENS_TO_LATIFE[LensId.ONTOLOJI], (Latife.AKIL,))

    def test_bayes_kalb(self):
        self.assertEqual(LENS_TO_LATIFE[LensId.BAYES], (Latife.KALB,))


class TestLensPackage(unittest.TestCase):
    """All 7 lenses have Python package names."""

    def test_all_lenses_mapped(self):
        for lid in LensId:
            self.assertIn(lid, LENS_PACKAGE)

    def test_specific_packages(self):
        self.assertEqual(LENS_PACKAGE[LensId.ONTOLOJI], "kavram_sozlugu")
        self.assertEqual(LENS_PACKAGE[LensId.MEREOLOJI], "mereoloji")
        self.assertEqual(LENS_PACKAGE[LensId.FOL], "fol_formalizasyon")
        self.assertEqual(LENS_PACKAGE[LensId.BAYES], "bayes_analiz")
        self.assertEqual(LENS_PACKAGE[LensId.OYUN_TEORISI], "oyun_teorisi")
        self.assertEqual(LENS_PACKAGE[LensId.KATEGORI_TEORISI], "kategori_teorisi")
        self.assertEqual(LENS_PACKAGE[LensId.TOPOLOJI_HOLOGRAFIK], "holografik")


class TestClampScore(unittest.TestCase):
    """T6/KV₄: Composite always < 1.0."""

    def test_clamp_normal(self):
        self.assertEqual(clamp_score(0.5), 0.5)

    def test_clamp_zero(self):
        self.assertEqual(clamp_score(0.0), 0.0)

    def test_clamp_negative(self):
        self.assertEqual(clamp_score(-0.5), 0.0)

    def test_clamp_above_one(self):
        self.assertEqual(clamp_score(1.5), 0.9999)

    def test_clamp_exactly_one(self):
        self.assertEqual(clamp_score(1.0), 0.9999)

    def test_clamp_at_max(self):
        self.assertEqual(clamp_score(0.9999), 0.9999)

    def test_clamp_just_below_max(self):
        self.assertEqual(clamp_score(0.9998), 0.9998)


# ========================================================================
# 2. LatifeVektor
# ========================================================================

class TestLatifeVektor(unittest.TestCase):
    """{0,1}⁷ latife coverage vector."""

    def test_construction_valid(self):
        v = LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0))
        self.assertEqual(v.bits, (1, 0, 0, 0, 0, 0, 0))

    def test_construction_wrong_length(self):
        with self.assertRaises(ValueError):
            LatifeVektor(bits=(1, 0, 0))

    def test_construction_invalid_bit(self):
        with self.assertRaises(ValueError):
            LatifeVektor(bits=(1, 2, 0, 0, 0, 0, 0))

    def test_ax53_ahfa_must_be_zero(self):
        """AX53: Ahfâ (index 6) must be 0."""
        with self.assertRaises(ValueError) as ctx:
            LatifeVektor(bits=(1, 1, 1, 1, 1, 1, 1))
        self.assertIn("AX53", str(ctx.exception))

    def test_active_count_full(self):
        v = LatifeVektor.full()
        self.assertEqual(v.active_count, 6)

    def test_active_count_empty(self):
        v = LatifeVektor.empty()
        self.assertEqual(v.active_count, 0)

    def test_active_count_partial(self):
        v = LatifeVektor(bits=(1, 1, 0, 0, 0, 0, 0))
        self.assertEqual(v.active_count, 2)

    def test_completeness_full(self):
        """T17: max completeness = 6/7."""
        v = LatifeVektor.full()
        self.assertAlmostEqual(v.completeness, 6 / 7)

    def test_completeness_empty(self):
        v = LatifeVektor.empty()
        self.assertAlmostEqual(v.completeness, 0.0)

    def test_accessible_bits(self):
        v = LatifeVektor(bits=(1, 1, 1, 1, 1, 1, 0))
        self.assertEqual(v.accessible_bits, (1, 1, 1, 1, 1, 1))

    def test_gate_score_full(self):
        """AX52: All accessible bits = 1 → gate = 1.0."""
        v = LatifeVektor.full()
        self.assertEqual(v.gate_score(), 1.0)

    def test_gate_score_one_zero(self):
        """AX52: One zero in accessible → gate collapses to 0.0."""
        v = LatifeVektor(bits=(1, 1, 1, 1, 0, 1, 0))
        self.assertEqual(v.gate_score(), 0.0)

    def test_gate_score_empty(self):
        v = LatifeVektor.empty()
        self.assertEqual(v.gate_score(), 0.0)

    def test_is_engaged(self):
        v = LatifeVektor(bits=(1, 0, 0, 1, 0, 0, 0))
        self.assertTrue(v.is_engaged(Latife.AKIL))
        self.assertFalse(v.is_engaged(Latife.KALB))
        self.assertTrue(v.is_engaged(Latife.RUH))
        self.assertFalse(v.is_engaged(Latife.AHFA))

    def test_to_dict(self):
        v = LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0))
        d = v.to_dict()
        self.assertEqual(d["Akıl"], 1)
        self.assertEqual(d["Ahfâ"], 0)
        self.assertEqual(len(d), 7)

    def test_from_active_lenses_all(self):
        """All 7 lenses active → 6 latifeler engaged (Mereoloji & FOL have none)."""
        v = LatifeVektor.from_active_lenses(list(LENS_ORDER))
        # Ontoloji→Akıl, Bayes→Kalb, OyunTeorisi→Nefs,
        # KategoriTeorisi→Sır, Holografik→(Ruh, Hafî)
        # Mereoloji→(), FOL→() → no latife contribution
        # Total unique: Akıl, Kalb, Nefs, Ruh, Sır, Hafî = 6
        self.assertEqual(v.active_count, 6)

    def test_from_active_lenses_all_latifeler_count(self):
        """All 7 lenses → 6 unique accessible latifeler (Mereoloji & FOL = no latife)."""
        v = LatifeVektor.from_active_lenses(list(LENS_ORDER))
        # Ontoloji→Akıl, Bayes→Kalb, OyunTeorisi→Nefs,
        # KategoriTeorisi→Sır, Holografik→(Ruh,Hafî)
        # Mereoloji→(), FOL→()
        # Total unique: Akıl, Kalb, Nefs, Ruh, Sır, Hafî = 6
        expected = LatifeVektor(bits=(1, 1, 1, 1, 1, 1, 0))
        self.assertEqual(v, expected)

    def test_from_active_lenses_single(self):
        """Single lens: Ontoloji → only Akıl engaged."""
        v = LatifeVektor.from_active_lenses([LensId.ONTOLOJI])
        self.assertEqual(v.active_count, 1)
        self.assertTrue(v.is_engaged(Latife.AKIL))

    def test_from_active_lenses_formal_only(self):
        """Mereoloji + FOL → no latifeler engaged."""
        v = LatifeVektor.from_active_lenses([LensId.MEREOLOJI, LensId.FOL])
        self.assertEqual(v.active_count, 0)

    def test_from_active_lenses_empty(self):
        v = LatifeVektor.from_active_lenses([])
        self.assertEqual(v, LatifeVektor.empty())

    def test_full_creation(self):
        v = LatifeVektor.full()
        self.assertEqual(v.bits, (1, 1, 1, 1, 1, 1, 0))
        self.assertEqual(v.bits[6], 0)  # AX53

    def test_empty_creation(self):
        v = LatifeVektor.empty()
        self.assertEqual(v.bits, (0, 0, 0, 0, 0, 0, 0))

    def test_frozen(self):
        """LatifeVektor is frozen dataclass."""
        v = LatifeVektor.full()
        with self.assertRaises(AttributeError):
            v.bits = (0, 0, 0, 0, 0, 0, 0)


# ========================================================================
# 3. OrtamVektor
# ========================================================================

class TestOrtamVektor(unittest.TestCase):
    """{0,1}³ epistemic media vector."""

    def test_construction_valid(self):
        v = OrtamVektor(bits=(1, 0, 1))
        self.assertEqual(v.bits, (1, 0, 1))

    def test_construction_wrong_length(self):
        with self.assertRaises(ValueError):
            OrtamVektor(bits=(1, 0))

    def test_construction_invalid_bit(self):
        with self.assertRaises(ValueError):
            OrtamVektor(bits=(1, 2, 0))

    def test_active_count_full(self):
        self.assertEqual(OrtamVektor.full().active_count, 3)

    def test_active_count_empty(self):
        self.assertEqual(OrtamVektor.empty().active_count, 0)

    def test_gate_score_full(self):
        self.assertEqual(OrtamVektor.full().gate_score(), 1.0)

    def test_gate_score_partial(self):
        """AX52: one zero → gate collapses."""
        v = OrtamVektor(bits=(1, 0, 1))
        self.assertEqual(v.gate_score(), 0.0)

    def test_gate_score_empty(self):
        self.assertEqual(OrtamVektor.empty().gate_score(), 0.0)

    def test_to_dict(self):
        v = OrtamVektor(bits=(1, 0, 1))
        d = v.to_dict()
        self.assertEqual(d["Nesim"], 1)
        self.assertEqual(d["Ziya"], 0)
        self.assertEqual(d["Ab-ı Hayat"], 1)
        self.assertEqual(len(d), 3)

    def test_full_creation(self):
        v = OrtamVektor.full()
        self.assertEqual(v.bits, (1, 1, 1))

    def test_empty_creation(self):
        v = OrtamVektor.empty()
        self.assertEqual(v.bits, (0, 0, 0))

    def test_frozen(self):
        v = OrtamVektor.full()
        with self.assertRaises(AttributeError):
            v.bits = (0, 0, 0)


# ========================================================================
# 4. LensResult
# ========================================================================

class TestLensResult(unittest.TestCase):
    """Per-lens yakinlasma result."""

    def test_construction_valid(self):
        r = make_result(LensId.ONTOLOJI, 0.75, EpistemicGrade.TASDIK)
        self.assertEqual(r.lens_id, LensId.ONTOLOJI)
        self.assertEqual(r.score, 0.75)
        self.assertEqual(r.grade, EpistemicGrade.TASDIK)

    def test_ax56_hakkalyakin_rejected(self):
        """AX56: Cannot create LensResult with Hakkalyakîn grade."""
        with self.assertRaises(ValueError) as ctx:
            LensResult(
                lens_id=LensId.ONTOLOJI,
                score=0.5,
                grade=EpistemicGrade.HAKKALYAKIN,
            )
        self.assertIn("AX56", str(ctx.exception))

    def test_clamped_score_normal(self):
        r = make_result(LensId.BAYES, 0.8)
        self.assertEqual(r.clamped_score, 0.8)

    def test_clamped_score_above_max(self):
        r = LensResult(lens_id=LensId.BAYES, score=1.5)
        self.assertEqual(r.clamped_score, 0.9999)

    def test_clamped_score_negative(self):
        r = LensResult(lens_id=LensId.BAYES, score=-0.5)
        self.assertEqual(r.clamped_score, 0.0)

    def test_default_grade(self):
        r = LensResult(lens_id=LensId.FOL, score=0.5)
        self.assertEqual(r.grade, EpistemicGrade.TASAVVUR)

    def test_to_dict_keys(self):
        r = make_result(LensId.ONTOLOJI)
        d = r.to_dict()
        self.assertIn("lens", d)
        self.assertIn("score", d)
        self.assertIn("grade", d)
        self.assertIn("checks_passed", d)
        self.assertIn("checks_total", d)

    def test_to_dict_values(self):
        r = make_result(LensId.ONTOLOJI, 0.75, EpistemicGrade.ILMELYAKIN, 8, 10)
        d = r.to_dict()
        self.assertEqual(d["lens"], "Ontoloji")
        self.assertAlmostEqual(d["score"], 0.75)
        self.assertEqual(d["grade"], "İlmelyakîn")

    def test_invalid_lens_id_type(self):
        with self.assertRaises(TypeError):
            LensResult(lens_id="not_a_lens", score=0.5)

    def test_invalid_score_type(self):
        with self.assertRaises(TypeError):
            LensResult(lens_id=LensId.BAYES, score="not_numeric")

    def test_frozen(self):
        r = make_result(LensId.ONTOLOJI)
        with self.assertRaises(AttributeError):
            r.score = 0.9


# ========================================================================
# 5. QualityReport
# ========================================================================

class TestQualityReport(unittest.TestCase):
    """§5.5 Quality Report Schema."""

    def test_default_construction(self):
        rpt = QualityReport()
        self.assertEqual(rpt.composite_score, 0.0)
        self.assertEqual(rpt.completeness, 0.0)
        self.assertEqual(rpt.lens_results, [])
        self.assertEqual(rpt.warnings, [])

    def test_completeness_from_latife(self):
        rpt = QualityReport(latife_vektor=LatifeVektor.full())
        self.assertAlmostEqual(rpt.completeness, 6 / 7)

    def test_latife_gate(self):
        rpt = QualityReport(latife_vektor=LatifeVektor.full())
        self.assertEqual(rpt.latife_gate, 1.0)

    def test_latife_gate_partial(self):
        rpt = QualityReport(
            latife_vektor=LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0))
        )
        self.assertEqual(rpt.latife_gate, 0.0)

    def test_ortam_gate(self):
        rpt = QualityReport(ortam_vektor=OrtamVektor.full())
        self.assertEqual(rpt.ortam_gate, 1.0)

    def test_practical_gate_ax55(self):
        """AX55: PracticalGate = Gate(latife₆) ∧ Gate(medium₃)."""
        rpt = QualityReport(
            latife_vektor=LatifeVektor.full(),
            ortam_vektor=OrtamVektor.full(),
        )
        self.assertEqual(rpt.practical_gate, 1.0)

    def test_practical_gate_partial_fails(self):
        rpt = QualityReport(
            latife_vektor=LatifeVektor.full(),
            ortam_vektor=OrtamVektor(bits=(1, 0, 1)),
        )
        self.assertEqual(rpt.practical_gate, 0.0)

    def test_all_kavaid_pass_empty(self):
        rpt = QualityReport()
        self.assertTrue(rpt.all_kavaid_pass)

    def test_all_kavaid_pass_true(self):
        rpt = QualityReport(kavaid_checks={"KV1": True, "KV2": True})
        self.assertTrue(rpt.all_kavaid_pass)

    def test_all_kavaid_pass_false(self):
        rpt = QualityReport(kavaid_checks={"KV1": True, "KV2": False})
        self.assertFalse(rpt.all_kavaid_pass)

    def test_has_kv4_warning_below(self):
        rpt = QualityReport(composite_score=0.8)
        self.assertFalse(rpt.has_kv4_warning)

    def test_has_kv4_warning_above(self):
        rpt = QualityReport(composite_score=0.96)
        self.assertTrue(rpt.has_kv4_warning)

    def test_has_kv4_warning_at_threshold(self):
        rpt = QualityReport(composite_score=0.95)
        self.assertTrue(rpt.has_kv4_warning)

    def test_to_dict_keys(self):
        rpt = QualityReport()
        d = rpt.to_dict()
        self.assertIn("composite_score", d)
        self.assertIn("coverage", d)
        self.assertIn("degree_distribution", d)
        self.assertIn("kavaid_checks", d)
        self.assertIn("completeness", d)
        self.assertIn("max_completeness", d)
        self.assertIn("latife_gate", d)
        self.assertIn("ortam_gate", d)
        self.assertIn("practical_gate", d)
        self.assertIn("all_kavaid_pass", d)
        self.assertIn("lens_results", d)
        self.assertIn("warnings", d)

    def test_to_dict_coverage_nesting(self):
        rpt = QualityReport()
        d = rpt.to_dict()
        self.assertIn("latife", d["coverage"])
        self.assertIn("medium", d["coverage"])

    def test_mutable_warnings(self):
        """QualityReport is mutable (not frozen) so warnings can be appended."""
        rpt = QualityReport()
        rpt.warnings.append("test warning")
        self.assertEqual(len(rpt.warnings), 1)


# ========================================================================
# 6. Engine core
# ========================================================================

class TestDefaultWeights(unittest.TestCase):
    """Default equal weights: 1/7 each, Σ=1.0."""

    def test_seven_weights(self):
        self.assertEqual(len(DEFAULT_WEIGHTS), 7)

    def test_all_equal(self):
        expected = 1.0 / 7
        for w in DEFAULT_WEIGHTS.values():
            self.assertAlmostEqual(w, expected)

    def test_sum_to_one(self):
        self.assertAlmostEqual(sum(DEFAULT_WEIGHTS.values()), 1.0)

    def test_all_lenses_present(self):
        for lid in LENS_ORDER:
            self.assertIn(lid, DEFAULT_WEIGHTS)


class TestValidateWeights(unittest.TestCase):
    """Weight validation."""

    def test_valid_weights(self):
        ok, msg = validate_weights(DEFAULT_WEIGHTS)
        self.assertTrue(ok)
        self.assertEqual(msg, "OK")

    def test_wrong_count(self):
        ok, msg = validate_weights({LensId.ONTOLOJI: 1.0})
        self.assertFalse(ok)
        self.assertIn("7", msg)

    def test_negative_weight(self):
        w = dict(DEFAULT_WEIGHTS)
        w[LensId.ONTOLOJI] = -0.1
        ok, msg = validate_weights(w)
        self.assertFalse(ok)
        self.assertIn("negative", msg.lower())

    def test_wrong_sum(self):
        w = {lid: 0.5 for lid in LENS_ORDER}
        ok, msg = validate_weights(w)
        self.assertFalse(ok)
        self.assertIn("sum", msg.lower())

    def test_missing_lens(self):
        w = {lid: 1.0 / 7 for lid in list(LENS_ORDER)[:6]}
        # Add a wrong key
        w["wrong"] = 1.0 / 7
        ok, msg = validate_weights(w)
        self.assertFalse(ok)


class TestBileshke(unittest.TestCase):
    """Core composite formula: bileshke(t) = Σᵢ wᵢ · yakinlasma(mᵢ, t)."""

    def test_empty_results(self):
        score = bileshke([])
        self.assertEqual(score, 0.0)

    def test_single_lens(self):
        """Single lens at 0.7 with default weights (1/7) → 0.7/7 ≈ 0.1."""
        results = [make_result(LensId.ONTOLOJI, 0.7)]
        score = bileshke(results)
        expected = 0.7 / 7
        self.assertAlmostEqual(score, expected, places=5)

    def test_all_lenses_uniform_score(self):
        """All 7 lenses at 0.6 → composite = 0.6."""
        results = make_all_results(0.6)
        score = bileshke(results)
        self.assertAlmostEqual(score, 0.6, places=5)

    def test_all_lenses_varying_scores(self):
        """Different scores, equal weights → average."""
        scores = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]
        results = [
            make_result(lid, s)
            for lid, s in zip(LENS_ORDER, scores)
        ]
        score = bileshke(results)
        expected = sum(scores) / 7
        self.assertAlmostEqual(score, expected, places=5)

    def test_custom_weights(self):
        """Custom weights: first lens gets all weight."""
        custom = {lid: 0.0 for lid in LENS_ORDER}
        custom[LensId.ONTOLOJI] = 1.0
        results = [
            make_result(LensId.ONTOLOJI, 0.8),
            make_result(LensId.BAYES, 0.2),
        ]
        score = bileshke(results, custom)
        self.assertAlmostEqual(score, 0.8, places=5)

    def test_score_always_below_one(self):
        """T6/KV₄: composite < 1.0 even with max scores."""
        results = [make_result(lid, 0.9999) for lid in LENS_ORDER]
        score = bileshke(results)
        self.assertLess(score, 1.0)

    def test_score_clamped_at_max(self):
        results = [make_result(lid, 1.5) for lid in LENS_ORDER]
        score = bileshke(results)
        self.assertLessEqual(score, 0.9999)

    def test_invalid_weights_raise(self):
        with self.assertRaises(ValueError):
            bileshke([], {LensId.ONTOLOJI: 1.0})

    def test_zero_scores(self):
        results = make_all_results(0.0)
        score = bileshke(results)
        self.assertEqual(score, 0.0)


# ========================================================================
# 7. Coverage & grade computation
# ========================================================================

class TestComputeLatife(unittest.TestCase):
    """compute_latife_vektor delegates to from_active_lenses."""

    def test_all_lenses(self):
        v = compute_latife_vektor(list(LENS_ORDER))
        self.assertEqual(v, LatifeVektor.full())

    def test_empty(self):
        v = compute_latife_vektor([])
        self.assertEqual(v, LatifeVektor.empty())

    def test_single_lens(self):
        v = compute_latife_vektor([LensId.BAYES])
        self.assertTrue(v.is_engaged(Latife.KALB))
        self.assertEqual(v.active_count, 1)


class TestComputeCoverageGate(unittest.TestCase):
    """AX55 dual gate."""

    def test_full_coverage(self):
        gate = compute_coverage_gate(LatifeVektor.full(), OrtamVektor.full())
        self.assertEqual(gate, 1.0)

    def test_latife_partial_fails(self):
        gate = compute_coverage_gate(
            LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0)),
            OrtamVektor.full(),
        )
        self.assertEqual(gate, 0.0)

    def test_ortam_partial_fails(self):
        gate = compute_coverage_gate(
            LatifeVektor.full(),
            OrtamVektor(bits=(1, 0, 1)),
        )
        self.assertEqual(gate, 0.0)

    def test_both_empty(self):
        gate = compute_coverage_gate(
            LatifeVektor.empty(), OrtamVektor.empty()
        )
        self.assertEqual(gate, 0.0)


class TestDetectKV4Warning(unittest.TestCase):
    """KV₄: composite ≥ 0.95 → warning."""

    def test_below_threshold(self):
        self.assertIsNone(detect_kv4_warning(0.8))

    def test_at_threshold(self):
        warning = detect_kv4_warning(0.95)
        self.assertIsNotNone(warning)
        self.assertIn("KV₄", warning)

    def test_above_threshold(self):
        warning = detect_kv4_warning(0.99)
        self.assertIsNotNone(warning)

    def test_zero(self):
        self.assertIsNone(detect_kv4_warning(0.0))

    def test_just_below(self):
        self.assertIsNone(detect_kv4_warning(0.9499))


class TestComputeAggregateGrade(unittest.TestCase):
    """Aggregate = minimum grade, capped at İlmelyakîn (AX56)."""

    def test_empty_results(self):
        grade = compute_aggregate_grade([])
        self.assertEqual(grade, EpistemicGrade.TASAVVUR)

    def test_single_tasdik(self):
        results = [make_result(LensId.ONTOLOJI, grade=EpistemicGrade.TASDIK)]
        grade = compute_aggregate_grade(results)
        self.assertEqual(grade, EpistemicGrade.TASDIK)

    def test_minimum_rule(self):
        """Weakest link determines aggregate."""
        results = [
            make_result(LensId.ONTOLOJI, grade=EpistemicGrade.ILMELYAKIN),
            make_result(LensId.BAYES, grade=EpistemicGrade.TASAVVUR),
        ]
        grade = compute_aggregate_grade(results)
        self.assertEqual(grade, EpistemicGrade.TASAVVUR)

    def test_all_ilmelyakin(self):
        results = [
            make_result(lid, grade=EpistemicGrade.ILMELYAKIN)
            for lid in LENS_ORDER
        ]
        grade = compute_aggregate_grade(results)
        self.assertEqual(grade, EpistemicGrade.ILMELYAKIN)

    def test_cap_at_ilmelyakin(self):
        """AX56: Even if all lenses claim İlmelyakîn, aggregate never exceeds it."""
        results = [
            make_result(lid, grade=EpistemicGrade.ILMELYAKIN)
            for lid in LENS_ORDER
        ]
        grade = compute_aggregate_grade(results)
        self.assertLessEqual(grade, EpistemicGrade.ILMELYAKIN)


# ========================================================================
# 8. Full pipeline + white light
# ========================================================================

class TestRunBileshke(unittest.TestCase):
    """Full pipeline: run_bileshke → QualityReport."""

    def test_returns_quality_report(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertIsInstance(report, QualityReport)

    def test_composite_score(self):
        results = make_all_results(0.7)
        report = run_bileshke(results)
        self.assertAlmostEqual(report.composite_score, 0.7, places=4)

    def test_latife_vektor_computed(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertEqual(report.latife_vektor, LatifeVektor.full())

    def test_degree_distribution(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertEqual(len(report.degree_distribution), 7)

    def test_default_ortam_is_full(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertEqual(report.ortam_vektor, OrtamVektor.full())

    def test_custom_ortam(self):
        results = make_all_results(0.5)
        ortam = OrtamVektor(bits=(1, 0, 1))
        report = run_bileshke(results, ortam=ortam)
        self.assertEqual(report.ortam_vektor, ortam)

    def test_kv4_warning_triggered(self):
        """High scores → KV₄ warning in report."""
        results = make_all_results(0.96)
        report = run_bileshke(results)
        kv4_warnings = [w for w in report.warnings if "KV₄" in w]
        self.assertTrue(len(kv4_warnings) > 0)

    def test_kv4_warning_absent_for_low_score(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        kv4_warnings = [w for w in report.warnings if "KV₄" in w]
        self.assertEqual(len(kv4_warnings), 0)

    def test_latife_coverage_warning(self):
        """Single lens → T17 incomplete coverage warning."""
        results = [make_result(LensId.ONTOLOJI, 0.5)]
        report = run_bileshke(results)
        t17_warnings = [w for w in report.warnings if "T17" in w]
        self.assertTrue(len(t17_warnings) > 0)

    def test_empty_results(self):
        report = run_bileshke([])
        self.assertEqual(report.composite_score, 0.0)
        self.assertEqual(report.latife_vektor, LatifeVektor.empty())

    def test_lens_results_stored(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertEqual(len(report.lens_results), 7)


class TestWhiteLightSummary(unittest.TestCase):
    """AX50: Elvan-ı Seb'a — 7 spectral contributions."""

    def test_returns_dict(self):
        report = run_bileshke(make_all_results(0.5))
        summary = white_light_summary(report)
        self.assertIsInstance(summary, dict)

    def test_seven_colors(self):
        report = run_bileshke(make_all_results(0.5))
        summary = white_light_summary(report)
        self.assertEqual(len(summary["colors"]), 7)

    def test_synthesis_complete_all_lenses(self):
        report = run_bileshke(make_all_results(0.5))
        summary = white_light_summary(report)
        self.assertTrue(summary["synthesis_complete"])

    def test_synthesis_incomplete_partial(self):
        report = run_bileshke([make_result(LensId.ONTOLOJI, 0.5)])
        summary = white_light_summary(report)
        self.assertFalse(summary["synthesis_complete"])

    def test_metaphor_present(self):
        report = run_bileshke(make_all_results(0.5))
        summary = white_light_summary(report)
        self.assertIn("AX50", summary["metaphor"])

    def test_color_structure(self):
        report = run_bileshke(make_all_results(0.5))
        summary = white_light_summary(report)
        color = summary["colors"][0]
        self.assertIn("lens", color)
        self.assertIn("score", color)
        self.assertIn("active", color)
        self.assertIn("latifeler", color)

    def test_composite_in_summary(self):
        report = run_bileshke(make_all_results(0.6))
        summary = white_light_summary(report)
        self.assertAlmostEqual(summary["composite"], 0.6, places=4)


# ========================================================================
# 9. Quality framework (Q-1 through Q-4)
# ========================================================================

class TestKavaidConstants(unittest.TestCase):
    """8 kavaid identifiers and descriptions."""

    def test_all_kavaid_count(self):
        self.assertEqual(len(ALL_KAVAID), 8)

    def test_all_kavaid_names(self):
        expected = {"KV1", "KV2", "KV3", "KV4", "KV5", "KV6", "KV7", "KV8"}
        self.assertEqual(set(ALL_KAVAID), expected)

    def test_descriptions_count(self):
        self.assertEqual(len(KAVAID_DESCRIPTIONS), 8)

    def test_all_have_description(self):
        for kv in ALL_KAVAID:
            self.assertIn(kv, KAVAID_DESCRIPTIONS)
            self.assertTrue(len(KAVAID_DESCRIPTIONS[kv]) > 0)


class TestQ1Coverage(unittest.TestCase):
    """Q-1: Coverage Gates — AX52 multiplicative, AX55 dual."""

    def test_full_coverage_passes(self):
        result = compute_q1_coverage(LatifeVektor.full(), OrtamVektor.full())
        self.assertTrue(result["passed"])
        self.assertEqual(result["practical_gate"], 1.0)

    def test_partial_latife_fails(self):
        v = LatifeVektor(bits=(1, 0, 1, 0, 0, 0, 0))
        result = compute_q1_coverage(v, OrtamVektor.full())
        self.assertFalse(result["passed"])
        self.assertEqual(result["latife_gate"], 0.0)

    def test_partial_ortam_fails(self):
        result = compute_q1_coverage(
            LatifeVektor.full(),
            OrtamVektor(bits=(1, 0, 1)),
        )
        self.assertFalse(result["passed"])
        self.assertEqual(result["ortam_gate"], 0.0)

    def test_empty_fails(self):
        result = compute_q1_coverage(
            LatifeVektor.empty(), OrtamVektor.empty()
        )
        self.assertFalse(result["passed"])

    def test_component_label(self):
        result = compute_q1_coverage(LatifeVektor.full(), OrtamVektor.full())
        self.assertEqual(result["component"], "Q-1")

    def test_latife_max(self):
        result = compute_q1_coverage(LatifeVektor.full(), OrtamVektor.full())
        self.assertEqual(result["latife_max"], 6)

    def test_ortam_max(self):
        result = compute_q1_coverage(LatifeVektor.full(), OrtamVektor.full())
        self.assertEqual(result["ortam_max"], 3)


class TestQ2Epistemic(unittest.TestCase):
    """Q-2: Epistemic Degree Scale — AX56."""

    def test_empty_results(self):
        result = compute_q2_epistemic([])
        self.assertTrue(result["passed"])
        self.assertEqual(result["aggregate_grade"], "Tasavvur")

    def test_uniform_tasdik(self):
        results = [
            make_result(lid, grade=EpistemicGrade.TASDIK)
            for lid in LENS_ORDER
        ]
        result = compute_q2_epistemic(results)
        self.assertTrue(result["passed"])
        self.assertEqual(result["aggregate_grade"], "Tasdik")

    def test_mixed_grades_minimum(self):
        results = [
            make_result(LensId.ONTOLOJI, grade=EpistemicGrade.ILMELYAKIN),
            make_result(LensId.BAYES, grade=EpistemicGrade.TASAVVUR),
        ]
        result = compute_q2_epistemic(results)
        self.assertEqual(result["aggregate_grade"], "Tasavvur")

    def test_per_lens_distribution(self):
        results = [
            make_result(LensId.ONTOLOJI, grade=EpistemicGrade.TASDIK),
            make_result(LensId.BAYES, grade=EpistemicGrade.ILMELYAKIN),
        ]
        result = compute_q2_epistemic(results)
        self.assertEqual(result["per_lens"]["Ontoloji"], "Tasdik")
        self.assertEqual(result["per_lens"]["Bayes"], "İlmelyakîn")

    def test_hakkalyakin_inaccessible(self):
        result = compute_q2_epistemic([])
        self.assertFalse(result["hakkalyakin_accessible"])

    def test_component_label(self):
        result = compute_q2_epistemic([])
        self.assertEqual(result["component"], "Q-2")


class TestQ3Kavaid(unittest.TestCase):
    """Q-3: Kavaid Register — KV₁–KV₈, multiplicative."""

    def test_all_pass(self):
        checks = {f"KV{i}": True for i in range(1, 9)}
        result = compute_q3_kavaid(checks, composite_score=0.5)
        self.assertTrue(result["passed"])
        self.assertTrue(result["all_pass"])

    def test_one_fails(self):
        checks = {f"KV{i}": True for i in range(1, 9)}
        checks["KV3"] = False
        result = compute_q3_kavaid(checks, composite_score=0.5)
        self.assertFalse(result["passed"])

    def test_kv4_auto_check_low_score(self):
        result = compute_q3_kavaid(None, composite_score=0.5)
        self.assertTrue(result["checks"]["KV4"])

    def test_kv4_auto_check_high_score(self):
        result = compute_q3_kavaid(None, composite_score=0.96)
        self.assertFalse(result["checks"]["KV4"])

    def test_kv7_default_true(self):
        """KV₇: bileshke doesn't import lenses → structurally independent."""
        result = compute_q3_kavaid(None, composite_score=0.5)
        self.assertTrue(result["checks"]["KV7"])

    def test_component_label(self):
        result = compute_q3_kavaid(None, composite_score=0.5)
        self.assertEqual(result["component"], "Q-3")

    def test_descriptions_present(self):
        checks = {"KV1": True, "KV4": True}
        result = compute_q3_kavaid(checks, composite_score=0.5)
        self.assertIn("KV1", result["descriptions"])
        self.assertTrue(len(result["descriptions"]["KV1"]) > 0)

    def test_empty_checks_pass(self):
        """No checks provided but KV4/KV7 auto-added."""
        result = compute_q3_kavaid(None, composite_score=0.5)
        self.assertTrue(result["passed"])

    def test_total_required(self):
        result = compute_q3_kavaid(None, composite_score=0.5)
        self.assertEqual(result["total_required"], 8)


class TestQ4Completeness(unittest.TestCase):
    """Q-4: Completeness and Transparency — T17, T18, AX57."""

    def test_full_coverage(self):
        results = make_all_results(0.5)
        latife = LatifeVektor.full()
        result = compute_q4_completeness(latife, results)
        self.assertAlmostEqual(result["completeness"], 6 / 7)
        self.assertEqual(result["latife_active"], 6)

    def test_partial_coverage(self):
        results = [make_result(LensId.ONTOLOJI, 0.5)]
        latife = compute_latife_vektor([LensId.ONTOLOJI])
        result = compute_q4_completeness(latife, results)
        self.assertEqual(result["latife_active"], 1)

    def test_ahfa_never_mapped(self):
        results = make_all_results(0.5)
        latife = LatifeVektor.full()
        result = compute_q4_completeness(latife, results)
        self.assertFalse(result["ahfa_mapped"])

    def test_per_module_diagnostic(self):
        """T18: per-module output."""
        results = [make_result(LensId.ONTOLOJI, 0.75, EpistemicGrade.TASDIK)]
        latife = compute_latife_vektor([LensId.ONTOLOJI])
        result = compute_q4_completeness(latife, results)
        self.assertIn("Ontoloji", result["per_module"])
        mod = result["per_module"]["Ontoloji"]
        self.assertAlmostEqual(mod["score"], 0.75)
        self.assertEqual(mod["grade"], "Tasdik")

    def test_t17_max_completeness(self):
        results = make_all_results(0.5)
        latife = LatifeVektor.full()
        result = compute_q4_completeness(latife, results)
        self.assertAlmostEqual(result["max_completeness"], 6 / 7)

    def test_component_label(self):
        result = compute_q4_completeness(LatifeVektor.empty(), [])
        self.assertEqual(result["component"], "Q-4")

    def test_always_passes(self):
        """Q-4 is informational, not gating."""
        result = compute_q4_completeness(LatifeVektor.empty(), [])
        self.assertTrue(result["passed"])

    def test_lenses_total(self):
        result = compute_q4_completeness(LatifeVektor.empty(), [])
        self.assertEqual(result["lenses_total"], 7)


class TestBuildQualityReport(unittest.TestCase):
    """Full Q-1 through Q-4 quality assessment."""

    def test_returns_dict(self):
        results = make_all_results(0.5)
        qr = build_quality_report(
            results, 0.5, LatifeVektor.full(), OrtamVektor.full()
        )
        self.assertIsInstance(qr, dict)

    def test_has_all_components(self):
        results = make_all_results(0.5)
        qr = build_quality_report(
            results, 0.5, LatifeVektor.full(), OrtamVektor.full()
        )
        self.assertIn("Q1_coverage", qr)
        self.assertIn("Q2_epistemic", qr)
        self.assertIn("Q3_kavaid", qr)
        self.assertIn("Q4_completeness", qr)
        self.assertIn("overall_pass", qr)
        self.assertIn("composite_score", qr)

    def test_overall_pass_full_coverage(self):
        results = make_all_results(0.5)
        checks = {f"KV{i}": True for i in range(1, 9)}
        qr = build_quality_report(
            results, 0.5, LatifeVektor.full(), OrtamVektor.full(), checks
        )
        self.assertTrue(qr["overall_pass"])

    def test_overall_fail_partial_coverage(self):
        results = [make_result(LensId.ONTOLOJI, 0.5)]
        qr = build_quality_report(
            results, 0.5,
            LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0)),
            OrtamVektor.full(),
        )
        self.assertFalse(qr["overall_pass"])

    def test_composite_score_clamped(self):
        qr = build_quality_report(
            [], 1.5, LatifeVektor.empty(), OrtamVektor.empty()
        )
        self.assertLessEqual(qr["composite_score"], 0.9999)

    def test_quality_framework_label(self):
        qr = build_quality_report(
            [], 0.0, LatifeVektor.empty(), OrtamVektor.empty()
        )
        self.assertEqual(qr["quality_framework"], "AX51")


class TestFrameworkSummary(unittest.TestCase):
    """AX57: Engine metadata disclosure."""

    def test_returns_dict(self):
        fs = framework_summary()
        self.assertIsInstance(fs, dict)

    def test_engine_name(self):
        fs = framework_summary()
        self.assertIn("Bileshke", fs["engine"])

    def test_formula_present(self):
        fs = framework_summary()
        self.assertIn("Σ", fs["formula"])

    def test_constraints_nonempty(self):
        fs = framework_summary()
        self.assertTrue(len(fs["constraints"]) > 0)

    def test_quality_components(self):
        fs = framework_summary()
        self.assertEqual(len(fs["quality_components"]), 4)

    def test_axioms_implemented(self):
        fs = framework_summary()
        self.assertIn("AX52", fs["axioms_implemented"])
        self.assertIn("AX53", fs["axioms_implemented"])
        self.assertIn("AX56", fs["axioms_implemented"])

    def test_theorems_implemented(self):
        fs = framework_summary()
        self.assertIn("T6", fs["theorems_implemented"])
        self.assertIn("T17", fs["theorems_implemented"])

    def test_kavaid_checked(self):
        fs = framework_summary()
        self.assertEqual(len(fs["kavaid_checked"]), 8)

    def test_tescil_mapping(self):
        fs = framework_summary()
        self.assertIn("Ahfâ", fs["tescil_mapping"])
        self.assertEqual(fs["tescil_mapping"]["Ahfâ"], "∅")

    def test_lens_order(self):
        fs = framework_summary()
        self.assertEqual(len(fs["lens_order"]), 7)

    def test_max_score(self):
        fs = framework_summary()
        self.assertEqual(fs["max_score"], 0.9999)


# ========================================================================
# 10. Structural constraints
# ========================================================================

class TestT6ConvergenceBound(unittest.TestCase):
    """T6: Convergence bound < 1.0 — never reached."""

    def test_max_possible_score(self):
        """Even max LensResult scores → composite < 1.0."""
        results = [make_result(lid, 0.9999) for lid in LENS_ORDER]
        score = bileshke(results)
        self.assertLess(score, 1.0)

    def test_clamp_enforces_bound(self):
        self.assertLess(clamp_score(999.0), 1.0)


class TestT17CompleteBound(unittest.TestCase):
    """T17: Max completeness = 6/7. Ahfâ is permanently unmapped."""

    def test_full_vektor_completeness(self):
        v = LatifeVektor.full()
        self.assertAlmostEqual(v.completeness, 6 / 7)

    def test_cannot_reach_7_of_7(self):
        """Cannot set all 7 bits to 1 — AX53 prevents it."""
        with self.assertRaises(ValueError):
            LatifeVektor(bits=(1, 1, 1, 1, 1, 1, 1))


class TestKV4Convergence(unittest.TestCase):
    """KV₄: 0 < Composite < 1. If ≥ 0.95 → error."""

    def test_score_range(self):
        for s in [0.0, 0.1, 0.5, 0.9, 0.9999]:
            clamped = clamp_score(s)
            self.assertGreaterEqual(clamped, 0.0)
            self.assertLess(clamped, 1.0)

    def test_high_composite_triggers_warning(self):
        results = make_all_results(0.96)
        report = run_bileshke(results)
        self.assertTrue(report.has_kv4_warning)

    def test_normal_composite_no_warning(self):
        results = make_all_results(0.5)
        report = run_bileshke(results)
        self.assertFalse(report.has_kv4_warning)


class TestKV7Independence(unittest.TestCase):
    """KV₇: No shared state between lenses. Bileshke doesn't import lenses."""

    def test_no_lens_imports(self):
        """bileshke package doesn't import from individual lens packages."""
        import bileshke.types as bt
        import bileshke.engine as be
        import bileshke.quality as bq

        # Check that no lens package names appear in the modules' imports
        lens_packages = [
            "kavram_sozlugu", "mereoloji", "fol_formalizasyon",
            "bayes_analiz", "oyun_teorisi", "kategori_teorisi", "holografik",
        ]
        for mod in [bt, be, bq]:
            source_file = mod.__file__
            with open(source_file, "r", encoding="utf-8") as f:
                source = f.read()
            for pkg in lens_packages:
                # Check for actual imports like "from kavram_sozlugu" or "import kavram_sozlugu"
                import_from = f"from {pkg}"
                import_direct = f"import {pkg}"
                self.assertNotIn(
                    import_from, source,
                    f"KV₇ violation: {mod.__name__} imports from {pkg}"
                )
                self.assertNotIn(
                    import_direct, source,
                    f"KV₇ violation: {mod.__name__} imports {pkg}"
                )


class TestAX52MultiplicativeGate(unittest.TestCase):
    """AX52: Gate = ∏ᵢ 𝟙(bᵢ > 0). Zero in any → total collapse."""

    def test_latife_gate_all_ones(self):
        v = LatifeVektor.full()
        self.assertEqual(v.gate_score(), 1.0)

    def test_latife_gate_one_zero(self):
        v = LatifeVektor(bits=(1, 1, 1, 1, 0, 1, 0))
        self.assertEqual(v.gate_score(), 0.0)

    def test_ortam_gate_all_ones(self):
        self.assertEqual(OrtamVektor.full().gate_score(), 1.0)

    def test_ortam_gate_one_zero(self):
        v = OrtamVektor(bits=(1, 1, 0))
        self.assertEqual(v.gate_score(), 0.0)

    def test_dual_gate_both_pass(self):
        gate = compute_coverage_gate(LatifeVektor.full(), OrtamVektor.full())
        self.assertEqual(gate, 1.0)

    def test_dual_gate_one_fails(self):
        gate = compute_coverage_gate(
            LatifeVektor.full(),
            OrtamVektor(bits=(1, 0, 1)),
        )
        self.assertEqual(gate, 0.0)


class TestAX53AhfaInAccessible(unittest.TestCase):
    """AX53: □(φ_L(Ahfâ) = ⊥) — permanently unmapped."""

    def test_tescil_maps_ahfa_to_none(self):
        self.assertIsNone(TESCIL[Latife.AHFA])

    def test_latife_vektor_bit6_always_zero(self):
        v = LatifeVektor.full()
        self.assertEqual(v.bits[6], 0)

    def test_cannot_set_ahfa(self):
        with self.assertRaises(ValueError):
            LatifeVektor(bits=(0, 0, 0, 0, 0, 0, 1))

    def test_from_active_lenses_never_sets_ahfa(self):
        """Even with all 7 lenses active, Ahfâ stays 0."""
        v = LatifeVektor.from_active_lenses(list(LENS_ORDER))
        self.assertEqual(v.bits[6], 0)
        self.assertFalse(v.is_engaged(Latife.AHFA))


class TestAX55DualGate(unittest.TestCase):
    """AX55: PracticalGate = Gate(latife₆) ∧ Gate(medium₃)."""

    def test_both_pass(self):
        rpt = QualityReport(
            latife_vektor=LatifeVektor.full(),
            ortam_vektor=OrtamVektor.full(),
        )
        self.assertEqual(rpt.practical_gate, 1.0)

    def test_latife_fail(self):
        rpt = QualityReport(
            latife_vektor=LatifeVektor(bits=(1, 0, 0, 0, 0, 0, 0)),
            ortam_vektor=OrtamVektor.full(),
        )
        self.assertEqual(rpt.practical_gate, 0.0)

    def test_ortam_fail(self):
        rpt = QualityReport(
            latife_vektor=LatifeVektor.full(),
            ortam_vektor=OrtamVektor(bits=(0, 1, 1)),
        )
        self.assertEqual(rpt.practical_gate, 0.0)


class TestAX56MaxGrade(unittest.TestCase):
    """AX56: Max achievable grade = İlmelyakîn. Hakkalyakîn inaccessible."""

    def test_lens_result_rejects_hakkalyakin(self):
        with self.assertRaises(ValueError):
            LensResult(
                lens_id=LensId.FOL,
                score=0.9,
                grade=EpistemicGrade.HAKKALYAKIN,
            )

    def test_aggregate_never_exceeds_ilmelyakin(self):
        results = [
            make_result(lid, grade=EpistemicGrade.ILMELYAKIN)
            for lid in LENS_ORDER
        ]
        grade = compute_aggregate_grade(results)
        self.assertLessEqual(grade.rank, EpistemicGrade.ILMELYAKIN.rank)

    def test_hakkalyakin_not_accessible(self):
        self.assertFalse(EpistemicGrade.HAKKALYAKIN.is_accessible)

    def test_ilmelyakin_accessible(self):
        self.assertTrue(EpistemicGrade.ILMELYAKIN.is_accessible)


# ========================================================================
# Package import test
# ========================================================================

class TestPackageImport(unittest.TestCase):
    """Verify bileshke package imports work."""

    def test_import_bileshke(self):
        import bileshke
        self.assertTrue(hasattr(bileshke, "bileshke"))
        self.assertTrue(hasattr(bileshke, "LensId"))
        self.assertTrue(hasattr(bileshke, "QualityReport"))

    def test_all_exports(self):
        import bileshke
        for name in bileshke.__all__:
            self.assertTrue(
                hasattr(bileshke, name),
                f"Missing export: {name}"
            )


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