"""
tests/test_bayes_analiz.py — Comprehensive tests for the Bayes Lens (Lens #4).

Covers:
  1. types.py    — Hypothesis, Evidence, PosteriorEntry, BayesModel, enums
  2. verification.py — All check_* functions, verify_all, yakinlasma, framework_summary
  3. constraints.py  — All constraint factories, _parse_model, composite
  4. __init__.py     — Public API completeness
  5. KV₇ independence — No cross-lens imports
  6. Framework axiom compliance (AX21, AX22, AX63, KV₄, T6, AX57)

Test runner: python -m unittest discover -s tests
"""

import ast
import json
import os
import unittest

from bayes_analiz.types import (
    BayesModel,
    CompositionMode,
    Evidence,
    EvidenceType,
    Hypothesis,
    HypothesisStatus,
    PosteriorEntry,
    clamp_score,
)
from bayes_analiz.verification import (
    check_convergence_bound,
    check_evidence_independence,
    check_hypothesis_coverage,
    check_likelihood_validity,
    check_posterior_consistency,
    check_prior_normalisation,
    check_tesanud_infirad,
    check_transparency,
    framework_summary,
    verify_all,
    yakinlasma,
)
from bayes_analiz.constraints import (
    bayes_convergence_score,
    convergence_bound,
    evidence_independence,
    model_transparency,
    posterior_consistency,
    tesanud_infirad,
    valid_bayes_entry,
    valid_likelihoods,
    valid_priors,
)


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

def _make_hypothesis(name="H1", prior=0.5, desc=""):
    return Hypothesis(name=name, prior=prior, description=desc)


def _make_evidence(name="E1", lh=0.8, lhc=0.2, etype=EvidenceType.CONFIRMING,
                   independent=True, desc=""):
    return Evidence(
        name=name,
        likelihood=lh,
        likelihood_complement=lhc,
        evidence_type=etype,
        description=desc,
        source_independent=independent,
    )


def _make_model(name="test_model"):
    """Create a simple valid model with 1 hypothesis and 1 confirming evidence."""
    m = BayesModel(name=name)
    m.add_hypothesis(_make_hypothesis("H1", 0.5))
    m.add_evidence(_make_evidence("E1", 0.8, 0.2))
    m.update("H1", "E1")
    return m


def _model_to_json(model: BayesModel) -> str:
    """Convert a BayesModel to JSON string for constraint testing."""
    hypotheses = []
    for name, h in model.hypotheses.items():
        hypotheses.append({
            "name": name,
            "prior": h.prior,
            "description": h.description,
        })
    evidence = []
    for name, e in model.evidence_items.items():
        evidence.append({
            "name": name,
            "likelihood": e.likelihood,
            "likelihood_complement": e.likelihood_complement,
            "type": e.evidence_type.value,
            "independent": e.source_independent,
            "description": e.description,
        })
    # Reconstruct update sequence from model.updates
    updates = []
    for u in model.updates:
        updates.append({
            "hypothesis": u.hypothesis_name,
            "evidence": u.evidence_name,
        })
    return json.dumps({
        "name": model.name,
        "hypotheses": hypotheses,
        "evidence": evidence,
        "updates": updates,
    })


# ===================================================================
# 1. TYPES — Enums
# ===================================================================

class TestEvidenceType(unittest.TestCase):
    def test_values(self):
        self.assertEqual(EvidenceType.CONFIRMING.value, "confirming")
        self.assertEqual(EvidenceType.DISCONFIRMING.value, "disconfirming")
        self.assertEqual(EvidenceType.NEUTRAL.value, "neutral")
        self.assertEqual(EvidenceType.MIXED.value, "mixed")

    def test_all_members(self):
        self.assertEqual(len(EvidenceType), 4)


class TestHypothesisStatus(unittest.TestCase):
    def test_values(self):
        self.assertEqual(HypothesisStatus.UNTESTED.value, "untested")
        self.assertEqual(HypothesisStatus.SUPPORTED.value, "supported")
        self.assertEqual(HypothesisStatus.WEAKENED.value, "weakened")
        self.assertEqual(HypothesisStatus.INDETERMINATE.value, "indeterminate")


