"""
tests/test_validate_stage.py — Tests for the validate_stage MCP tool.

Covers:
  - _STAGE_LENS_MAP structure and completeness
  - validate_stage basic invocation for every stage identifier
  - Gate verdict logic: PASS / WARN / FAIL
  - Anomaly detection: ZERO-SCORE, KAVAID-FAIL, KV4-BREACH, LENS-ERROR
  - Remediation hint generation
  - ax57_compact format
  - JSON round-trip and required response keys
  - Edge cases: invalid stage, empty summary, whitespace/case handling

Design:
  KV₇: Each test function is independent — no shared state.
  AX56: validate_stage must never return grade = Hakkalyakîn.
  T6/KV₄: composite_score always < 1.0 in non-error cases.
"""

from __future__ import annotations

import json
import pytest


# ═══════════════════════════════════════════════════════════════════════
# SECTION 1: _STAGE_LENS_MAP structure
# ═══════════════════════════════════════════════════════════════════════


class TestStageLensMap:
    """Tests for the _STAGE_LENS_MAP configuration dict."""

    def test_stage_lens_map_exists(self):
        from fw_server.server import _STAGE_LENS_MAP
        assert isinstance(_STAGE_LENS_MAP, dict)

    def test_stage_lens_map_has_all_stages(self):
        """Every pipeline stage identifier must have an entry."""
        from fw_server.server import _STAGE_LENS_MAP
        expected = {
            "preflight", "1", "2", "3", "4", "5", "6", "7", "8",
            "9", "9a", "9b",
        }
        assert expected == set(_STAGE_LENS_MAP.keys())

    def test_stage_lens_map_values_are_lists(self):
        from fw_server.server import _STAGE_LENS_MAP
        for stage, lenses in _STAGE_LENS_MAP.items():
            assert isinstance(lenses, list), f"Stage {stage}: expected list"
            assert len(lenses) >= 1, f"Stage {stage}: empty lens list"

    def test_stage_lens_map_values_are_valid_lens_ids(self):
        from fw_server.server import _STAGE_LENS_MAP
        from fw_server.types import LENS_IDS
        valid = set(LENS_IDS)
        for stage, lenses in _STAGE_LENS_MAP.items():
            for lid in lenses:
                assert lid in valid, (
                    f"Stage {stage}: '{lid}' not in LENS_IDS"
                )

    def test_stages_9_9a_9b_cover_all_lenses(self):
        """Final stages must run full analysis (all 7 lenses)."""
        from fw_server.server import _STAGE_LENS_MAP
        from fw_server.types import LENS_IDS
        for stage in ("9", "9a", "9b"):
            assert set(_STAGE_LENS_MAP[stage]) == set(LENS_IDS), (
                f"Stage {stage} should cover all lenses"
            )

    def test_stage_1_uses_ontoloji(self):
        """Stage 1 (Output Translation) → Ontoloji lens."""
        from fw_server.server import _STAGE_LENS_MAP
        assert "Ontoloji" in _STAGE_LENS_MAP["1"]

    def test_stage_2_uses_fol(self):
        """Stage 2 (Foundational Commitments) → FOL lens."""
        from fw_server.server import _STAGE_LENS_MAP
        assert "FOL" in _STAGE_LENS_MAP["2"]

    def test_stage_4_uses_bayes(self):
        """Stage 4 (Epistemic Architecture) → Bayes lens."""
        from fw_server.server import _STAGE_LENS_MAP
        assert "Bayes" in _STAGE_LENS_MAP["4"]


# ═══════════════════════════════════════════════════════════════════════
# SECTION 2: validate_stage — basic invocation
# ═══════════════════════════════════════════════════════════════════════


