"""
tests/test_kategori_teorisi.py — Tests for the KategoriTeorisi Lens (Lens #6).

Coverage:
  - TestCatObject:                    CatObject construction, frozen, validation
  - TestMorphism:                     Morphism construction, is_identity, is_endomorphism
  - TestMorphismKind:                 Enum completeness
  - TestCategoryBasic:                add_object, add_morphism, hom, compose, identity, to_dict
  - TestCategoryErrors:               Validation errors (missing objects, empty names)
  - TestFunctor:                      map_object, map_morphism, F_obj, F_mor, to_dict
  - TestFunctorErrors:                Validation errors (unknown objects/morphisms)
  - TestNaturalTransformation:        set_component, properties, validation
  - TestCategoryValidity:             has_identities, check_composition_defined, is_valid_category
  - TestFunctorVerification:          check_objs/mors, is_faithful, is_full, identity_pres, verify
  - TestNaturalityVerification:       check_naturality, check_component_coverage, verify
  - TestVahidiyet:                    AX12 connectedness (connected/disconnected/trivial)
  - TestEhadiyet:                     AX13 local reflection (all connected/isolated/partial)
  - TestYakinlasma:                   Convergence score, upper bound, partial inputs
  - TestClampScore:                   clamp_score edge cases
  - TestConstraintCategoryValidity:   category_validity constraint factory
  - TestConstraintFunctorFaith:       functor_faithfulness constraint factory
  - TestConstraintVahidiyet:          vahidiyet_containment constraint factory
  - TestConstraintEhadiyet:           ehadiyet_reflection constraint factory
  - TestConstraintNaturality:         naturality_check constraint factory
  - TestConstraintConvergenceBound:   category_convergence_bound constraint factory
  - TestConstraintValidEntry:         valid_category_entry composite constraint
  - TestKV7Independence:              No shared mutable state with other lenses
  - TestFrameworkSummary:             framework_summary metadata
"""

import unittest
import json
import ast
import os
import sys


# ---------------------------------------------------------------------------
# Test helpers
# ---------------------------------------------------------------------------

def _obj(name, sort="", tags=()):
    """Shortcut to build a CatObject."""
    from kategori_teorisi.types import CatObject
    return CatObject(name=name, sort=sort, tags=tags)


def _mor(name, src, tgt, kind=None):
    """Shortcut to build a Morphism."""
    from kategori_teorisi.types import Morphism, MorphismKind
    if kind is None:
        kind = MorphismKind.DERIVATION
    return Morphism(name=name, source=src, target=tgt, kind=kind)


def _make_triangle():
    """Build a valid triangle category: A→B→C with composition."""
    from kategori_teorisi.types import Category, CategoryRole
    cat = Category(name="Triangle", role=CategoryRole.CUSTOM)
    a, b, c = _obj("A"), _obj("B"), _obj("C")
    cat.add_object(a)
    cat.add_object(b)
    cat.add_object(c)
    f = _mor("f", a, b)
    g = _mor("g", b, c)
    gf = _mor("gf", a, c)
    cat.add_morphism(f)
    cat.add_morphism(g)
    cat.add_morphism(gf)
    cat.set_composition("f", "g", "gf")
    return cat, a, b, c, f, g, gf


def _make_pair():
    """Build a simple 2-object category: A→B."""
    from kategori_teorisi.types import Category
    cat = Category(name="Pair")
    a, b = _obj("A"), _obj("B")
    cat.add_object(a)
    cat.add_object(b)
    f = _mor("f", a, b)
    cat.add_morphism(f)
    return cat, a, b, f


def _make_disconnected():
    """Build a disconnected 2-object category (identities only)."""
    from kategori_teorisi.types import Category
    cat = Category(name="Disconnected")
    a, b = _obj("A"), _obj("B")
    cat.add_object(a)
    cat.add_object(b)
    return cat, a, b


def _make_rep_hakikat_functor():
    """Build Rep → Hakikat functor with 2 objects each, faithful mapping."""
    from kategori_teorisi.types import Category, CategoryRole, Functor
    # Rep category
    rep = Category(name="Rep", role=CategoryRole.REP)
    r1, r2 = _obj("AX1", sort="S_Kavram"), _obj("AX2", sort="S_Kavram")
    rep.add_object(r1)
    rep.add_object(r2)
    f_rep = _mor("derives", r1, r2)
    rep.add_morphism(f_rep)

    # Hakikat category
    hak = Category(name="Hakikat", role=CategoryRole.HAKIKAT)
    h1, h2 = _obj("Hakim", sort="S_Isim"), _obj("Alim", sort="S_Isim")
    hak.add_object(h1)
    hak.add_object(h2)
    f_hak = _mor("tecelli_link", h1, h2)
    hak.add_morphism(f_hak)

    # Functor
    F = Functor(name="F", source_cat=rep, target_cat=hak)
    F.map_object("AX1", "Hakim")
    F.map_object("AX2", "Alim")
    F.map_morphism("id_AX1", "id_Hakim")
    F.map_morphism("id_AX2", "id_Alim")
    F.map_morphism("derives", "tecelli_link")
    return rep, hak, F


def _cat_to_json(cat):
    """Convert a Category to JSON-friendly dict for constraint tests."""
    objs = []
    for o in sorted(cat.objects, key=lambda o: o.name):
        objs.append({"name": o.name, "sort": o.sort, "tags": list(o.tags)})
    mors = []
    for m in sorted(cat.morphisms, key=lambda m: m.name):
        mors.append({
            "name": m.name,
            "source": m.source.name,
            "target": m.target.name,
            "kind": m.kind.value,
        })
    comps = []
    for (f_name, g_name), gf_name in cat._composition.items():
        comps.append({"f": f_name, "g": g_name, "gf": gf_name})
    return {
        "name": cat.name,
        "role": cat.role.value,
        "objects": objs,
        "morphisms": mors,
        "compositions": comps,
    }


# =========================================================================
# CatObject
# =========================================================================