class TestCompositionMode(unittest.TestCase):
    def test_values(self):
        self.assertEqual(CompositionMode.TESANUD.value, "tesanud")
        self.assertEqual(CompositionMode.INFIRAD.value, "infirad")
        self.assertEqual(CompositionMode.INDEPENDENT.value, "independent")


# ===================================================================
# 1. TYPES — clamp_score
# ===================================================================

class TestClampScore(unittest.TestCase):
    def test_normal(self):
        self.assertEqual(clamp_score(0.5), 0.5)

    def test_negative_clamped(self):
        self.assertEqual(clamp_score(-0.1), 0.0)

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

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

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

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


# ===================================================================
# 1. TYPES — Hypothesis
# ===================================================================

class TestHypothesis(unittest.TestCase):
    def test_basic_creation(self):
        h = Hypothesis(name="H1", prior=0.5)
        self.assertEqual(h.name, "H1")
        self.assertEqual(h.prior, 0.5)

    def test_default_prior(self):
        h = Hypothesis(name="H1")
        self.assertEqual(h.prior, 0.5)

    def test_custom_prior(self):
        h = Hypothesis(name="H1", prior=0.3, description="test")
        self.assertEqual(h.prior, 0.3)
        self.assertEqual(h.description, "test")

    def test_frozen(self):
        h = Hypothesis(name="H1")
        with self.assertRaises(AttributeError):
            h.name = "H2"

    def test_empty_name_rejected(self):
        with self.assertRaises(ValueError):
            Hypothesis(name="")

    def test_whitespace_name_rejected(self):
        with self.assertRaises(ValueError):
            Hypothesis(name="   ")

    def test_prior_zero_rejected(self):
        """AX22: 0.0 is unreachable."""
        with self.assertRaises(ValueError):
            Hypothesis(name="H", prior=0.0)

    def test_prior_one_rejected(self):
        """AX22: 1.0 is unreachable."""
        with self.assertRaises(ValueError):
            Hypothesis(name="H", prior=1.0)

    def test_prior_negative_rejected(self):
        with self.assertRaises(ValueError):
            Hypothesis(name="H", prior=-0.1)

    def test_prior_above_one_rejected(self):
        with self.assertRaises(ValueError):
            Hypothesis(name="H", prior=1.5)

    def test_near_zero_prior_valid(self):
        h = Hypothesis(name="H", prior=0.001)
        self.assertAlmostEqual(h.prior, 0.001)

    def test_near_one_prior_valid(self):
        h = Hypothesis(name="H", prior=0.999)
        self.assertAlmostEqual(h.prior, 0.999)


# ===================================================================
# 1. TYPES — Evidence
# ===================================================================

class TestEvidence(unittest.TestCase):
    def test_basic_creation(self):
        e = Evidence(name="E1", likelihood=0.8, likelihood_complement=0.2)
        self.assertEqual(e.name, "E1")
        self.assertEqual(e.likelihood, 0.8)
        self.assertEqual(e.likelihood_complement, 0.2)

    def test_defaults(self):
        e = Evidence(name="E1", likelihood=0.5, likelihood_complement=0.5)
        self.assertEqual(e.evidence_type, EvidenceType.NEUTRAL)
        self.assertTrue(e.source_independent)

    def test_frozen(self):
        e = Evidence(name="E1", likelihood=0.5, likelihood_complement=0.5)
        with self.assertRaises(AttributeError):
            e.name = "E2"

    def test_empty_name_rejected(self):
        with self.assertRaises(ValueError):
            Evidence(name="", likelihood=0.5, likelihood_complement=0.5)

    def test_likelihood_zero_rejected(self):
        with self.assertRaises(ValueError):
            Evidence(name="E", likelihood=0.0, likelihood_complement=0.5)

    def test_likelihood_one_rejected(self):
        with self.assertRaises(ValueError):
            Evidence(name="E", likelihood=1.0, likelihood_complement=0.5)

    def test_complement_zero_rejected(self):
        with self.assertRaises(ValueError):
            Evidence(name="E", likelihood=0.5, likelihood_complement=0.0)

    def test_complement_one_rejected(self):
        with self.assertRaises(ValueError):
            Evidence(name="E", likelihood=0.5, likelihood_complement=1.0)

    def test_bayes_factor_confirming(self):
        e = Evidence(name="E1", likelihood=0.8, likelihood_complement=0.2)
        self.assertAlmostEqual(e.bayes_factor, 4.0)

    def test_bayes_factor_disconfirming(self):
        e = Evidence(name="E1", likelihood=0.2, likelihood_complement=0.8)
        self.assertAlmostEqual(e.bayes_factor, 0.25)

    def test_bayes_factor_neutral(self):
        e = Evidence(name="E1", likelihood=0.5, likelihood_complement=0.5)
        self.assertAlmostEqual(e.bayes_factor, 1.0)