class TestValidateStageBasic:
    """Basic invocation tests — every stage returns valid JSON."""

    @pytest.mark.parametrize("stage", [
        "preflight", "1", "2", "3", "4", "5", "6", "7", "8",
        "9", "9a", "9b",
    ])
    def test_returns_valid_json(self, stage):
        from fw_server.server import validate_stage
        result = validate_stage(stage, f"Test stage {stage}")
        parsed = json.loads(result)
        assert isinstance(parsed, dict)
        # Should NOT be an error-only response
        assert "gate" in parsed or "error" in parsed

    @pytest.mark.parametrize("stage", [
        "preflight", "1", "2", "3", "4", "5", "6", "7", "8",
        "9", "9a", "9b",
    ])
    def test_required_keys_present(self, stage):
        """Every successful response must carry all required keys."""
        from fw_server.server import validate_stage
        result = validate_stage(stage, "testing")
        parsed = json.loads(result)
        if "error" in parsed:
            pytest.skip(f"Tool error: {parsed['error']}")
        required = {
            "gate", "composite_score", "kavaid", "kavaid_pass_count",
            "relevant_lens_scores", "all_lens_scores", "anomalies",
            "remediation", "ax57_compact", "stage", "summary",
            "completeness", "latife_gate", "ortam_gate",
        }
        missing = required - set(parsed.keys())
        assert not missing, f"Missing keys for stage {stage}: {missing}"

    def test_empty_summary_accepted(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "")
        parsed = json.loads(result)
        assert "gate" in parsed or "error" in parsed

    def test_default_summary(self):
        """Calling with only stage arg should work (summary defaults)."""
        from fw_server.server import validate_stage
        result = validate_stage("1")
        parsed = json.loads(result)
        assert "gate" in parsed or "error" in parsed

    def test_stage_echoed_in_response(self):
        from fw_server.server import validate_stage
        result = validate_stage("5", "echo test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert parsed["stage"] == "5"
            assert parsed["summary"] == "echo test"


# ═══════════════════════════════════════════════════════════════════════
# SECTION 3: Gate verdict logic
# ═══════════════════════════════════════════════════════════════════════


class TestGateVerdict:
    """Tests for the PASS / WARN / FAIL gate logic."""

    def test_gate_is_valid_value(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "verdict test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert parsed["gate"] in ("PASS", "WARN", "FAIL")

    def test_pass_has_no_anomalies(self):
        """If gate=PASS, anomalies must be empty."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "pass test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] == "PASS":
            assert parsed["anomalies"] == []

    def test_warn_has_anomalies(self):
        """If gate=WARN, anomalies list must be non-empty."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "warn test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] == "WARN":
            assert len(parsed["anomalies"]) > 0

    def test_fail_has_critical_anomaly(self):
        """If gate=FAIL, must have KV4-BREACH or ERROR anomaly."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "fail test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] == "FAIL":
            has_critical = any(
                a.startswith("KV4-BREACH") or a.startswith("ERROR")
                for a in parsed["anomalies"]
            )
            assert has_critical

    def test_gate_not_pass_implies_non_empty_remediation(self):
        """Non-PASS gates should have remediation hints."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "remediation test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] != "PASS":
            assert len(parsed["remediation"]) > 0


# ═══════════════════════════════════════════════════════════════════════
# SECTION 4: Structural constraints (T6, KV₄, AX56)
# ═══════════════════════════════════════════════════════════════════════


class TestStructuralConstraints:
    """Framework constraints that validate_stage must honour."""

    def test_t6_composite_below_one(self):
        """T6/KV₄: composite_score must be < 1.0."""
        from fw_server.server import validate_stage
        result = validate_stage("9a", "T6 test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert parsed["composite_score"] < 1.0, (
                f"T6 violation: composite={parsed['composite_score']}"
            )

    def test_kv4_high_composite_flagged(self):
        """If composite ≥ 0.95 somehow, anomalies must include KV4-BREACH."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "KV4 test")
        parsed = json.loads(result)
        if "error" not in parsed:
            score = parsed["composite_score"]
            if score >= 0.95:
                kv4_anomalies = [
                    a for a in parsed["anomalies"]
                    if "KV4-BREACH" in a
                ]
                assert len(kv4_anomalies) > 0

    def test_ax56_grade_never_hakkalyakin(self):
        """AX56: epistemic grade must never reach Hakkalyakîn."""
        from fw_server.server import validate_stage
        result = validate_stage("9a", "AX56 test")
        parsed = json.loads(result)
        if "error" not in parsed:
            ax57 = parsed["ax57_compact"]
            assert "Hakkalyakîn" not in ax57, (
                f"AX56 violation in ax57_compact: {ax57}"
            )

    def test_all_7_lenses_scored(self):
        """all_lens_scores must contain all 7 lens IDs."""
        from fw_server.server import validate_stage
        from fw_server.types import LENS_IDS
        result = validate_stage("9a", "full lens test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert set(parsed["all_lens_scores"].keys()) == set(LENS_IDS)

    def test_kavaid_pass_count_in_range(self):
        """kavaid_pass_count must be 0–8."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "kavaid count test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert 0 <= parsed["kavaid_pass_count"] <= 8