class TestCatObject(unittest.TestCase):
    """Test CatObject construction and invariants."""

    def test_basic_construction(self):
        o = _obj("Hakim")
        self.assertEqual(o.name, "Hakim")
        self.assertEqual(o.sort, "")
        self.assertEqual(o.tags, ())

    def test_with_sort_and_tags(self):
        o = _obj("Hakim", sort="S_Isim", tags=("divine",))
        self.assertEqual(o.sort, "S_Isim")
        self.assertEqual(o.tags, ("divine",))

    def test_frozen(self):
        o = _obj("X")
        with self.assertRaises(AttributeError):
            o.name = "Y"

    def test_empty_name_raises(self):
        from kategori_teorisi.types import CatObject
        with self.assertRaises(ValueError):
            CatObject(name="")

    def test_equality(self):
        a = _obj("A", sort="S_Isim")
        b = _obj("A", sort="S_Isim")
        self.assertEqual(a, b)

    def test_inequality(self):
        a = _obj("A")
        b = _obj("B")
        self.assertNotEqual(a, b)

    def test_hashable(self):
        s = {_obj("A"), _obj("A"), _obj("B")}
        self.assertEqual(len(s), 2)


# =========================================================================
# Morphism
# =========================================================================

class TestMorphism(unittest.TestCase):
    """Test Morphism construction and properties."""

    def test_basic_construction(self):
        a, b = _obj("A"), _obj("B")
        m = _mor("f", a, b)
        self.assertEqual(m.name, "f")
        self.assertEqual(m.source, a)
        self.assertEqual(m.target, b)

    def test_empty_name_raises(self):
        from kategori_teorisi.types import Morphism, MorphismKind
        a = _obj("A")
        with self.assertRaises(ValueError):
            Morphism(name="", source=a, target=a, kind=MorphismKind.IDENTITY)

    def test_is_identity_true(self):
        from kategori_teorisi.types import MorphismKind
        a = _obj("A")
        m = _mor("id_A", a, a, kind=MorphismKind.IDENTITY)
        self.assertTrue(m.is_identity)

    def test_is_identity_false_different_kind(self):
        a = _obj("A")
        m = _mor("endo", a, a)
        self.assertFalse(m.is_identity)

    def test_is_identity_false_different_endpoints(self):
        from kategori_teorisi.types import MorphismKind
        a, b = _obj("A"), _obj("B")
        m = _mor("f", a, b, kind=MorphismKind.IDENTITY)
        self.assertFalse(m.is_identity)

    def test_is_endomorphism(self):
        a = _obj("A")
        m = _mor("endo", a, a)
        self.assertTrue(m.is_endomorphism)

    def test_not_endomorphism(self):
        a, b = _obj("A"), _obj("B")
        m = _mor("f", a, b)
        self.assertFalse(m.is_endomorphism)

    def test_frozen(self):
        a = _obj("A")
        m = _mor("f", a, a)
        with self.assertRaises(AttributeError):
            m.name = "g"

    def test_default_kind_is_derivation(self):
        from kategori_teorisi.types import MorphismKind
        a, b = _obj("A"), _obj("B")
        m = _mor("f", a, b)
        self.assertEqual(m.kind, MorphismKind.DERIVATION)


# =========================================================================
# MorphismKind enum
# =========================================================================

class TestMorphismKind(unittest.TestCase):
    """Test MorphismKind enum completeness."""

    def test_all_kinds(self):
        from kategori_teorisi.types import MorphismKind
        expected = {
            "IDENTITY", "DERIVATION", "DEPENDENCY", "TECELLI",
            "KASKAD", "DELALET", "COMPOSITION",
        }
        actual = {k.name for k in MorphismKind}
        self.assertEqual(actual, expected)

    def test_functor_property_enum(self):
        from kategori_teorisi.types import FunctorProperty
        expected = {"FAITHFUL", "FULL", "ESSENTIALLY_SURJECTIVE", "NATURAL"}
        actual = {k.name for k in FunctorProperty}
        self.assertEqual(actual, expected)

    def test_category_role_enum(self):
        from kategori_teorisi.types import CategoryRole
        expected = {"REP", "HAKIKAT", "CUSTOM"}
        actual = {k.name for k in CategoryRole}
        self.assertEqual(actual, expected)


# =========================================================================
# Category basics
# =========================================================================

class TestCategoryBasic(unittest.TestCase):
    """Test Category construction, mutations, and queries."""

    def test_empty_category(self):
        from kategori_teorisi.types import Category
        cat = Category(name="Empty")
        self.assertEqual(cat.object_count(), 0)
        self.assertEqual(cat.morphism_count(), 0)

    def test_add_object_creates_identity(self):
        from kategori_teorisi.types import Category
        cat = Category(name="C")
        a = _obj("A")
        cat.add_object(a)
        self.assertEqual(cat.object_count(), 1)
        # Should have identity morphism now
        self.assertEqual(cat.morphism_count(), 1)
        id_a = cat.identity_for(a)
        self.assertIsNotNone(id_a)
        self.assertTrue(id_a.is_identity)

    def test_add_morphism(self):
        cat, a, b, f = _make_pair()
        self.assertEqual(cat.morphism_count(), 3)  # id_A, id_B, f
        self.assertIn(f, cat.morphisms)

    def test_hom_set(self):
        cat, a, b, f = _make_pair()
        hom_ab = cat.hom(a, b)
        self.assertEqual(len(hom_ab), 1)
        self.assertIn(f, hom_ab)
        # Reverse hom should be empty
        hom_ba = cat.hom(b, a)
        self.assertEqual(len(hom_ba), 0)

    def test_identity_in_hom(self):
        cat, a, b, f = _make_pair()
        hom_aa = cat.hom(a, a)
        self.assertEqual(len(hom_aa), 1)
        self.assertTrue(list(hom_aa)[0].is_identity)

    def test_compose(self):
        cat, a, b, c, f, g, gf = _make_triangle()
        result = cat.compose("f", "g")
        self.assertEqual(result, "gf")

    def test_compose_undeclared(self):
        cat, a, b, f = _make_pair()
        result = cat.compose("f", "nonexistent")
        self.assertIsNone(result)

    def test_get_object(self):
        cat, a, b, f = _make_pair()
        self.assertEqual(cat.get_object("A"), a)
        self.assertIsNone(cat.get_object("Z"))

    def test_get_morphism(self):
        cat, a, b, f = _make_pair()
        self.assertEqual(cat.get_morphism("f"), f)
        self.assertIsNone(cat.get_morphism("nonexistent"))

    def test_non_identity_morphisms(self):
        cat, a, b, c, f, g, gf = _make_triangle()
        non_id = cat.non_identity_morphisms()
        self.assertIn(f, non_id)
        self.assertIn(g, non_id)
        self.assertIn(gf, non_id)
        # Identities should NOT be present
        for m in non_id:
            self.assertFalse(m.is_identity)

    def test_to_dict(self):
        cat, a, b, f = _make_pair()
        d = cat.to_dict()
        self.assertEqual(d["name"], "Pair")
        self.assertEqual(d["object_count"], 2)
        self.assertEqual(d["morphism_count"], 3)
        self.assertIn("A", d["objects"])
        self.assertIn("B", d["objects"])

    def test_objects_is_frozenset(self):
        cat, a, b, f = _make_pair()
        self.assertIsInstance(cat.objects, frozenset)

    def test_morphisms_is_frozenset(self):
        cat, a, b, f = _make_pair()
        self.assertIsInstance(cat.morphisms, frozenset)