# ===================================================================
# 1. TYPES — PosteriorEntry
# ===================================================================

class TestPosteriorEntry(unittest.TestCase):
    def test_basic_creation(self):
        p = PosteriorEntry(
            hypothesis_name="H1", evidence_name="E1",
            prior=0.5, posterior=0.8, bayes_factor=4.0,
        )
        self.assertEqual(p.hypothesis_name, "H1")
        self.assertAlmostEqual(p.posterior, 0.8)

    def test_posterior_zero_rejected(self):
        with self.assertRaises(ValueError):
            PosteriorEntry(
                hypothesis_name="H", evidence_name="E",
                prior=0.5, posterior=0.0, bayes_factor=1.0,
            )

    def test_posterior_one_rejected(self):
        with self.assertRaises(ValueError):
            PosteriorEntry(
                hypothesis_name="H", evidence_name="E",
                prior=0.5, posterior=1.0, bayes_factor=1.0,
            )

    def test_composition_mode_default(self):
        p = PosteriorEntry(
            hypothesis_name="H", evidence_name="E",
            prior=0.5, posterior=0.5, bayes_factor=1.0,
        )
        self.assertEqual(p.composition_mode, CompositionMode.INDEPENDENT)


# ===================================================================
# 1. TYPES — BayesModel
# ===================================================================