# ═══════════════════════════════════════════════════════════════════════
# SECTION 5: Anomaly detection — stage awareness
# ═══════════════════════════════════════════════════════════════════════


class TestAnomalyDetection:
    """Stage-aware anomaly detection tests."""

    def test_relevant_lens_scores_match_stage_map(self):
        """relevant_lens_scores must contain exactly the lenses from _STAGE_LENS_MAP."""
        from fw_server.server import validate_stage, _STAGE_LENS_MAP
        for stage in ("1", "4", "9a"):
            result = validate_stage(stage, "relevance test")
            parsed = json.loads(result)
            if "error" in parsed:
                continue
            expected_lenses = set(_STAGE_LENS_MAP[stage])
            actual_lenses = set(parsed["relevant_lens_scores"].keys())
            assert expected_lenses == actual_lenses, (
                f"Stage {stage}: expected {expected_lenses}, got {actual_lenses}"
            )

    def test_zero_score_anomaly_format(self):
        """ZERO-SCORE anomalies must reference lens ID and stage."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "zero score test")
        parsed = json.loads(result)
        if "error" not in parsed:
            for a in parsed["anomalies"]:
                if a.startswith("ZERO-SCORE"):
                    assert "stage" in a.lower() or "0.0" in a

    def test_kavaid_failures_detected(self):
        """If a kavaid check fails, anomalies must include KAVAID-FAIL."""
        from fw_server.server import validate_stage
        result = validate_stage("5", "kavaid test")
        parsed = json.loads(result)
        if "error" not in parsed:
            kavaid = parsed["kavaid"]
            checks = kavaid.get("checks", kavaid)
            for kv_id, val in checks.items():
                if val is False:
                    kavaid_anomalies = [
                        a for a in parsed["anomalies"]
                        if f"KAVAID-FAIL: {kv_id}" in a
                    ]
                    assert len(kavaid_anomalies) > 0, (
                        f"{kv_id} failed but not in anomalies"
                    )


# ═══════════════════════════════════════════════════════════════════════
# SECTION 6: AX57 compact disclosure
# ═══════════════════════════════════════════════════════════════════════


class TestAX57Compact:
    """Tests for the ax57_compact disclosure line."""

    def test_ax57_format(self):
        """ax57_compact must follow: DUSUN: stage=... | composite=... | kavaid=.../8 | gate=... | grade=..."""
        from fw_server.server import validate_stage
        result = validate_stage("3", "ax57 format test")
        parsed = json.loads(result)
        if "error" not in parsed:
            ax57 = parsed["ax57_compact"]
            assert ax57.startswith("DUSUN: stage=")
            assert "composite=" in ax57
            assert "kavaid=" in ax57
            assert "/8" in ax57
            assert "gate=" in ax57
            assert "grade=" in ax57

    def test_ax57_stage_matches(self):
        """The stage= in ax57_compact must match the input stage."""
        from fw_server.server import validate_stage
        result = validate_stage("7", "stage match test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert "stage=7" in parsed["ax57_compact"]

    def test_ax57_gate_matches(self):
        """The gate= in ax57_compact must match the gate field."""
        from fw_server.server import validate_stage
        result = validate_stage("2", "gate match test")
        parsed = json.loads(result)
        if "error" not in parsed:
            gate = parsed["gate"]
            assert f"gate={gate}" in parsed["ax57_compact"]

    def test_ax57_grade_is_valid(self):
        """Grade in ax57_compact must be one of the 3 valid grades (AX56)."""
        from fw_server.server import validate_stage
        valid_grades = {"Tasavvur", "Tasdik", "İlmelyakîn"}
        result = validate_stage("1", "grade test")
        parsed = json.loads(result)
        if "error" not in parsed:
            ax57 = parsed["ax57_compact"]
            grade_part = ax57.split("grade=")[1] if "grade=" in ax57 else ""
            assert any(g in grade_part for g in valid_grades), (
                f"Invalid grade in ax57: {ax57}"
            )


# ═══════════════════════════════════════════════════════════════════════
# SECTION 7: Edge cases
# ═══════════════════════════════════════════════════════════════════════


class TestEdgeCases:
    """Edge case and robustness tests."""

    def test_whitespace_stage_trimmed(self):
        """Leading/trailing whitespace in stage should be handled."""
        from fw_server.server import validate_stage
        result = validate_stage("  3  ", "whitespace test")
        parsed = json.loads(result)
        # Should not crash — either success or graceful error
        assert isinstance(parsed, dict)

    def test_uppercase_stage_normalised(self):
        """Stage identifiers should be case-insensitive."""
        from fw_server.server import validate_stage
        r1 = validate_stage("9A", "upper test")
        p1 = json.loads(r1)
        r2 = validate_stage("9a", "lower test")
        p2 = json.loads(r2)
        # Both should produce the same gate (same lenses)
        if "error" not in p1 and "error" not in p2:
            assert p1["gate"] == p2["gate"]

    def test_unknown_stage_uses_all_lenses(self):
        """An unrecognised stage should fall back to all lenses."""
        from fw_server.server import validate_stage
        from fw_server.types import LENS_IDS
        result = validate_stage("unknown_stage_xyz", "fallback test")
        parsed = json.loads(result)
        if "error" not in parsed:
            # Should fall back to all lenses as relevant
            assert set(parsed["relevant_lens_scores"].keys()) == set(LENS_IDS)

    def test_error_response_has_traceback(self):
        """If an internal error occurs, response must have traceback."""
        # We can't easily force an internal error, but verify the
        # except branch structure by checking a valid call doesn't
        # produce error+traceback
        from fw_server.server import validate_stage
        result = validate_stage("1", "error structure test")
        parsed = json.loads(result)
        if "error" in parsed:
            assert "traceback" in parsed

    def test_completeness_is_float(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "completeness type test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert isinstance(parsed["completeness"], (int, float))

    def test_latife_gate_is_numeric(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "latife gate type test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert isinstance(parsed["latife_gate"], (int, float))

    def test_ortam_gate_is_numeric(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "ortam gate type test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert isinstance(parsed["ortam_gate"], (int, float))


# ═══════════════════════════════════════════════════════════════════════
# SECTION 8: Remediation hints
# ═══════════════════════════════════════════════════════════════════════


class TestRemediation:
    """Tests for remediation hint generation (Prescription 3)."""

    def test_remediation_is_list(self):
        from fw_server.server import validate_stage
        result = validate_stage("1", "remediation type test")
        parsed = json.loads(result)
        if "error" not in parsed:
            assert isinstance(parsed["remediation"], list)

    def test_pass_has_empty_remediation(self):
        """PASS gate should have no remediation hints."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "pass remediation test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] == "PASS":
            assert parsed["remediation"] == []

    def test_anomalies_and_remediation_same_length(self):
        """Each anomaly should have exactly one remediation hint."""
        from fw_server.server import validate_stage
        result = validate_stage("1", "1:1 remediation test")
        parsed = json.loads(result)
        if "error" not in parsed and parsed["gate"] != "PASS":
            assert len(parsed["remediation"]) == len(parsed["anomalies"])