# =========================================================================
# Category errors
# =========================================================================

class TestCategoryErrors(unittest.TestCase):
    """Test error handling in Category."""

    def test_add_morphism_unknown_source(self):
        from kategori_teorisi.types import Category
        cat = Category(name="C")
        a = _obj("A")
        b = _obj("B")
        cat.add_object(b)
        m = _mor("f", a, b)
        with self.assertRaises(ValueError):
            cat.add_morphism(m)

    def test_add_morphism_unknown_target(self):
        from kategori_teorisi.types import Category
        cat = Category(name="C")
        a = _obj("A")
        b = _obj("B")
        cat.add_object(a)
        m = _mor("f", a, b)
        with self.assertRaises(ValueError):
            cat.add_morphism(m)


# =========================================================================
# Functor
# =========================================================================

class TestFunctor(unittest.TestCase):
    """Test Functor construction and mapping."""

    def test_basic_functor(self):
        rep, hak, F = _make_rep_hakikat_functor()
        self.assertEqual(F.name, "F")
        self.assertIs(F.source_cat, rep)
        self.assertIs(F.target_cat, hak)

    def test_map_object(self):
        rep, hak, F = _make_rep_hakikat_functor()
        self.assertEqual(F.F_obj("AX1"), "Hakim")
        self.assertEqual(F.F_obj("AX2"), "Alim")

    def test_map_morphism(self):
        rep, hak, F = _make_rep_hakikat_functor()
        self.assertEqual(F.F_mor("derives"), "tecelli_link")

    def test_identity_mapping(self):
        rep, hak, F = _make_rep_hakikat_functor()
        self.assertEqual(F.F_mor("id_AX1"), "id_Hakim")
        self.assertEqual(F.F_mor("id_AX2"), "id_Alim")

    def test_unmapped_returns_none(self):
        rep, hak, F = _make_rep_hakikat_functor()
        self.assertIsNone(F.F_obj("nonexistent"))
        self.assertIsNone(F.F_mor("nonexistent"))

    def test_to_dict(self):
        rep, hak, F = _make_rep_hakikat_functor()
        d = F.to_dict()
        self.assertEqual(d["name"], "F")
        self.assertEqual(d["source"], "Rep")
        self.assertEqual(d["target"], "Hakikat")
        self.assertIn("AX1", d["object_map"])


# =========================================================================
# Functor errors
# =========================================================================

class TestFunctorErrors(unittest.TestCase):
    """Test validation errors in Functor."""

    def test_map_object_unknown_source(self):
        rep, hak, F = _make_rep_hakikat_functor()
        with self.assertRaises(ValueError):
            F.map_object("nonexistent", "Hakim")

    def test_map_object_unknown_target(self):
        rep, hak, F = _make_rep_hakikat_functor()
        with self.assertRaises(ValueError):
            F.map_object("AX1", "nonexistent")

    def test_map_morphism_unknown_source(self):
        rep, hak, F = _make_rep_hakikat_functor()
        with self.assertRaises(ValueError):
            F.map_morphism("nonexistent", "tecelli_link")

    def test_map_morphism_unknown_target(self):
        rep, hak, F = _make_rep_hakikat_functor()
        with self.assertRaises(ValueError):
            F.map_morphism("derives", "nonexistent")


# =========================================================================
# NaturalTransformation
# =========================================================================

class TestNaturalTransformation(unittest.TestCase):
    """Test NaturalTransformation construction and validation."""

    def _make_eta(self):
        """Build η: F ⇒ G with matching categories."""
        from kategori_teorisi.types import (
            Category, CategoryRole, Functor, NaturalTransformation,
        )
        src = Category(name="C", role=CategoryRole.CUSTOM)
        tgt = Category(name="D", role=CategoryRole.CUSTOM)
        a = _obj("A")
        src.add_object(a)
        fa = _obj("FA")
        ga = _obj("GA")
        tgt.add_object(fa)
        tgt.add_object(ga)
        eta_a_mor = _mor("eta_A", fa, ga)
        tgt.add_morphism(eta_a_mor)

        F = Functor(name="F", source_cat=src, target_cat=tgt)
        F.map_object("A", "FA")
        F.map_morphism("id_A", "id_FA")

        G = Functor(name="G", source_cat=src, target_cat=tgt)
        G.map_object("A", "GA")
        G.map_morphism("id_A", "id_GA")

        eta = NaturalTransformation(name="eta", source_functor=F, target_functor=G)
        eta.set_component("A", "eta_A")
        return eta, src, tgt

    def test_basic_construction(self):
        eta, src, tgt = self._make_eta()
        self.assertEqual(eta.name, "eta")
        self.assertIs(eta.source_category, src)
        self.assertIs(eta.target_category, tgt)

    def test_component_stored(self):
        eta, _, _ = self._make_eta()
        self.assertEqual(eta.components["A"], "eta_A")

    def test_mismatched_source_raises(self):
        from kategori_teorisi.types import (
            Category, Functor, NaturalTransformation,
        )
        c1 = Category(name="C1")
        c2 = Category(name="C2")
        d = Category(name="D")
        F = Functor(name="F", source_cat=c1, target_cat=d)
        G = Functor(name="G", source_cat=c2, target_cat=d)
        with self.assertRaises(ValueError):
            NaturalTransformation(name="eta", source_functor=F, target_functor=G)

    def test_mismatched_target_raises(self):
        from kategori_teorisi.types import (
            Category, Functor, NaturalTransformation,
        )
        c = Category(name="C")
        d1 = Category(name="D1")
        d2 = Category(name="D2")
        F = Functor(name="F", source_cat=c, target_cat=d1)
        G = Functor(name="G", source_cat=c, target_cat=d2)
        with self.assertRaises(ValueError):
            NaturalTransformation(name="eta", source_functor=F, target_functor=G)

    def test_set_component_unknown_object(self):
        eta, _, _ = self._make_eta()
        with self.assertRaises(ValueError):
            eta.set_component("nonexistent", "eta_A")

    def test_set_component_unknown_morphism(self):
        eta, _, _ = self._make_eta()
        with self.assertRaises(ValueError):
            eta.set_component("A", "nonexistent")

    def test_to_dict(self):
        eta, _, _ = self._make_eta()
        d = eta.to_dict()
        self.assertEqual(d["name"], "eta")
        self.assertIn("A", d["components"])