class TestBayesModel(unittest.TestCase):
    def test_empty_model(self):
        m = BayesModel(name="empty")
        self.assertEqual(m.name, "empty")
        self.assertEqual(len(m.hypotheses), 0)
        self.assertEqual(len(m.evidence_items), 0)

    def test_add_hypothesis(self):
        m = BayesModel(name="m")
        h = _make_hypothesis("H1", 0.5)
        m.add_hypothesis(h)
        self.assertIn("H1", m.hypotheses)
        self.assertAlmostEqual(m.current_posteriors["H1"], 0.5)

    def test_duplicate_hypothesis_rejected(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1"))
        with self.assertRaises(ValueError):
            m.add_hypothesis(_make_hypothesis("H1"))

    def test_add_evidence(self):
        m = BayesModel(name="m")
        e = _make_evidence("E1", 0.8, 0.2)
        m.add_evidence(e)
        self.assertIn("E1", m.evidence_items)

    def test_duplicate_evidence_rejected(self):
        m = BayesModel(name="m")
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        with self.assertRaises(ValueError):
            m.add_evidence(_make_evidence("E1", 0.7, 0.3))

    def test_update_basic(self):
        m = _make_model()
        self.assertEqual(len(m.updates), 1)
        # P(H|E) = 0.8*0.5 / (0.8*0.5 + 0.2*0.5) = 0.4/0.5 = 0.8
        self.assertAlmostEqual(m.current_posteriors["H1"], 0.8, places=4)

    def test_update_unknown_hypothesis(self):
        m = BayesModel(name="m")
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        with self.assertRaises(KeyError):
            m.update("nonexistent", "E1")

    def test_update_unknown_evidence(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1"))
        with self.assertRaises(KeyError):
            m.update("H1", "nonexistent")

    def test_sequential_updates(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.9, 0.3))
        m.update("H1", "E1")
        m.update("H1", "E2")
        self.assertEqual(len(m.updates), 2)
        # After E1: 0.8, After E2: 0.9*0.8 / (0.9*0.8 + 0.3*0.2) = 0.72/0.78 ≈ 0.923
        post = m.current_posteriors["H1"]
        self.assertGreater(post, 0.8)
        self.assertLess(post, 1.0)

    def test_update_clamps_posterior(self):
        """AX22/T6: posterior is clamped to (0.0001, 0.9999)."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.9))
        m.add_evidence(_make_evidence("E1", 0.99, 0.01))
        entry = m.update("H1", "E1")
        self.assertLessEqual(entry.posterior, 0.9999)

    def test_get_posterior(self):
        m = _make_model()
        self.assertAlmostEqual(m.get_posterior("H1"), 0.8, places=4)

    def test_get_posterior_unknown(self):
        m = BayesModel(name="m")
        with self.assertRaises(KeyError):
            m.get_posterior("nonexistent")

    def test_get_updates_for(self):
        m = _make_model()
        updates = m.get_updates_for("H1")
        self.assertEqual(len(updates), 1)
        self.assertEqual(updates[0].hypothesis_name, "H1")

    def test_get_status_untested(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1"))
        self.assertEqual(m.get_status("H1"), HypothesisStatus.UNTESTED)

    def test_get_status_supported(self):
        m = _make_model()  # confirming evidence
        self.assertEqual(m.get_status("H1"), HypothesisStatus.SUPPORTED)

    def test_get_status_weakened(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.2, 0.8,
                                      etype=EvidenceType.DISCONFIRMING))
        m.update("H1", "E1")
        self.assertEqual(m.get_status("H1"), HypothesisStatus.WEAKENED)

    def test_get_status_unknown_hypothesis(self):
        m = BayesModel(name="m")
        with self.assertRaises(KeyError):
            m.get_status("nonexistent")

    def test_count_independent_confirmations(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.7, 0.3))
        m.update("H1", "E1")
        m.update("H1", "E2")
        count = m.count_independent_confirmations("H1")
        self.assertEqual(count, 2)

    def test_count_independent_no_confirmations(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        self.assertEqual(m.count_independent_confirmations("H1"), 0)

    def test_tesanud_strength_zero(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        self.assertEqual(m.tesanud_strength("H1"), 0.0)

    def test_tesanud_strength_bounded(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.7, 0.3))
        m.add_evidence(_make_evidence("E3", 0.75, 0.25))
        m.update("H1", "E1")
        m.update("H1", "E2")
        m.update("H1", "E3")
        strength = m.tesanud_strength("H1")
        self.assertGreater(strength, 0.0)
        self.assertLess(strength, 1.0)  # KV₄/T6

    def test_to_dict(self):
        m = _make_model()
        d = m.to_dict()
        self.assertIn("name", d)
        self.assertIn("hypotheses", d)
        self.assertIn("evidence_items", d)
        self.assertIn("update_count", d)
        self.assertEqual(d["update_count"], 1)

    def test_to_dict_hypothesis_fields(self):
        m = _make_model()
        h_data = m.to_dict()["hypotheses"]["H1"]
        self.assertIn("prior", h_data)
        self.assertIn("current_posterior", h_data)
        self.assertIn("status", h_data)

    def test_composition_mode_tesanud(self):
        """AX63: confirming + independent → TESANUD."""
        m = _make_model()
        entry = m.updates[0]
        self.assertEqual(entry.composition_mode, CompositionMode.TESANUD)

    def test_composition_mode_infirad(self):
        """AX63: disconfirming → INFIRAD."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.2, 0.8,
                                      etype=EvidenceType.DISCONFIRMING))
        m.update("H1", "E1")
        entry = m.updates[0]
        self.assertEqual(entry.composition_mode, CompositionMode.INFIRAD)

    def test_composition_mode_independent(self):
        """Neutral BF → INDEPENDENT."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.5, 0.5,
                                      etype=EvidenceType.NEUTRAL))
        m.update("H1", "E1")
        entry = m.updates[0]
        self.assertEqual(entry.composition_mode, CompositionMode.INDEPENDENT)


# ===================================================================
# 2. VERIFICATION — Individual checks
# ===================================================================

class TestCheckPriorNormalisation(unittest.TestCase):
    def test_valid_model(self):
        m = _make_model()
        passed, _ = check_prior_normalisation(m)
        self.assertTrue(passed)

    def test_no_hypotheses(self):
        m = BayesModel(name="m")
        passed, _ = check_prior_normalisation(m)
        self.assertFalse(passed)


class TestCheckLikelihoodValidity(unittest.TestCase):
    def test_valid_model(self):
        m = _make_model()
        passed, _ = check_likelihood_validity(m)
        self.assertTrue(passed)

    def test_no_evidence(self):
        m = BayesModel(name="m")
        passed, _ = check_likelihood_validity(m)
        self.assertFalse(passed)


class TestCheckEvidenceIndependence(unittest.TestCase):
    def test_all_independent(self):
        m = _make_model()
        passed, _ = check_evidence_independence(m)
        self.assertTrue(passed)

    def test_non_independent_flagged(self):
        m = BayesModel(name="m")
        m.add_evidence(_make_evidence("E1", 0.8, 0.2, independent=False))
        passed, detail = check_evidence_independence(m)
        self.assertFalse(passed)
        self.assertIn("KV₇", detail)

    def test_no_evidence_passes(self):
        m = BayesModel(name="m")
        passed, _ = check_evidence_independence(m)
        self.assertTrue(passed)


class TestCheckPosteriorConsistency(unittest.TestCase):
    def test_valid_updates(self):
        m = _make_model()
        passed, _ = check_posterior_consistency(m)
        self.assertTrue(passed)

    def test_no_updates(self):
        m = BayesModel(name="m")
        passed, _ = check_posterior_consistency(m)
        self.assertTrue(passed)

    def test_multiple_updates_consistent(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.7, 0.3))
        m.update("H1", "E1")
        m.update("H1", "E2")
        passed, _ = check_posterior_consistency(m)
        self.assertTrue(passed)


class TestCheckConvergenceBound(unittest.TestCase):
    def test_normal_model(self):
        m = _make_model()
        passed, _ = check_convergence_bound(m)
        self.assertTrue(passed)

    def test_high_posterior_flagged(self):
        """KV₄: posterior ≥ 0.95 should be flagged."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.9))
        m.add_evidence(_make_evidence("E1", 0.99, 0.01))
        m.update("H1", "E1")
        # Posterior should be clamped to 0.9999 but still ≥ 0.95
        passed, detail = check_convergence_bound(m)
        self.assertFalse(passed)
        self.assertIn("KV₄", detail)