# ═══════════════════════════════════════════════════════════════════════
# SECTION 9: KV₇ compliance — independence
# ═══════════════════════════════════════════════════════════════════════


class TestKV7Independence:
    """validate_stage must not share state between invocations (KV₇)."""

    def test_sequential_calls_independent(self):
        """Two consecutive calls should not contaminate each other."""
        from fw_server.server import validate_stage
        r1 = validate_stage("1", "first call")
        r2 = validate_stage("1", "second call")
        p1 = json.loads(r1)
        p2 = json.loads(r2)
        if "error" not in p1 and "error" not in p2:
            # Same stage, same (empty) content → same scores
            assert p1["composite_score"] == p2["composite_score"]
            assert p1["gate"] == p2["gate"]

    def test_different_stages_produce_different_relevant_scores(self):
        """Different stages should have different relevant_lens_scores keys."""
        from fw_server.server import validate_stage
        r1 = validate_stage("1", "ontoloji stage")  # Ontoloji only
        r4 = validate_stage("4", "bayes stage")      # Bayes only
        p1 = json.loads(r1)
        p4 = json.loads(r4)
        if "error" not in p1 and "error" not in p4:
            assert set(p1["relevant_lens_scores"].keys()) != \
                   set(p4["relevant_lens_scores"].keys())