# =========================================================================
# Category validity (verification.py)
# =========================================================================

class TestCategoryValidity(unittest.TestCase):
    """Test has_identities, check_composition_defined, is_valid_category."""

    def test_valid_triangle(self):
        cat, *_ = _make_triangle()
        valid, issues = is_valid_category_fn(cat)
        self.assertTrue(valid)
        self.assertEqual(len(issues), 0)

    def test_valid_pair(self):
        cat, *_ = _make_pair()
        valid, issues = is_valid_category_fn(cat)
        self.assertTrue(valid)

    def test_has_identities_ok(self):
        cat, *_ = _make_pair()
        ok, missing = has_identities_fn(cat)
        self.assertTrue(ok)
        self.assertEqual(len(missing), 0)

    def test_composition_defined_triangle(self):
        cat, *_ = _make_triangle()
        ok, missing = check_composition_defined_fn(cat)
        self.assertTrue(ok)

    def test_missing_composition(self):
        """Triangle without declared composition → should fail."""
        from kategori_teorisi.types import Category
        cat = Category(name="Broken")
        a, b, c = _obj("A"), _obj("B"), _obj("C")
        cat.add_object(a)
        cat.add_object(b)
        cat.add_object(c)
        cat.add_morphism(_mor("f", a, b))
        cat.add_morphism(_mor("g", b, c))
        # Missing: g ∘ f but no composition declared, no gf morphism
        ok, missing = check_composition_defined_fn(cat)
        self.assertFalse(ok)
        self.assertGreater(len(missing), 0)

    def test_trivial_category_valid(self):
        from kategori_teorisi.types import Category
        cat = Category(name="Trivial")
        cat.add_object(_obj("X"))
        valid, issues = is_valid_category_fn(cat)
        self.assertTrue(valid)


# Lazy-loaded verification functions for use in test classes
def has_identities_fn(cat):
    from kategori_teorisi.verification import has_identities
    return has_identities(cat)

def check_composition_defined_fn(cat):
    from kategori_teorisi.verification import check_composition_defined
    return check_composition_defined(cat)

def is_valid_category_fn(cat):
    from kategori_teorisi.verification import is_valid_category
    return is_valid_category(cat)


# =========================================================================
# Functor verification (KV₅)
# =========================================================================

class TestFunctorVerification(unittest.TestCase):
    """Test functor checks: objects, morphisms, faithfulness, fullness."""

    def test_objects_mapped(self):
        from kategori_teorisi.verification import check_functor_objects
        _, _, F = _make_rep_hakikat_functor()
        ok, unmapped = check_functor_objects(F)
        self.assertTrue(ok)
        self.assertEqual(len(unmapped), 0)

    def test_morphisms_mapped(self):
        from kategori_teorisi.verification import check_functor_morphisms
        _, _, F = _make_rep_hakikat_functor()
        ok, unmapped = check_functor_morphisms(F)
        self.assertTrue(ok)

    def test_faithful(self):
        from kategori_teorisi.verification import is_faithful
        _, _, F = _make_rep_hakikat_functor()
        ok, violations = is_faithful(F)
        self.assertTrue(ok)

    def test_not_faithful(self):
        """Two distinct morphisms mapping to the same target → not faithful."""
        from kategori_teorisi.types import Category, Functor
        from kategori_teorisi.verification import is_faithful
        src = Category(name="Src")
        tgt = Category(name="Tgt")
        a, b = _obj("A"), _obj("B")
        src.add_object(a)
        src.add_object(b)
        f1 = _mor("f1", a, b)
        f2 = _mor("f2", a, b)
        src.add_morphism(f1)
        src.add_morphism(f2)

        ta, tb = _obj("TA"), _obj("TB")
        tgt.add_object(ta)
        tgt.add_object(tb)
        g = _mor("g", ta, tb)
        tgt.add_morphism(g)

        F = Functor(name="F", source_cat=src, target_cat=tgt)
        F.map_object("A", "TA")
        F.map_object("B", "TB")
        F.map_morphism("id_A", "id_TA")
        F.map_morphism("id_B", "id_TB")
        F.map_morphism("f1", "g")
        F.map_morphism("f2", "g")  # same image as f1 → violates faithfulness

        ok, violations = is_faithful(F)
        self.assertFalse(ok)
        self.assertGreater(len(violations), 0)

    def test_full(self):
        from kategori_teorisi.verification import is_full
        _, _, F = _make_rep_hakikat_functor()
        ok, violations = is_full(F)
        self.assertTrue(ok)

    def test_not_full(self):
        """Target has morphism not in image of source hom-set → not full."""
        from kategori_teorisi.types import Category, Functor
        from kategori_teorisi.verification import is_full
        src = Category(name="Src")
        tgt = Category(name="Tgt")
        a = _obj("A")
        src.add_object(a)
        ta = _obj("TA")
        tgt.add_object(ta)
        extra = _mor("extra", ta, ta)
        tgt.add_morphism(extra)

        F = Functor(name="F", source_cat=src, target_cat=tgt)
        F.map_object("A", "TA")
        F.map_morphism("id_A", "id_TA")

        ok, violations = is_full(F)
        self.assertFalse(ok)

    def test_identity_preservation(self):
        from kategori_teorisi.verification import check_identity_preservation
        _, _, F = _make_rep_hakikat_functor()
        ok, violations = check_identity_preservation(F)
        self.assertTrue(ok)

    def test_identity_not_preserved(self):
        """F(id_A) maps to non-identity → violation."""
        from kategori_teorisi.types import Category, Functor
        from kategori_teorisi.verification import check_identity_preservation
        src = Category(name="Src")
        tgt = Category(name="Tgt")
        a = _obj("A")
        src.add_object(a)
        ta, tb = _obj("TA"), _obj("TB")
        tgt.add_object(ta)
        tgt.add_object(tb)
        f = _mor("f", ta, tb)
        tgt.add_morphism(f)

        F = Functor(name="F", source_cat=src, target_cat=tgt)
        F.map_object("A", "TA")
        F.map_morphism("id_A", "f")  # maps identity to non-identity!

        ok, violations = check_identity_preservation(F)
        self.assertFalse(ok)

    def test_verify_functor_complete(self):
        from kategori_teorisi.verification import verify_functor
        _, _, F = _make_rep_hakikat_functor()
        result = verify_functor(F)
        self.assertTrue(result["passed"])
        self.assertTrue(result["objects_mapped"])
        self.assertTrue(result["morphisms_mapped"])
        self.assertTrue(result["faithful"])
        self.assertTrue(result["identity_preservation"])

    def test_unmapped_objects(self):
        from kategori_teorisi.types import Category, Functor
        from kategori_teorisi.verification import check_functor_objects
        src = Category(name="Src")
        tgt = Category(name="Tgt")
        src.add_object(_obj("A"))
        src.add_object(_obj("B"))
        tgt.add_object(_obj("TA"))
        F = Functor(name="F", source_cat=src, target_cat=tgt)
        F.map_object("A", "TA")
        # B not mapped
        ok, unmapped = check_functor_objects(F)
        self.assertFalse(ok)
        self.assertIn("B", unmapped)