class TestCheckTesanudInfirad(unittest.TestCase):
    def test_valid_labels(self):
        m = _make_model()
        passed, _ = check_tesanud_infirad(m)
        self.assertTrue(passed)

    def test_no_updates(self):
        m = BayesModel(name="m")
        passed, _ = check_tesanud_infirad(m)
        self.assertTrue(passed)


class TestCheckHypothesisCoverage(unittest.TestCase):
    def test_sufficient_coverage(self):
        m = _make_model()
        passed, _ = check_hypothesis_coverage(m)
        self.assertTrue(passed)

    def test_insufficient_coverage(self):
        m = BayesModel(name="m")
        passed, _ = check_hypothesis_coverage(m, min_hypotheses=1)
        self.assertFalse(passed)

    def test_uncovered_reported(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1"))
        m.add_hypothesis(_make_hypothesis("H2", 0.3))
        passed, detail = check_hypothesis_coverage(m)
        self.assertTrue(passed)
        self.assertIn("without evidence", detail)


class TestCheckTransparency(unittest.TestCase):
    def test_valid_model(self):
        m = _make_model()
        passed, _ = check_transparency(m)
        self.assertTrue(passed)

    def test_empty_model(self):
        m = BayesModel(name="m")
        passed, _ = check_transparency(m)
        self.assertTrue(passed)


# ===================================================================
# 2. VERIFICATION — verify_all
# ===================================================================

class TestVerifyAll(unittest.TestCase):
    def test_returns_all_checks(self):
        m = _make_model()
        results = verify_all(m)
        expected_keys = {
            "prior_normalisation", "likelihood_validity",
            "evidence_independence", "posterior_consistency",
            "convergence_bound", "tesanud_infirad",
            "hypothesis_coverage", "transparency",
        }
        self.assertEqual(set(results.keys()), expected_keys)

    def test_valid_model_all_pass(self):
        m = _make_model()
        results = verify_all(m)
        for check_name, (passed, _) in results.items():
            if check_name == "convergence_bound":
                # May warn if posterior high — skip
                continue
            self.assertTrue(passed, f"{check_name} should pass")


# ===================================================================
# 2. VERIFICATION — yakinlasma
# ===================================================================

class TestYakinlasma(unittest.TestCase):
    def test_none_returns_zero(self):
        self.assertEqual(yakinlasma(None), 0.0)

    def test_valid_model_positive(self):
        m = _make_model()
        score = yakinlasma(m)
        self.assertGreater(score, 0.0)

    def test_score_bounded(self):
        """T6/KV₄: score must be < 1.0."""
        m = _make_model()
        score = yakinlasma(m)
        self.assertLess(score, 1.0)

    def test_empty_model_lower_than_valid(self):
        m_empty = BayesModel(name="m")
        m_valid = _make_model()
        score_empty = yakinlasma(m_empty)
        score_valid = yakinlasma(m_valid)
        # Empty model should score lower than a valid model
        self.assertLess(score_empty, score_valid)


# ===================================================================
# 2. VERIFICATION — framework_summary
# ===================================================================

class TestFrameworkSummary(unittest.TestCase):
    def test_none_model(self):
        s = framework_summary(None)
        self.assertEqual(s["lens"], "Bayes")
        self.assertEqual(s["faculty"], "Kalb")
        self.assertEqual(s["convergence_score"], 0.0)

    def test_valid_model(self):
        m = _make_model()
        s = framework_summary(m)
        self.assertEqual(s["lens"], "Bayes")
        self.assertEqual(s["faculty"], "Kalb")
        self.assertGreater(s["convergence_score"], 0.0)
        self.assertLess(s["convergence_score"], 1.0)
        self.assertEqual(s["hypothesis_count"], 1)
        self.assertEqual(s["evidence_count"], 1)
        self.assertEqual(s["update_count"], 1)

    def test_summary_has_checks(self):
        m = _make_model()
        s = framework_summary(m)
        self.assertIn("checks", s)
        for check_name, check_data in s["checks"].items():
            self.assertIn("passed", check_data)
            self.assertIn("detail", check_data)

    def test_summary_has_tesanud_counts(self):
        m = _make_model()
        s = framework_summary(m)
        self.assertIn("tesanud_counts", s)
        self.assertIn("H1", s["tesanud_counts"])


# ===================================================================
# 3. CONSTRAINTS — Individual factories
# ===================================================================

class TestConstraintValidPriors(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = valid_priors()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)

    def test_invalid_json(self):
        c = valid_priors()
        result = c.check("not json")
        self.assertFalse(result.passed)

    def test_empty_model(self):
        c = valid_priors()
        result = c.check(json.dumps({"name": "m"}))
        self.assertFalse(result.passed)


class TestConstraintValidLikelihoods(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = valid_likelihoods()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)

    def test_no_evidence(self):
        c = valid_likelihoods()
        j = json.dumps({
            "name": "m",
            "hypotheses": [{"name": "H1", "prior": 0.5}],
        })
        result = c.check(j)
        self.assertFalse(result.passed)


class TestConstraintEvidenceIndependence(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = evidence_independence()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)

    def test_non_independent(self):
        c = evidence_independence()
        j = json.dumps({
            "name": "m",
            "evidence": [{
                "name": "E1",
                "likelihood": 0.8,
                "likelihood_complement": 0.2,
                "independent": False,
            }],
        })
        result = c.check(j)
        self.assertFalse(result.passed)


class TestConstraintPosteriorConsistency(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = posterior_consistency()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)


class TestConstraintConvergenceBound(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = convergence_bound()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)


class TestConstraintTesanudInfirad(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = tesanud_infirad()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)


class TestConstraintModelTransparency(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = model_transparency()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)


class TestConstraintBayesConvergenceScore(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = bayes_convergence_score()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)
        self.assertGreater(result.score, 0.0)
        self.assertLess(result.score, 1.0)


class TestConstraintValidBayesEntry(unittest.TestCase):
    def test_valid(self):
        m = _make_model()
        c = valid_bayes_entry()
        result = c.check(_model_to_json(m))
        self.assertTrue(result.passed)

    def test_invalid_json(self):
        c = valid_bayes_entry()
        result = c.check("broken")
        self.assertFalse(result.passed)

    def test_score_bounded(self):
        m = _make_model()
        c = valid_bayes_entry()
        result = c.check(_model_to_json(m))
        self.assertLess(result.score, 1.0)


# ===================================================================
# 4. __init__.py — Public API completeness
# ===================================================================

class TestPublicAPI(unittest.TestCase):
    def test_types_importable(self):
        import bayes_analiz
        for name in ["Hypothesis", "Evidence", "PosteriorEntry", "BayesModel",
                      "EvidenceType", "HypothesisStatus", "CompositionMode",
                      "clamp_score"]:
            self.assertTrue(hasattr(bayes_analiz, name), f"Missing: {name}")

    def test_verification_importable(self):
        import bayes_analiz
        for name in ["check_prior_normalisation", "check_likelihood_validity",
                      "check_evidence_independence", "check_posterior_consistency",
                      "check_convergence_bound", "check_tesanud_infirad",
                      "check_hypothesis_coverage", "check_transparency",
                      "verify_all", "yakinlasma", "framework_summary"]:
            self.assertTrue(hasattr(bayes_analiz, name), f"Missing: {name}")

    def test_constraints_importable(self):
        import bayes_analiz
        for name in ["valid_priors", "valid_likelihoods",
                      "evidence_independence", "posterior_consistency",
                      "convergence_bound", "tesanud_infirad",
                      "model_transparency", "bayes_convergence_score",
                      "valid_bayes_entry"]:
            self.assertTrue(hasattr(bayes_analiz, name), f"Missing: {name}")


# ===================================================================
# 5. KV₇ — Independence (no cross-lens imports)
# ===================================================================

class TestKV7Independence(unittest.TestCase):
    """KV₇ (İhlâs): bayes_analiz must not import from other lenses."""

    OTHER_LENSES = {"kavram_sozlugu", "mereoloji", "fol_formalizasyon",
                    "kategori_teorisi", "oyun_teorisi", "holografik"}

    def _check_file(self, filepath: str):
        with open(filepath, "r", encoding="utf-8") as f:
            source = f.read()
        tree = ast.parse(source)
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    top = alias.name.split(".")[0]
                    self.assertNotIn(
                        top, self.OTHER_LENSES,
                        f"KV₇ violation: {filepath} imports {alias.name}",
                    )
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.level == 0:
                    top = node.module.split(".")[0]
                    self.assertNotIn(
                        top, self.OTHER_LENSES,
                        f"KV₇ violation: {filepath} imports from {node.module}",
                    )

    def test_types_independence(self):
        self._check_file(os.path.join("bayes_analiz", "types.py"))

    def test_verification_independence(self):
        self._check_file(os.path.join("bayes_analiz", "verification.py"))

    def test_constraints_independence(self):
        self._check_file(os.path.join("bayes_analiz", "constraints.py"))

    def test_init_independence(self):
        self._check_file(os.path.join("bayes_analiz", "__init__.py"))


# ===================================================================
# 6. Framework Axiom Compliance
# ===================================================================

class TestAX21ContinuousDegrees(unittest.TestCase):
    """AX21: Tecelli degrees are continuous (float), not binary."""

    def test_prior_is_float(self):
        h = _make_hypothesis("H1", 0.3)
        self.assertIsInstance(h.prior, float)

    def test_posterior_is_float(self):
        m = _make_model()
        post = m.get_posterior("H1")
        self.assertIsInstance(post, float)

    def test_score_is_float(self):
        m = _make_model()
        score = yakinlasma(m)
        self.assertIsInstance(score, float)


class TestAX22UnreachableSupremum(unittest.TestCase):
    """AX22: No score reaches 0.0 or 1.0 (extremes are unreachable)."""

    def test_posterior_never_zero(self):
        """Even with strong disconfirming evidence, posterior > 0."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.01, 0.99,
                                      etype=EvidenceType.DISCONFIRMING))
        m.update("H1", "E1")
        self.assertGreater(m.get_posterior("H1"), 0.0)

    def test_posterior_never_one(self):
        """Even with strong confirming evidence, posterior < 1."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.9))
        m.add_evidence(_make_evidence("E1", 0.99, 0.01))
        m.update("H1", "E1")
        self.assertLess(m.get_posterior("H1"), 1.0)


class TestAX63TesanudInfiradAsymmetry(unittest.TestCase):
    """AX63: Independent confirmations compound; denials stay isolated."""

    def test_multiple_confirmations_compound(self):
        """Multiple independent confirmations should raise posterior monotonically."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.7, 0.3))
        m.update("H1", "E1")
        after_one = m.get_posterior("H1")
        m.update("H1", "E2")
        after_two = m.get_posterior("H1")
        self.assertGreater(after_two, after_one)

    def test_confirmation_modes_tesanud(self):
        """Independent confirming → all TESANUD."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.8, 0.2))
        m.add_evidence(_make_evidence("E2", 0.7, 0.3))
        m.update("H1", "E1")
        m.update("H1", "E2")
        for u in m.updates:
            self.assertEqual(u.composition_mode, CompositionMode.TESANUD)

    def test_denial_isolated(self):
        """Disconfirming → INFIRAD."""
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        m.add_evidence(_make_evidence("E1", 0.2, 0.8,
                                      etype=EvidenceType.DISCONFIRMING))
        m.update("H1", "E1")
        self.assertEqual(m.updates[0].composition_mode, CompositionMode.INFIRAD)


class TestKV4ConvergenceBound(unittest.TestCase):
    """KV₄/T6: All scores in [0, 1), never reaching 1.0."""

    def test_yakinlasma_bounded(self):
        m = _make_model()
        score = yakinlasma(m)
        self.assertGreaterEqual(score, 0.0)
        self.assertLess(score, 1.0)

    def test_clamp_enforces_bound(self):
        self.assertEqual(clamp_score(1.0), 0.9999)
        self.assertEqual(clamp_score(2.0), 0.9999)

    def test_tesanud_strength_bounded(self):
        m = BayesModel(name="m")
        m.add_hypothesis(_make_hypothesis("H1", 0.5))
        for i in range(10):
            m.add_evidence(_make_evidence(f"E{i}", 0.8, 0.2))
        for i in range(10):
            m.update("H1", f"E{i}")
        strength = m.tesanud_strength("H1")
        self.assertLess(strength, 1.0)


class TestAX57Transparency(unittest.TestCase):
    """AX57: Every output must include transparency disclosure."""

    def test_framework_summary_has_all_keys(self):
        m = _make_model()
        s = framework_summary(m)
        required = {"lens", "faculty", "convergence_score", "checks",
                    "hypothesis_count", "evidence_count", "update_count",
                    "tesanud_counts"}
        self.assertEqual(required, set(s.keys()) & required)

    def test_model_to_dict_has_required_keys(self):
        m = _make_model()
        d = m.to_dict()
        for key in ["name", "hypotheses", "evidence_items", "update_count"]:
            self.assertIn(key, d)


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