"""
tests/test_fw_server.py — Comprehensive tests for the fw_server MCP bridge.

Covers:
  - types: LENS_IDS, VALID_GRADES, all request/response dataclasses
  - grade_map: score_to_grade boundaries, pass_ratio_to_grade, AX56 ceiling
  - adapters: all 7 run_* with empty params, ADAPTER_MAP completeness, dispatch
  - server: mcp instance, all 8 tool functions callable, JSON round-trip
  - integration: bileshke pipeline end-to-end, kavaid evaluation, chain verification

Design:
  KV₇: Tests are independent — no shared state between test functions.
  AX56: Explicit checks that Hakkalyakîn is never returned.
  T6/KV₄: Composite scores always < 1.0.
"""

from __future__ import annotations

import json
import pytest

# ═══════════════════════════════════════════════════════════════════════
# SECTION 1: fw_server.types
# ═══════════════════════════════════════════════════════════════════════


class TestTypes:
    """Tests for fw_server.types module."""

    def test_lens_ids_count(self):
        from fw_server.types import LENS_IDS
        assert len(LENS_IDS) == 7

    def test_lens_ids_contains_all_lenses(self):
        from fw_server.types import LENS_IDS
        expected = {
            "Ontoloji", "Mereoloji", "FOL", "Bayes",
            "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik",
        }
        assert set(LENS_IDS) == expected

    def test_valid_grades_count(self):
        """3 valid grades — Hakkalyakîn excluded per AX56."""
        from fw_server.types import VALID_GRADES
        assert len(VALID_GRADES) == 3

    def test_valid_grades_excludes_hakkalyakin(self):
        from fw_server.types import VALID_GRADES
        for g in VALID_GRADES:
            assert "Hakkalyakîn" not in g, f"AX56 violation: {g}"

    def test_lens_response_to_dict(self):
        from fw_server.types import LensResponse
        r = LensResponse(
            lens_id="FOL", score=0.75, grade="Tasdik",
            checks_passed=7, checks_total=10, detail="test",
        )
        d = r.to_dict()
        assert d["lens_id"] == "FOL"
        assert d["score"] == 0.75
        assert d["grade"] == "Tasdik"

    def test_bileshke_response_to_dict(self):
        from fw_server.types import BileshkeResponse
        r = BileshkeResponse(
            composite_score=0.65,
            lens_results=[],
            kavaid_checks={},
            completeness=0.7,
            framework_disclosure={"note": "test"},
        )
        d = r.to_dict()
        assert d["composite_score"] == 0.65
        assert d["framework_disclosure"] == {"note": "test"}

    def test_kavaid_response_to_dict(self):
        from fw_server.types import KavaidResponse
        r = KavaidResponse(
            checks={"KV1": True, "KV4": True},
            all_pass=True,
            kv4_warning=None,
            descriptions={},
        )
        d = r.to_dict()
        assert d["all_pass"] is True

    def test_hcp_response_to_dict(self):
        from fw_server.types import HCPResponse
        r = HCPResponse(action="ingest", diagnostics={"n_chunks": 5})
        d = r.to_dict()
        assert d["action"] == "ingest"


# ═══════════════════════════════════════════════════════════════════════
# SECTION 2: fw_server.grade_map
# ═══════════════════════════════════════════════════════════════════════