# =========================================================================
# Naturality verification
# =========================================================================

class TestNaturalityVerification(unittest.TestCase):
    """Test naturality square checks."""

    def _make_full_nat_transform(self):
        """Build a complete nat. transform with verifiable naturality."""
        from kategori_teorisi.types import (
            Category, Functor, NaturalTransformation, MorphismKind,
        )
        # Source category C: A -f-> B
        C = Category(name="C")
        a, b = _obj("A"), _obj("B")
        C.add_object(a)
        C.add_object(b)
        f = _mor("f", a, b)
        C.add_morphism(f)

        # Target category D: FA, GA, FB, GB with components + images
        D = Category(name="D")
        fa, ga, fb, gb = _obj("FA"), _obj("GA"), _obj("FB"), _obj("GB")
        D.add_object(fa)
        D.add_object(ga)
        D.add_object(fb)
        D.add_object(gb)
        eta_a = _mor("eta_A", fa, ga)
        eta_b = _mor("eta_B", fb, gb)
        Ff = _mor("Ff", fa, fb)
        Gf = _mor("Gf", ga, gb)
        D.add_morphism(eta_a)
        D.add_morphism(eta_b)
        D.add_morphism(Ff)
        D.add_morphism(Gf)
        # Compositions for naturality: G(f)∘η_A and η_B∘F(f)
        gf_eta_a = _mor("Gf_eta_A", fa, gb)
        eta_b_Ff = _mor("eta_B_Ff", fa, gb)
        D.add_morphism(gf_eta_a)
        D.add_morphism(eta_b_Ff)
        D.set_composition("eta_A", "Gf", "Gf_eta_A")
        D.set_composition("Ff", "eta_B", "eta_B_Ff")

        # Functors
        F = Functor(name="F", source_cat=C, target_cat=D)
        F.map_object("A", "FA")
        F.map_object("B", "FB")
        F.map_morphism("id_A", "id_FA")
        F.map_morphism("id_B", "id_FB")
        F.map_morphism("f", "Ff")

        G = Functor(name="G", source_cat=C, target_cat=D)
        G.map_object("A", "GA")
        G.map_object("B", "GB")
        G.map_morphism("id_A", "id_GA")
        G.map_morphism("id_B", "id_GB")
        G.map_morphism("f", "Gf")

        eta = NaturalTransformation(name="eta", source_functor=F, target_functor=G)
        eta.set_component("A", "eta_A")
        eta.set_component("B", "eta_B")
        return eta, C, D

    def test_naturality_commutes(self):
        """Naturality passes when G(f)∘η_A = η_B∘F(f) (same composite)."""
        from kategori_teorisi.verification import check_naturality
        eta, C, D = self._make_full_nat_transform()
        # Both composites have different names → naturality fails in strict check
        # unless they are equal. Let me fix: they should compose to the SAME morphism.
        # In our setup: Gf_eta_A ≠ eta_B_Ff → fails
        # For a passing test, they must be equal.
        # Let's rebuild with the same composite:
        D._composition[("eta_A", "Gf")] = "composite"
        D._composition[("Ff", "eta_B")] = "composite"
        # Also need the composite morphism
        fa = D.get_object("FA")
        gb = D.get_object("GB")
        comp = _mor("composite", fa, gb)
        D.add_morphism(comp)
        ok, issues = check_naturality(eta)
        self.assertTrue(ok, f"Naturality failed: {issues}")

    def test_naturality_fails_different_composites(self):
        from kategori_teorisi.verification import check_naturality
        eta, C, D = self._make_full_nat_transform()
        # Default setup has different composite names → should fail
        ok, issues = check_naturality(eta)
        self.assertFalse(ok)

    def test_component_coverage_ok(self):
        from kategori_teorisi.verification import check_component_coverage
        eta, _, _ = self._make_full_nat_transform()
        ok, missing = check_component_coverage(eta)
        self.assertTrue(ok)
        self.assertEqual(len(missing), 0)

    def test_component_coverage_missing(self):
        from kategori_teorisi.types import (
            Category, Functor, NaturalTransformation,
        )
        from kategori_teorisi.verification import check_component_coverage
        C = Category(name="C")
        D = Category(name="D")
        a, b = _obj("A"), _obj("B")
        C.add_object(a)
        C.add_object(b)
        fa, fb = _obj("FA"), _obj("FB")
        D.add_object(fa)
        D.add_object(fb)

        F = Functor(name="F", source_cat=C, target_cat=D)
        F.map_object("A", "FA")
        F.map_object("B", "FB")
        G = Functor(name="G", source_cat=C, target_cat=D)
        G.map_object("A", "FA")
        G.map_object("B", "FB")

        eta = NaturalTransformation(name="eta", source_functor=F, target_functor=G)
        # Only set component for A, not B
        # Need a morphism eta_A in D
        eta_a_m = _mor("eta_A", fa, fa)
        D.add_morphism(eta_a_m)
        eta.set_component("A", "eta_A")

        ok, missing = check_component_coverage(eta)
        self.assertFalse(ok)
        self.assertIn("B", missing)

    def test_verify_natural_transformation(self):
        from kategori_teorisi.verification import verify_natural_transformation
        eta, C, D = self._make_full_nat_transform()
        # Make naturality pass
        fa = D.get_object("FA")
        gb = D.get_object("GB")
        comp = _mor("composite", fa, gb)
        D.add_morphism(comp)
        D._composition[("eta_A", "Gf")] = "composite"
        D._composition[("Ff", "eta_B")] = "composite"
        result = verify_natural_transformation(eta)
        self.assertTrue(result["passed"])
        self.assertTrue(result["component_coverage"])
        self.assertTrue(result["naturality"])


