"""
tests/test_phase8_source_integration.py â€” Phase 8: Source Text Integration

WATERFALL_PLAN Â§12 â€” Ground-truth calibration pipeline.
Addresses BS-21 (Empty kaynak_metinler) and BS-22 (No ground truth).

Test IDs: 8.01 â€“ 8.08

Key principles:
  - AX64: Source texts are immutable (SHA-256 verification).
  - AX65: Every formalization paired with axiom references.
  - KVâ‚„:  0 < composite < 1.
  - T17:  Coverage â‰¤ 6/7.
"""

from __future__ import annotations

import importlib
import json
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock
from typing import Dict, Any, List

from fw_server.calibration import (
    SourceText,
    LensCalibration,
    TextCalibration,
    CalibrationResult,
    load_source_texts,
    verify_immutability,
    verify_axiom_maps,
    calibrate_pipeline,
    _grade_rank,
    _grade_meets_min,
    _calibrate_single,
    _DEFAULT_KAYNAK_DIR,
)

# fw_server.__init__ shadows the dusun module with the dusun function.
# Use importlib to get the actual module object for patch.object().
_dusun_module = importlib.import_module("fw_server.dusun")


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# Helpers
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

def _make_mock_dusun_result(
    path: str = "P4",
    grade: str = "Tasdik",
    composite: float = 0.45,
    lens_scores: Dict[str, float] | None = None,
    patterns: List[str] | None = None,
) -> Dict[str, Any]:
    """Build a synthetic dusun() return dict for testing."""
    if lens_scores is None:
        lens_scores = {
            "Ontoloji": 0.40,
            "Mereoloji": 0.30,
            "FOL": 0.45,
            "Bayes": 0.25,
            "OyunTeorisi": 0.20,
            "KategoriTeorisi": 0.30,
            "Topoloji + Holografik": 0.35,
        }
    if patterns is None:
        patterns = ["esma_ontology", "holographic_architecture",
                     "transparent_vessels", "constitutive_dependency"]
    return {
        "path": path,
        "grade": grade,
        "composite_score": composite,
        "lens_scores": lens_scores,
        "patterns_applied": patterns,
        "kavaid": {"KV1": True, "KV2": True},
        "kavaid_pass_count": 2,
        "vocabulary_detected": ["AX17", "AX37"],
        "fire_plan": ["Ontoloji", "FOL"],
        "translation_hints": {},
        "anomalies": [],
        "ax57_compact": "P4 | Tasdik | esma_ontology",
    }


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.01: Source texts exist and are loadable
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestSourceTextsLoad(unittest.TestCase):
    """8.01 â€” kaynak_metinler/ has at least 3 valid entries."""

    def test_load_source_texts_returns_entries(self):
        texts = load_source_texts()
        self.assertGreaterEqual(len(texts), 3,
            "Expected â‰¥3 source text entries in kaynak_metinler/")

    def test_source_texts_have_required_fields(self):
        texts = load_source_texts()
        for st in texts:
            self.assertTrue(st.text_id, f"text_id empty for {st}")
            self.assertTrue(st.original_text.strip(),
                f"original_text empty for {st.text_id}")
            self.assertIsInstance(st.axiom_map, dict,
                f"axiom_map not dict for {st.text_id}")
            self.assertIsInstance(st.expected_scores, dict,
                f"expected_scores not dict for {st.text_id}")
            self.assertTrue(st.text_hash, f"text_hash empty for {st.text_id}")

    def test_known_entries_present(self):
        texts = load_source_texts()
        ids = {st.text_id for st in texts}
        for expected_id in ("birinciSoz", "ikincisoz", "ucuncusoz"):
            self.assertIn(expected_id, ids,
                f"Missing expected entry: {expected_id}")


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.02: Source text immutability â€” AX64
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestSourceTextsImmutable(unittest.TestCase):
    """8.02 â€” SHA-256 hashes match on-disk files (AX64)."""

    def test_all_texts_immutable(self):
        texts = load_source_texts()
        results = verify_immutability(texts)
        for text_id, is_immutable in results:
            self.assertTrue(is_immutable,
                f"Immutability check failed for {text_id} â€” AX64 violation")

    def test_immutability_count_matches_texts(self):
        texts = load_source_texts()
        results = verify_immutability(texts)
        self.assertEqual(len(results), len(texts))


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.03: Axiom maps complete â€” AX65
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestAxiomMapsComplete(unittest.TestCase):
    """8.03 â€” Every axiom map has â‰¥1 grounded axiom (AX65)."""

    def test_all_maps_have_axioms(self):
        texts = load_source_texts()
        results = verify_axiom_maps(texts)
        for text_id, has_axioms, count in results:
            self.assertTrue(has_axioms,
                f"No axioms_grounded in {text_id} â€” AX65 violation")
            self.assertGreater(count, 0,
                f"axiom_count = 0 for {text_id}")

    def test_birinciSoz_axiom_richness(self):
        """birinciSoz should have â‰¥5 grounded axioms."""
        texts = load_source_texts()
        bs = [st for st in texts if st.text_id == "birinciSoz"]
        self.assertEqual(len(bs), 1)
        axioms = bs[0].axiom_map.get("axioms_grounded", [])
        self.assertGreaterEqual(len(axioms), 5,
            "birinciSoz should ground at least 5 axioms")


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.04: Calibration baseline â€” pipeline runs without error
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestCalibrationBaseline(unittest.TestCase):
    """8.04 â€” calibrate_pipeline() runs to completion and returns valid result."""

    @patch.object(_dusun_module, "dusun")
    def test_pipeline_runs_without_error(self, mock_dusun):
        """Pipeline completes with mocked dusun() (no live computation)."""
        mock_dusun.return_value = _make_mock_dusun_result()
        result = calibrate_pipeline()
        self.assertIsNone(result.error,
            f"Pipeline error: {result.error}")
        self.assertGreaterEqual(result.total_texts, 3)

    @patch.object(_dusun_module, "dusun")
    def test_pipeline_returns_calibration_result(self, mock_dusun):
        mock_dusun.return_value = _make_mock_dusun_result()
        result = calibrate_pipeline()
        self.assertIsInstance(result, CalibrationResult)
        self.assertIsInstance(result.text_results, list)
        self.assertGreater(len(result.text_results), 0)

    @patch.object(_dusun_module, "dusun")
    def test_pipeline_to_dict_serializable(self, mock_dusun):
        """CalibrationResult.to_dict() produces JSON-serializable output."""
        mock_dusun.return_value = _make_mock_dusun_result()
        result = calibrate_pipeline()
        d = result.to_dict()
        # Should not raise
        serialized = json.dumps(d, ensure_ascii=False)
        self.assertIsInstance(serialized, str)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.05: Ground truth â€” lens scores within expected ranges
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestGroundTruthLensScores(unittest.TestCase):
    """8.05 â€” Each lens score falls within expected_scores range."""

    @patch.object(_dusun_module, "dusun")
    def test_lens_scores_in_range(self, mock_dusun):
        """All lens scores from mock dusun() within bootstrap ranges."""
        mock_dusun.return_value = _make_mock_dusun_result()
        result = calibrate_pipeline()
        for tc in result.text_results:
            for lc in tc.lens_results:
                self.assertTrue(lc.in_range,
                    f"{tc.text_id}/{lc.lens_id}: {lc.actual_score:.3f} "
                    f"outside [{lc.expected_min}, {lc.expected_max}]")

    def test_lens_calibration_delta_computation(self):
        """Delta = 0 when in range, negative when below, positive when above."""
        lc_in = LensCalibration("FOL", 0.50, 0.10, 0.80)
        self.assertEqual(lc_in.delta, 0.0)
        self.assertTrue(lc_in.in_range)

        lc_below = LensCalibration("FOL", 0.05, 0.10, 0.80)
        self.assertLess(lc_below.delta, 0)
        self.assertFalse(lc_below.in_range)

        lc_above = LensCalibration("FOL", 0.90, 0.10, 0.80)
        self.assertGreater(lc_above.delta, 0)
        self.assertFalse(lc_above.in_range)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.06: Ground truth â€” path classification
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestGroundTruthPath(unittest.TestCase):
    """8.06 â€” Path classification matches expected_path for each text."""

    @patch.object(_dusun_module, "dusun")
    def test_path_matches_expected(self, mock_dusun):
        mock_dusun.return_value = _make_mock_dusun_result(path="P4")
        result = calibrate_pipeline()
        for tc in result.text_results:
            self.assertTrue(tc.path_match,
                f"{tc.text_id}: path={tc.path_actual}, "
                f"expected={tc.path_expected}")

    @patch.object(_dusun_module, "dusun")
    def test_path_accuracy_is_1_when_all_match(self, mock_dusun):
        mock_dusun.return_value = _make_mock_dusun_result(path="P4")
        result = calibrate_pipeline()
        self.assertEqual(result.path_accuracy, 1.0)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.07: Ground truth â€” grade meets minimum
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestGroundTruthGrade(unittest.TestCase):
    """8.07 â€” Epistemic grade meets or exceeds expected_grade_min."""

    def test_grade_rank_ordering(self):
        """Tasavvur < Tasdik < İlmelyakîn."""
        self.assertLess(_grade_rank("Tasavvur"), _grade_rank("Tasdik"))
        self.assertLess(_grade_rank("Tasdik"), _grade_rank("İlmelyakîn"))

    def test_grade_meets_min_logic(self):
        self.assertTrue(_grade_meets_min("Tasdik", "Tasavvur"))
        self.assertTrue(_grade_meets_min("Tasdik", "Tasdik"))
        self.assertFalse(_grade_meets_min("Tasavvur", "Tasdik"))
        self.assertTrue(_grade_meets_min("İlmelyakîn", "Tasdik"))

    @patch.object(_dusun_module, "dusun")
    def test_grade_meets_minimum_for_all_texts(self, mock_dusun):
        mock_dusun.return_value = _make_mock_dusun_result(grade="Tasdik")
        result = calibrate_pipeline()
        for tc in result.text_results:
            self.assertTrue(tc.grade_meets_min,
                f"{tc.text_id}: grade={tc.grade_actual}, "
                f"min={tc.grade_expected_min}")

    @patch.object(_dusun_module, "dusun")
    def test_grade_accuracy_is_1_when_all_pass(self, mock_dusun):
        mock_dusun.return_value = _make_mock_dusun_result(grade="İlmelyakîn")
        result = calibrate_pipeline()
        self.assertEqual(result.grade_accuracy, 1.0)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.08: Ground truth â€” patterns detected
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestGroundTruthPatterns(unittest.TestCase):
    """8.08 â€” Expected patterns are detected in dusun() output."""

    @patch.object(_dusun_module, "dusun")
    def test_expected_patterns_detected(self, mock_dusun):
        """All expected_patterns_min found in patterns_applied."""
        # Return a wide set of patterns to cover all expectations
        mock_dusun.return_value = _make_mock_dusun_result(
            patterns=[
                "esma_ontology", "holographic_architecture",
                "shadow_ontology", "constitutive_dependency",
                "transparent_vessels", "convergent_abstraction",
                "three_phase_actualization", "multiplicative_gate",
                "continuous_degrees", "structural_incompleteness",
            ]
        )
        result = calibrate_pipeline()
        for tc in result.text_results:
            if tc.patterns_expected_min:
                self.assertGreater(tc.pattern_coverage, 0.0,
                    f"{tc.text_id}: no expected patterns detected")
                # With all patterns in mock, should be 100%
                self.assertEqual(tc.pattern_coverage, 1.0,
                    f"{tc.text_id}: coverage={tc.pattern_coverage:.2f}, "
                    f"expected=1.0, missing={set(tc.patterns_expected_min) - set(tc.patterns_detected)}")

    @patch.object(_dusun_module, "dusun")
    def test_pattern_accuracy_aggregate(self, mock_dusun):
        """Aggregate pattern accuracy reflects per-text coverage."""
        mock_dusun.return_value = _make_mock_dusun_result(
            patterns=[
                "esma_ontology", "holographic_architecture",
                "shadow_ontology", "constitutive_dependency",
                "transparent_vessels", "convergent_abstraction",
                "three_phase_actualization", "multiplicative_gate",
                "continuous_degrees", "structural_incompleteness",
            ]
        )
        result = calibrate_pipeline()
        self.assertGreater(result.pattern_accuracy, 0.0)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.R: Regression guard â€” _calibrate_single unit tests
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestCalibrateSingleUnit(unittest.TestCase):
    """Regression: _calibrate_single correctly compares dusun output."""

    def _make_source(self) -> SourceText:
        return SourceText(
            text_id="test_entry",
            original_text="Test text content for calibration.",
            axiom_map={"axioms_grounded": [{"axiom_id": "AX1"}]},
            expected_scores={
                "expected_scores": {
                    "Ontoloji": {"min": 0.10, "max": 0.80},
                    "FOL": {"min": 0.10, "max": 0.80},
                },
                "expected_composite": {"min": 0.10, "max": 0.80},
                "expected_path": "P1",
                "expected_grade_min": "Tasavvur",
                "expected_patterns_min": ["transparent_vessels"],
            },
        )

    def test_perfect_match(self):
        source = self._make_source()
        dusun_result = _make_mock_dusun_result(
            path="P1", grade="Tasdik", composite=0.45,
            lens_scores={"Ontoloji": 0.40, "FOL": 0.45},
            patterns=["transparent_vessels", "esma_ontology"],
        )
        tc = _calibrate_single(source, dusun_result)
        self.assertTrue(tc.path_match)
        self.assertTrue(tc.grade_meets_min)
        self.assertTrue(tc.composite_in_range)
        self.assertEqual(tc.pattern_coverage, 1.0)
        for lc in tc.lens_results:
            self.assertTrue(lc.in_range, f"{lc.lens_id} out of range")

    def test_path_mismatch(self):
        source = self._make_source()
        dusun_result = _make_mock_dusun_result(path="P0")
        tc = _calibrate_single(source, dusun_result)
        self.assertFalse(tc.path_match)

    def test_grade_below_minimum(self):
        source = self._make_source()
        source.expected_scores["expected_grade_min"] = "İlmelyakîn"
        dusun_result = _make_mock_dusun_result(grade="Tasavvur")
        tc = _calibrate_single(source, dusun_result)
        self.assertFalse(tc.grade_meets_min)

    def test_missing_patterns(self):
        source = self._make_source()
        dusun_result = _make_mock_dusun_result(patterns=["esma_ontology"])
        tc = _calibrate_single(source, dusun_result)
        self.assertEqual(tc.pattern_coverage, 0.0)
        self.assertEqual(len(tc.patterns_detected), 0)


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# 8.E: Edge cases
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class TestCalibrationEdgeCases(unittest.TestCase):
    """Edge cases and error handling in calibration pipeline."""

    def test_empty_directory_returns_error(self):
        """No source texts â†’ CalibrationResult with error message."""
        import tempfile
        with tempfile.TemporaryDirectory() as tmpdir:
            result = calibrate_pipeline(kaynak_dir=Path(tmpdir))
            self.assertIsNotNone(result.error)

    @patch.object(_dusun_module, "dusun")
    def test_dusun_exception_captured(self, mock_dusun):
        """If dusun() throws, TextCalibration gets error field."""
        mock_dusun.side_effect = RuntimeError("boom")
        result = calibrate_pipeline()
        for tc in result.text_results:
            self.assertIsNotNone(tc.error)
            self.assertIn("boom", tc.error)

    def test_source_text_hash_stability(self):
        """Same content â†’ same hash; different â†’ different."""
        st1 = SourceText("a", "hello world", {}, {})
        st2 = SourceText("b", "hello world", {}, {})
        st3 = SourceText("c", "different content", {}, {})
        self.assertEqual(st1.text_hash, st2.text_hash)
        self.assertNotEqual(st1.text_hash, st3.text_hash)


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