class TestGradeMap:
    """Tests for fw_server.grade_map module."""

    def test_score_zero_is_tasavvur(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.0) == EpistemicGrade.TASAVVUR

    def test_score_low_is_tasavvur(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.30) == EpistemicGrade.TASAVVUR

    def test_score_boundary_060_is_tasavvur(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.60) == EpistemicGrade.TASAVVUR

    def test_score_boundary_061_is_tasdik(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.61) == EpistemicGrade.TASDIK

    def test_score_mid_is_tasdik(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.75) == EpistemicGrade.TASDIK

    def test_score_boundary_085_is_tasdik(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.85) == EpistemicGrade.TASDIK

    def test_score_boundary_086_is_ilmelyakin(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.86) == EpistemicGrade.ILMELYAKIN

    def test_score_high_is_ilmelyakin(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(0.95) == EpistemicGrade.ILMELYAKIN

    def test_score_max_is_ilmelyakin(self):
        """Even 1.0 should clamp to İlmelyakîn, never Hakkalyakîn (AX56)."""
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        result = score_to_grade(1.0)
        assert result == EpistemicGrade.ILMELYAKIN
        assert result != EpistemicGrade.HAKKALYAKIN

    def test_score_above_one_clamped(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(5.0) == EpistemicGrade.ILMELYAKIN

    def test_negative_score_clamped(self):
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        assert score_to_grade(-0.5) == EpistemicGrade.TASAVVUR

    def test_score_never_returns_hakkalyakin(self):
        """AX56: Sweep full range — Hakkalyakîn must never appear."""
        from fw_server.grade_map import score_to_grade
        from bileshke.types import EpistemicGrade
        for i in range(101):
            score = i / 100.0
            grade = score_to_grade(score)
            assert grade != EpistemicGrade.HAKKALYAKIN, (
                f"AX56 violation at score={score}"
            )

    def test_pass_ratio_perfect(self):
        from fw_server.grade_map import pass_ratio_to_grade
        from bileshke.types import EpistemicGrade
        assert pass_ratio_to_grade(10, 10) == EpistemicGrade.ILMELYAKIN

    def test_pass_ratio_zero_total(self):
        from fw_server.grade_map import pass_ratio_to_grade
        from bileshke.types import EpistemicGrade
        assert pass_ratio_to_grade(0, 0) == EpistemicGrade.TASAVVUR

    def test_pass_ratio_70_percent(self):
        from fw_server.grade_map import pass_ratio_to_grade
        from bileshke.types import EpistemicGrade
        assert pass_ratio_to_grade(7, 10) == EpistemicGrade.TASDIK

    def test_grade_to_string(self):
        from fw_server.grade_map import grade_to_string
        from bileshke.types import EpistemicGrade
        assert grade_to_string(EpistemicGrade.ILMELYAKIN) == "İlmelyakîn"
        assert grade_to_string(EpistemicGrade.TASDIK) == "Tasdik"
        assert grade_to_string(EpistemicGrade.TASAVVUR) == "Tasavvur"


# ═══════════════════════════════════════════════════════════════════════
# SECTION 3: fw_server.adapters
# ═══════════════════════════════════════════════════════════════════════


class TestAdapters:
    """Tests for fw_server.adapters — stateless JSON→LensResult bridges."""

    def test_adapter_map_has_7_entries(self):
        from fw_server.adapters import ADAPTER_MAP
        assert len(ADAPTER_MAP) == 7

    def test_adapter_map_keys_match_lens_ids(self):
        from fw_server.adapters import ADAPTER_MAP
        from fw_server.types import LENS_IDS
        assert set(ADAPTER_MAP.keys()) == set(LENS_IDS)

    def test_run_lens_unknown_id_raises(self):
        from fw_server.adapters import run_lens
        with pytest.raises(ValueError, match="Unknown lens_id"):
            run_lens("NonExistent", {})

    def test_run_lens_none_params_defaults(self):
        """run_lens with params=None should not crash."""
        from fw_server.adapters import run_lens
        result = run_lens("FOL", None)
        assert result.score >= 0.0

    # ── Per-lens smoke tests (empty params → valid LensResult) ──

    def test_ontoloji_empty_params(self):
        from fw_server.adapters import run_ontoloji
        from bileshke.types import LensId
        r = run_ontoloji({})
        assert r.lens_id == LensId.ONTOLOJI
        assert 0.0 <= r.score < 1.0
        assert r.grade is not None

    def test_mereoloji_empty_params(self):
        from fw_server.adapters import run_mereoloji
        from bileshke.types import LensId
        r = run_mereoloji({})
        assert r.lens_id == LensId.MEREOLOJI
        assert 0.0 <= r.score < 1.0

    def test_fol_empty_params(self):
        from fw_server.adapters import run_fol
        from bileshke.types import LensId
        r = run_fol({})
        assert r.lens_id == LensId.FOL
        assert r.score >= 0.0

    def test_fol_with_text(self):
        from fw_server.adapters import run_fol
        r = run_fol({"text": "AX1 states order. T3 propagates. KV4 bounds."})
        assert r.score >= 0.0
        assert "AX refs" in r.detail

    def test_bayes_empty_params(self):
        from fw_server.adapters import run_bayes
        from bileshke.types import LensId
        r = run_bayes({})
        assert r.lens_id == LensId.BAYES
        assert r.score >= 0.0

    def test_oyun_teorisi_empty_params(self):
        from fw_server.adapters import run_oyun_teorisi
        from bileshke.types import LensId
        r = run_oyun_teorisi({})
        assert r.lens_id == LensId.OYUN_TEORISI
        assert r.score >= 0.0

    def test_kategori_teorisi_empty_params(self):
        from fw_server.adapters import run_kategori_teorisi
        from bileshke.types import LensId
        r = run_kategori_teorisi({})
        assert r.lens_id == LensId.KATEGORI_TEORISI
        assert r.score >= 0.0

    def test_holografik_empty_params(self):
        from fw_server.adapters import run_holografik
        from bileshke.types import LensId
        r = run_holografik({})
        assert r.lens_id == LensId.TOPOLOJI_HOLOGRAFIK
        assert r.score >= 0.0

    # ── KV₇ compliance: fresh instances ──

    def test_kv7_ontoloji_independent(self):
        """Two calls should not share state (KV₇)."""
        from fw_server.adapters import run_ontoloji
        r1 = run_ontoloji({"kavramlar": [
            {"name": "test1",
             "hakikat": [{"isim": "Muhyi", "derece": 0.5}],
             "dual_class": "harfi"}
        ]})
        r2 = run_ontoloji({})
        # r2 should not see test1's kavram
        assert r1.checks_total != r2.checks_total or r1.score != r2.score

    # ── AX56: No adapter returns Hakkalyakîn ──

    def test_ax56_no_adapter_returns_hakkalyakin(self):
        from fw_server.adapters import ADAPTER_MAP
        from bileshke.types import EpistemicGrade
        for lid, adapter in ADAPTER_MAP.items():
            r = adapter({})
            assert r.grade != EpistemicGrade.HAKKALYAKIN, (
                f"AX56 violation in {lid}"
            )

    # ── T6/KV₄: Scores < 1.0 ──

    def test_t6_scores_below_one(self):
        from fw_server.adapters import ADAPTER_MAP
        for lid, adapter in ADAPTER_MAP.items():
            r = adapter({})
            assert r.score < 1.0, f"T6 violation in {lid}: {r.score}"


# ═══════════════════════════════════════════════════════════════════════
# SECTION 4: fw_server.server — MCP tool registration
# ═══════════════════════════════════════════════════════════════════════


class TestServer:
    """Tests for fw_server.server — MCP server and tool functions."""

    def test_mcp_instance_exists(self):
        from fw_server.server import mcp
        assert mcp is not None
        assert mcp.name == "fw-engine"

    def test_run_single_lens_returns_json(self):
        from fw_server.server import run_single_lens
        result = run_single_lens("FOL", None)
        parsed = json.loads(result)
        assert "score" in parsed or "error" in parsed

    def test_run_single_lens_invalid_id(self):
        from fw_server.server import run_single_lens
        result = run_single_lens("INVALID", None)
        parsed = json.loads(result)
        assert "error" in parsed

    def test_run_bileshke_pipeline_default(self):
        from fw_server.server import run_bileshke_pipeline
        result = run_bileshke_pipeline(None, None, None)
        parsed = json.loads(result)
        assert "composite_score" in parsed or "error" in parsed

    def test_check_kavaid_default(self):
        from fw_server.server import check_kavaid
        result = check_kavaid(0.5, None, None)
        parsed = json.loads(result)
        assert isinstance(parsed, dict)

    def test_verify_chains_all(self):
        from fw_server.server import verify_chains
        result = verify_chains(None, None)
        parsed = json.loads(result)
        assert "chains" in parsed or "error" in parsed

    def test_verify_chains_single(self):
        from fw_server.server import verify_chains
        result = verify_chains("C1", None)
        parsed = json.loads(result)
        if "chains" in parsed:
            assert "C1" in parsed["chains"]

    def test_verify_chains_with_text(self):
        from fw_server.server import verify_chains
        result = verify_chains(
            None, "AX30 defines Fakr. T7 derives Creation Motor."
        )
        parsed = json.loads(result)
        if "axiom_refs" in parsed:
            assert "axioms" in parsed["axiom_refs"]

    def test_get_framework_summary(self):
        from fw_server.server import get_framework_summary
        result = get_framework_summary()
        parsed = json.loads(result)
        # Should have at least some of the 6 modules
        assert isinstance(parsed, dict)
        assert len(parsed) >= 1

    def test_hcp_ingest_and_query(self):
        """Full HCP round-trip: ingest → query → diagnostics."""
        from fw_server.server import hcp_ingest, hcp_query, hcp_diagnostics

        # Ingest
        result = hcp_ingest(
            "AX1 states all beings are formally ordered. T6 limits convergence."
        )
        parsed = json.loads(result)
        assert parsed.get("action") == "ingest"
        assert parsed.get("n_chunks", 0) >= 1

        # Query
        result = hcp_query("convergence bound", None, 3)
        parsed = json.loads(result)
        assert parsed.get("action") == "query"

        # Diagnostics
        result = hcp_diagnostics()
        parsed = json.loads(result)
        assert "status" in parsed or "n_chunks" in parsed

    def test_hcp_query_before_ingest_returns_error(self):
        """Query without ingest should return error."""
        # We need a fresh HCP for this — but the singleton persists.
        # So we test that a query at least returns valid JSON.
        from fw_server.server import hcp_query
        result = hcp_query("test query", None, 3)
        parsed = json.loads(result)
        assert isinstance(parsed, dict)


# ═══════════════════════════════════════════════════════════════════════
# SECTION 5: Integration tests — bileshke pipeline end-to-end
# ═══════════════════════════════════════════════════════════════════════


class TestBileshkeIntegration:
    """End-to-end tests through the MCP tool interface."""

    def test_full_pipeline_returns_valid_report(self):
        """Run bileshke pipeline with default params and verify structure."""
        from fw_server.server import run_bileshke_pipeline

        result = run_bileshke_pipeline(None, None, "[1,1,1]")
        parsed = json.loads(result)

        if "error" in parsed:
            pytest.skip(f"Pipeline error: {parsed['error']}")

        assert "composite_score" in parsed
        assert 0.0 <= parsed["composite_score"] < 1.0  # T6/KV₄
        assert "ax57_disclosure" in parsed

    def test_pipeline_kv4_below_threshold(self):
        """composite_score < 0.95 (KV₄ bound)."""
        from fw_server.server import run_bileshke_pipeline

        result = run_bileshke_pipeline(None, None, None)
        parsed = json.loads(result)

        if "error" in parsed:
            pytest.skip(f"Pipeline error: {parsed['error']}")

        score = parsed.get("composite_score", 0.0)
        # If score >= 0.95, the kv4_warning must be present
        if score >= 0.95:
            assert parsed.get("ax57_disclosure", {}).get("kv4_status", "").startswith("FAIL")

    def test_pipeline_ax57_disclosure_present(self):
        """Every pipeline response must carry AX57 disclosure (AX57)."""
        from fw_server.server import run_bileshke_pipeline

        result = run_bileshke_pipeline(None, None, None)
        parsed = json.loads(result)

        if "error" not in parsed:
            disclosure = parsed.get("ax57_disclosure", {})
            assert "composite_score" in disclosure
            assert "grade" in disclosure
            assert "kv4_status" in disclosure
            assert "kv7_status" in disclosure
            assert "ax56_status" in disclosure

    def test_kavaid_all_eight_present(self):
        """check_kavaid must evaluate all 8 kavaid."""
        from fw_server.server import check_kavaid

        result = check_kavaid(
            0.5, "[1,1,1,1,1,1,0]", None
        )
        parsed = json.loads(result)

        if "error" not in parsed:
            assert "descriptions" in parsed
            descriptions = parsed["descriptions"]
            for kv in ["KV1", "KV2", "KV3", "KV4", "KV5", "KV6", "KV7", "KV8"]:
                assert kv in descriptions, f"Missing {kv} in kavaid descriptions"


# ═══════════════════════════════════════════════════════════════════════
# SECTION 6: Package-level imports
# ═══════════════════════════════════════════════════════════════════════


class TestPackageImports:
    """Verify fw_server __init__.py exports."""

    def test_import_adapter_map(self):
        from fw_server import ADAPTER_MAP
        assert len(ADAPTER_MAP) == 7

    def test_import_run_lens(self):
        from fw_server import run_lens
        assert callable(run_lens)

    def test_import_grade_functions(self):
        from fw_server import score_to_grade, pass_ratio_to_grade, grade_to_string
        assert callable(score_to_grade)
        assert callable(pass_ratio_to_grade)
        assert callable(grade_to_string)

    def test_import_constants(self):
        from fw_server import LENS_IDS, VALID_GRADES
        assert len(LENS_IDS) == 7
        assert len(VALID_GRADES) == 3