# =========================================================================
# Vahidiyet (AX12)
# =========================================================================

class TestVahidiyet(unittest.TestCase):
    """Test AX12 — Vahidiyet connectedness check."""

    def test_connected_pair(self):
        from kategori_teorisi.verification import check_vahidiyet
        cat, *_ = _make_pair()
        ok, score, msg = check_vahidiyet(cat)
        self.assertTrue(ok)
        self.assertGreater(score, 0.0)

    def test_connected_triangle(self):
        from kategori_teorisi.verification import check_vahidiyet
        cat, *_ = _make_triangle()
        ok, score, msg = check_vahidiyet(cat)
        self.assertTrue(ok)

    def test_disconnected(self):
        from kategori_teorisi.verification import check_vahidiyet
        cat, *_ = _make_disconnected()
        ok, score, msg = check_vahidiyet(cat)
        self.assertFalse(ok)
        self.assertLess(score, 1.0)
        self.assertIn("unreachable", msg)

    def test_trivial_single_object(self):
        from kategori_teorisi.types import Category
        from kategori_teorisi.verification import check_vahidiyet
        cat = Category(name="Trivial")
        cat.add_object(_obj("X"))
        ok, score, msg = check_vahidiyet(cat)
        self.assertTrue(ok)

    def test_empty_category(self):
        from kategori_teorisi.types import Category
        from kategori_teorisi.verification import check_vahidiyet
        cat = Category(name="Empty")
        ok, score, msg = check_vahidiyet(cat)
        self.assertTrue(ok)

    def test_score_less_than_one(self):
        """T6/KV₄: score must be < 1.0."""
        from kategori_teorisi.verification import check_vahidiyet
        cat, *_ = _make_pair()
        ok, score, msg = check_vahidiyet(cat)
        self.assertLess(score, 1.0)


# =========================================================================
# Ehadiyet (AX13)
# =========================================================================

class TestEhadiyet(unittest.TestCase):
    """Test AX13 — Ehadiyet local reflection check."""

    def test_all_reflected(self):
        from kategori_teorisi.verification import check_ehadiyet
        cat, *_ = _make_pair()
        ok, score, msg = check_ehadiyet(cat)
        self.assertTrue(ok)

    def test_isolated_objects(self):
        from kategori_teorisi.verification import check_ehadiyet
        cat, *_ = _make_disconnected()
        ok, score, msg = check_ehadiyet(cat)
        self.assertFalse(ok)
        self.assertIn("isolated", msg)

    def test_partial_reflection(self):
        """One object connected, one isolated."""
        from kategori_teorisi.types import Category
        from kategori_teorisi.verification import check_ehadiyet
        cat = Category(name="Partial")
        a, b, c = _obj("A"), _obj("B"), _obj("C")
        cat.add_object(a)
        cat.add_object(b)
        cat.add_object(c)
        cat.add_morphism(_mor("f", a, b))
        # C has no non-identity connections
        ok, score, msg = check_ehadiyet(cat)
        self.assertFalse(ok)
        self.assertIn("C", msg)

    def test_trivial(self):
        from kategori_teorisi.types import Category
        from kategori_teorisi.verification import check_ehadiyet
        cat = Category(name="Trivial")
        cat.add_object(_obj("X"))
        ok, score, msg = check_ehadiyet(cat)
        self.assertTrue(ok)

    def test_score_less_than_one(self):
        from kategori_teorisi.verification import check_ehadiyet
        cat, *_ = _make_triangle()
        ok, score, msg = check_ehadiyet(cat)
        self.assertLess(score, 1.0)


# =========================================================================
# Yakinlasma — Convergence score
# =========================================================================

class TestYakinlasma(unittest.TestCase):
    """Test yakinlasma convergence score."""

    def test_with_category_only(self):
        from kategori_teorisi.verification import yakinlasma
        cat, *_ = _make_triangle()
        score = yakinlasma(cat=cat)
        self.assertGreater(score, 0.0)
        self.assertLess(score, 1.0)

    def test_with_functor(self):
        from kategori_teorisi.verification import yakinlasma
        rep, hak, F = _make_rep_hakikat_functor()
        score = yakinlasma(cat=rep, functor=F)
        self.assertGreater(score, 0.0)
        self.assertLess(score, 1.0)

    def test_empty_returns_zero(self):
        from kategori_teorisi.verification import yakinlasma
        score = yakinlasma()
        self.assertEqual(score, 0.0)

    def test_score_bounded_below_one(self):
        """T6/KV₄ — convergence must be strictly < 1.0."""
        from kategori_teorisi.verification import yakinlasma
        cat, *_ = _make_triangle()
        rep, hak, F = _make_rep_hakikat_functor()
        score = yakinlasma(cat=cat, functor=F)
        self.assertLess(score, 1.0)

    def test_disconnected_category_lower_score(self):
        from kategori_teorisi.verification import yakinlasma
        cat_good, *_ = _make_pair()
        cat_bad, *_ = _make_disconnected()
        score_good = yakinlasma(cat=cat_good)
        score_bad = yakinlasma(cat=cat_bad)
        self.assertGreater(score_good, score_bad)


# =========================================================================
# clamp_score
# =========================================================================

class TestClampScore(unittest.TestCase):
    """Test clamp_score edge cases."""

    def test_normal_value(self):
        from kategori_teorisi.types import clamp_score
        self.assertEqual(clamp_score(0.5), 0.5)

    def test_negative_clamps_to_zero(self):
        from kategori_teorisi.types import clamp_score
        self.assertEqual(clamp_score(-0.1), 0.0)

    def test_one_clamps_below(self):
        from kategori_teorisi.types import clamp_score
        result = clamp_score(1.0)
        self.assertLess(result, 1.0)
        self.assertEqual(result, 0.9999)

    def test_above_one_clamps(self):
        from kategori_teorisi.types import clamp_score
        result = clamp_score(5.0)
        self.assertEqual(result, 0.9999)

    def test_zero_stays_zero(self):
        from kategori_teorisi.types import clamp_score
        self.assertEqual(clamp_score(0.0), 0.0)

    def test_just_below_one(self):
        from kategori_teorisi.types import clamp_score
        self.assertEqual(clamp_score(0.999), 0.999)


# =========================================================================
# Constraint: category_validity
# =========================================================================

class TestConstraintCategoryValidity(unittest.TestCase):
    """Test category_validity constraint factory."""

    def test_valid_category(self):
        from kategori_teorisi.constraints import category_validity
        cat, *_ = _make_pair()
        payload = json.dumps({"category": _cat_to_json(cat)})
        c = category_validity()
        result = c.check(payload)
        self.assertTrue(result.passed)
        self.assertGreater(result.score, 0.0)
        self.assertLess(result.score, 1.0)

    def test_invalid_json(self):
        from kategori_teorisi.constraints import category_validity
        c = category_validity()
        result = c.check("not json")
        self.assertFalse(result.passed)

    def test_missing_category_key(self):
        from kategori_teorisi.constraints import category_validity
        c = category_validity()
        result = c.check(json.dumps({"foo": "bar"}))
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: functor_faithfulness
# =========================================================================

class TestConstraintFunctorFaith(unittest.TestCase):
    """Test functor_faithfulness constraint factory (KV₅)."""

    def test_faithful_functor(self):
        from kategori_teorisi.constraints import functor_faithfulness
        rep, hak, F = _make_rep_hakikat_functor()
        payload = json.dumps({
            "source_category": _cat_to_json(rep),
            "target_category": _cat_to_json(hak),
            "functor": {
                "name": F.name,
                "object_map": dict(F.object_map),
                "morphism_map": dict(F.morphism_map),
            },
        })
        c = functor_faithfulness()
        result = c.check(payload)
        self.assertTrue(result.passed)
        self.assertLess(result.score, 1.0)

    def test_invalid_json(self):
        from kategori_teorisi.constraints import functor_faithfulness
        c = functor_faithfulness()
        result = c.check("bad")
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: vahidiyet_containment
# =========================================================================

class TestConstraintVahidiyet(unittest.TestCase):
    """Test vahidiyet_containment constraint factory (AX12)."""

    def test_connected(self):
        from kategori_teorisi.constraints import vahidiyet_containment
        cat, *_ = _make_pair()
        payload = json.dumps({"category": _cat_to_json(cat)})
        c = vahidiyet_containment()
        result = c.check(payload)
        self.assertTrue(result.passed)

    def test_disconnected(self):
        from kategori_teorisi.constraints import vahidiyet_containment
        cat, *_ = _make_disconnected()
        payload = json.dumps({"category": _cat_to_json(cat)})
        c = vahidiyet_containment()
        result = c.check(payload)
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: ehadiyet_reflection
# =========================================================================

class TestConstraintEhadiyet(unittest.TestCase):
    """Test ehadiyet_reflection constraint factory (AX13)."""

    def test_all_reflected(self):
        from kategori_teorisi.constraints import ehadiyet_reflection
        cat, *_ = _make_pair()
        payload = json.dumps({"category": _cat_to_json(cat)})
        c = ehadiyet_reflection()
        result = c.check(payload)
        self.assertTrue(result.passed)

    def test_isolated(self):
        from kategori_teorisi.constraints import ehadiyet_reflection
        cat, *_ = _make_disconnected()
        payload = json.dumps({"category": _cat_to_json(cat)})
        c = ehadiyet_reflection()
        result = c.check(payload)
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: naturality_check
# =========================================================================

class TestConstraintNaturality(unittest.TestCase):
    """Test naturality_check constraint factory."""

    def test_valid_naturality(self):
        from kategori_teorisi.constraints import naturality_check
        c = naturality_check()
        # Build a trivial 1-object natural transformation (no non-id morphisms)
        payload = json.dumps({
            "source_category": {
                "name": "C",
                "objects": [{"name": "A"}],
                "morphisms": [],
            },
            "target_category": {
                "name": "D",
                "objects": [{"name": "FA"}, {"name": "GA"}],
                "morphisms": [
                    {"name": "eta_A", "source": "FA", "target": "GA", "kind": "derivation"},
                ],
            },
            "source_functor": {
                "name": "F",
                "object_map": {"A": "FA"},
                "morphism_map": {"id_A": "id_FA"},
            },
            "target_functor": {
                "name": "G",
                "object_map": {"A": "GA"},
                "morphism_map": {"id_A": "id_GA"},
            },
            "transformation": {
                "name": "eta",
                "components": {"A": "eta_A"},
            },
        })
        result = c.check(payload)
        self.assertTrue(result.passed)

    def test_invalid_json(self):
        from kategori_teorisi.constraints import naturality_check
        c = naturality_check()
        result = c.check("bad")
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: category_convergence_bound
# =========================================================================

class TestConstraintConvergenceBound(unittest.TestCase):
    """Test category_convergence_bound constraint (KV₄/T6)."""

    def test_valid_score(self):
        from kategori_teorisi.constraints import category_convergence_bound
        c = category_convergence_bound()
        result = c.check(json.dumps({"convergence_score": 0.85}))
        self.assertTrue(result.passed)
        self.assertLess(result.score, 1.0)

    def test_score_at_one(self):
        from kategori_teorisi.constraints import category_convergence_bound
        c = category_convergence_bound()
        result = c.check(json.dumps({"convergence_score": 1.0}))
        self.assertFalse(result.passed)

    def test_score_above_95(self):
        from kategori_teorisi.constraints import category_convergence_bound
        c = category_convergence_bound()
        result = c.check(json.dumps({"convergence_score": 0.96}))
        self.assertFalse(result.passed)
        self.assertIn("0.95", result.message)

    def test_score_zero(self):
        from kategori_teorisi.constraints import category_convergence_bound
        c = category_convergence_bound()
        result = c.check(json.dumps({"convergence_score": 0.0}))
        self.assertTrue(result.passed)

    def test_invalid_json(self):
        from kategori_teorisi.constraints import category_convergence_bound
        c = category_convergence_bound()
        result = c.check("bad")
        self.assertFalse(result.passed)


# =========================================================================
# Constraint: valid_category_entry (composite)
# =========================================================================

class TestConstraintValidEntry(unittest.TestCase):
    """Test valid_category_entry composite constraint."""

    def test_valid_entry(self):
        from kategori_teorisi.constraints import valid_category_entry
        cat, *_ = _make_pair()
        payload = json.dumps({
            "category": _cat_to_json(cat),
            "convergence_score": 0.75,
        })
        c = valid_category_entry()
        result = c.check(payload)
        self.assertTrue(result.passed)
        self.assertLess(result.score, 1.0)

    def test_missing_category(self):
        from kategori_teorisi.constraints import valid_category_entry
        payload = json.dumps({"convergence_score": 0.5})
        c = valid_category_entry()
        result = c.check(payload)
        self.assertFalse(result.passed)

    def test_convergence_too_high(self):
        from kategori_teorisi.constraints import valid_category_entry
        cat, *_ = _make_pair()
        payload = json.dumps({
            "category": _cat_to_json(cat),
            "convergence_score": 1.5,
        })
        c = valid_category_entry()
        result = c.check(payload)
        self.assertFalse(result.passed)

    def test_with_functor(self):
        from kategori_teorisi.constraints import valid_category_entry
        rep, hak, F = _make_rep_hakikat_functor()
        payload = json.dumps({
            "category": _cat_to_json(rep),
            "convergence_score": 0.7,
            "source_category": _cat_to_json(rep),
            "target_category": _cat_to_json(hak),
            "functor": {
                "name": F.name,
                "object_map": dict(F.object_map),
                "morphism_map": dict(F.morphism_map),
            },
        })
        c = valid_category_entry()
        result = c.check(payload)
        self.assertTrue(result.passed)
        self.assertLess(result.score, 1.0)


# =========================================================================
# KV₇ — Independence (no shared state with other lenses)
# =========================================================================

class TestKV7Independence(unittest.TestCase):
    """KV₇ — kategori_teorisi must NOT import from other lens packages."""

    LENS_PACKAGES = {"kavram_sozlugu", "mereoloji", "fol_formalizasyon"}
    SOURCE_FILES = ["types.py", "verification.py", "constraints.py", "__init__.py"]

    def test_no_cross_lens_imports(self):
        """Parse all source files; no import from another lens."""
        base_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "kategori_teorisi",
        )
        for fname in self.SOURCE_FILES:
            fpath = os.path.join(base_dir, fname)
            if not os.path.exists(fpath):
                continue
            with open(fpath, encoding="utf-8") as fh:
                source = fh.read()
            tree = ast.parse(source, filename=fname)
            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for alias in node.names:
                        for pkg in self.LENS_PACKAGES:
                            self.assertFalse(
                                alias.name.startswith(pkg),
                                f"{fname} imports {alias.name} — KV₇ violation",
                            )
                elif isinstance(node, ast.ImportFrom):
                    if node.module:
                        for pkg in self.LENS_PACKAGES:
                            self.assertFalse(
                                node.module.startswith(pkg),
                                f"{fname} imports from {node.module} — KV₇ violation",
                            )

    def test_only_imports_ai_assert_and_self(self):
        """External imports limited to ai_assert, stdlib, and self."""
        allowed_prefixes = {"ai_assert", "kategori_teorisi"}
        base_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "kategori_teorisi",
        )
        for fname in self.SOURCE_FILES:
            fpath = os.path.join(base_dir, fname)
            if not os.path.exists(fpath):
                continue
            with open(fpath, encoding="utf-8") as fh:
                source = fh.read()
            tree = ast.parse(source, filename=fname)
            for node in ast.walk(tree):
                if isinstance(node, ast.ImportFrom) and node.module:
                    # Relative imports (level > 0) are intra-package — skip
                    if node.level and node.level > 0:
                        continue
                    mod = node.module
                    # Check if it's stdlib or allowed
                    is_allowed = any(
                        mod.startswith(p) for p in allowed_prefixes
                    )
                    is_stdlib = mod.split(".")[0] in {
                        "json", "dataclasses", "enum", "typing",
                        "collections", "functools", "re", "os", "sys",
                        "__future__",
                    }
                    self.assertTrue(
                        is_allowed or is_stdlib,
                        f"{fname}: unexpected import from {mod}",
                    )


# =========================================================================
# Framework summary
# =========================================================================

class TestFrameworkSummary(unittest.TestCase):
    """Test framework_summary metadata."""

    def test_keys_present(self):
        from kategori_teorisi.verification import framework_summary
        s = framework_summary()
        self.assertEqual(s["lens"], "KategoriTeorisi")
        self.assertEqual(s["lens_number"], 6)
        self.assertEqual(s["faculty"], "Sır")
        self.assertIn("AX12", s["axioms_checked"])
        self.assertIn("AX13", s["axioms_checked"])
        self.assertIn("KV5", s["kavaid_checked"])
        self.assertIn("KV4", s["kavaid_checked"])
        self.assertIn("KV7", s["kavaid_checked"])

    def test_max_score_below_one(self):
        from kategori_teorisi.verification import framework_summary
        s = framework_summary()
        self.assertLess(s["max_score"], 1.0)

    def test_capabilities_not_empty(self):
        from kategori_teorisi.verification import framework_summary
        s = framework_summary()
        self.assertGreater(len(s["capabilities"]), 0)


# =========================================================================

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