"""
Phase 4 Gate Evaluation — Spearman Correlation Benchmark.

Purpose: Determine whether Phases 1–3 regex+rules scoring is sufficient
for lens relevance, or whether Phase 4 NLP is needed.

Gate criterion (WATERFALL_PLAN §8.1):
    Spearman ρ ≥ 0.5 between regex scores and ground-truth → SKIP Phase 4
    Spearman ρ < 0.5 → PROCEED with Phase 4

The benchmark contains 50 test texts spanning 3 categories:
  - IN-VOCABULARY (25): texts using terms present in the regex vocabulary
  - SYNONYM/PARAPHRASE (15): texts expressing same concepts via synonyms
  - CROSS-LENS/ADVERSARIAL (10): multi-domain or irrelevant texts

Ground truth is a 7-element vector per text: [Ont, Mer, FOL, Bay, Oyu, Kat, Hol]
Each value ∈ [0.0, 1.0] representing ideal relevance.

Spearman is computed over all 350 pairs (50 texts × 7 lenses), measuring
whether the regex system produces the correct RANKING of lens relevance.
"""
from __future__ import annotations

import math
import sys
from typing import List, Tuple

import pytest

sys.path.insert(0, ".")

from fw_server.adapters import TEXT_SCORE_MAP

# =====================================================================
# The 7 lenses in canonical order
# =====================================================================
LENS_ORDER = [
    "Ontoloji",
    "Mereoloji",
    "FOL",
    "Bayes",
    "OyunTeorisi",
    "KategoriTeorisi",
    "Topoloji + Holografik",
]

# =====================================================================
# Helper: Spearman rank correlation (stdlib only)
# =====================================================================

def _rank(values: List[float]) -> List[float]:
    """Assign average ranks to values (handles ties)."""
    n = len(values)
    indexed = sorted(range(n), key=lambda i: values[i])
    ranks = [0.0] * n
    i = 0
    while i < n:
        j = i
        # Find group of ties
        while j < n - 1 and values[indexed[j]] == values[indexed[j + 1]]:
            j += 1
        # Average rank for ties (1-based)
        avg_rank = (i + j) / 2.0 + 1.0
        for k in range(i, j + 1):
            ranks[indexed[k]] = avg_rank
        i = j + 1
    return ranks


def spearman_rho(x: List[float], y: List[float]) -> float:
    """Compute Spearman rank correlation coefficient.

    Returns value in [-1, 1]. Handles ties via average ranks.
    """
    assert len(x) == len(y), "Vectors must have equal length"
    n = len(x)
    if n < 3:
        return 0.0

    rx = _rank(x)
    ry = _rank(y)

    # Pearson correlation of ranks
    mean_rx = sum(rx) / n
    mean_ry = sum(ry) / n

    cov = sum((rx[i] - mean_rx) * (ry[i] - mean_ry) for i in range(n))
    std_x = math.sqrt(sum((rx[i] - mean_rx) ** 2 for i in range(n)))
    std_y = math.sqrt(sum((ry[i] - mean_ry) ** 2 for i in range(n)))

    if std_x == 0 or std_y == 0:
        return 0.0

    return cov / (std_x * std_y)


# =====================================================================
# 50 Ground-Truth Benchmark Texts
# =====================================================================
# Each entry: (text, [Ont, Mer, FOL, Bay, Oyu, Kat, Hol])
#
# Categories:
#   IN-VOC (1-25): texts using vocabulary that the regex should catch
#   SYNONYM (26-40): texts using paraphrases / out-of-vocabulary synonyms
#   CROSS/ADV (41-50): multi-domain or irrelevant texts

BENCHMARK: List[Tuple[str, List[float]]] = [
    # ===== IN-VOCABULARY: ONTOLOJI (1-4) =====
    (
        "The tecelli of the Esmâ in every created being reveals the hakikat "
        "that underlies its mahiyet. Each isim manifests with a specific "
        "degree, forming the ontological structure of reality.",
        [0.90, 0.05, 0.05, 0.00, 0.00, 0.00, 0.05],
    ),
    (
        "Vahidiyet and ehadiyet represent two faces of divine unity: the "
        "global containment of all Names under one system, and the local "
        "reflection of all Names in each particular being. Sifat bridge "
        "the gap between Zat and fiil.",
        [0.85, 0.05, 0.00, 0.00, 0.00, 0.00, 0.10],
    ),
    (
        "The mana-yı harfi mode of reading sees everything as pointing "
        "beyond itself to the Names — el-Hakîm, er-Rahmân, el-Musavvir. "
        "This ontological stance treats istinad and delalet as primary.",
        [0.90, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "Sudur from the source of wisdom establishes formal order in "
        "all beings. The şuunat are dispositional roots from which the "
        "seven sifat emerge: hayat, ilim, irade, kudret, sem, basar, kelam.",
        [0.95, 0.00, 0.05, 0.00, 0.00, 0.00, 0.00],
    ),

    # ===== IN-VOCABULARY: MEREOLOJI (5-8) =====
    (
        "The part-whole structure of the system reveals that each component "
        "is not merely a fragment but carries a teleological function. The "
        "mereological containment relation is strict partial order.",
        [0.00, 0.90, 0.05, 0.00, 0.00, 0.00, 0.05],
    ),
    (
        "This text is composed of parts that form a whole. The structure "
        "consists of subparts arranged hierarchically. Each element has "
        "a telos — a purpose that defines its contribution.",
        [0.00, 0.85, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "The transparent vessel mediates between the whole and its parts. "
        "Mereological analysis reveals that containment is not mere spatial "
        "inclusion but teleological participation.",
        [0.05, 0.80, 0.00, 0.00, 0.00, 0.00, 0.05],
    ),
    (
        "Parça and bütün form the basic vocabulary of mereological "
        "investigation. The bölüm is composed of elements each contributing "
        "to the holographic seed of the whole.",
        [0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.15],
    ),

    # ===== IN-VOCABULARY: FOL (9-12) =====
    (
        "AX1 states that ∀x ∈ Being: FormallyOrdered(x). From T3 we derive "
        "that perfection propagates by transitivity. KV₄ constrains the "
        "convergence bound: 0 < C < 1.",
        [0.05, 0.00, 0.95, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "For all x, there exists a y such that x implies y. The logical "
        "structure ∀x∃y(x → y) is the backbone of the formal system. "
        "If and only if the axiom holds, therefore the theorem follows.",
        [0.00, 0.00, 0.90, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "T15 establishes the convergence gradient: structural claims carry "
        "more weight than detail claims. AX57 requires transparency. "
        "AX56 caps the epistemic grade. Necessarily, ¬(grade = Hakkalyakîn).",
        [0.05, 0.00, 0.90, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "Consider the first-order sentence ∃x(P(x) ∧ ¬Q(x)). This implies "
        "that not all P are Q. The proof proceeds by contradiction: assume "
        "∀x(P(x) → Q(x)), which leads to ⊥.",
        [0.00, 0.00, 0.85, 0.00, 0.00, 0.00, 0.00],
    ),

    # ===== IN-VOCABULARY: BAYES (13-16) =====
    (
        "The Bayesian analysis updates the prior probability P(H) with "
        "new evidence to yield the posterior P(H|E). The likelihood ratio "
        "determines how much the evidence shifts our degree of belief.",
        [0.00, 0.00, 0.00, 0.90, 0.00, 0.00, 0.00],
    ),
    (
        "Evidence confirmation through Bayesian updating: start with a "
        "prior, observe data, compute the likelihood, and multiply to get "
        "posterior credence. Probabilistic reasoning is key.",
        [0.00, 0.00, 0.00, 0.85, 0.00, 0.00, 0.00],
    ),
    (
        "The delil and burhan provide ispat for the hypothesis. When the "
        "conditional probability is high and the prior is reasonable, "
        "the Bayesian update yields strong confirmation.",
        [0.10, 0.00, 0.00, 0.80, 0.00, 0.00, 0.00],
    ),
    (
        "Computing P(A|B) = P(B|A) · P(A) / P(B), we see that the "
        "posterior depends on both the prior probability and the likelihood. "
        "Degree of belief updates are the heart of probabilistic inference.",
        [0.00, 0.00, 0.00, 0.90, 0.00, 0.00, 0.00],
    ),

    # ===== IN-VOCABULARY: OYUN TEORISI (17-20) =====
    (
        "The Nash equilibrium emerges when no player can improve their "
        "payoff by unilateral deviation. In this game theory model, "
        "strategy profiles determine the optimal interaction.",
        [0.00, 0.00, 0.00, 0.00, 0.90, 0.00, 0.00],
    ),
    (
        "The nefs station determines the payoff function: at the emmare "
        "level, defection dominates; at mutmainne, cooperation is the "
        "dominant strategy. The rational player adapts.",
        [0.00, 0.00, 0.00, 0.00, 0.90, 0.00, 0.00],
    ),
    (
        "Strategic interaction between agents: each player chooses a "
        "strategy to maximize utility. The payoff matrix shows that "
        "cooperation yields higher equilibrium than defection.",
        [0.00, 0.00, 0.00, 0.00, 0.85, 0.00, 0.00],
    ),
    (
        "Game model analysis: the levvame stage introduces guilt after "
        "defection, shifting the payoff. The nefs ascent from emmare to "
        "mutmainne transforms the game's equilibrium structure.",
        [0.00, 0.00, 0.00, 0.00, 0.85, 0.00, 0.00],
    ),

    # ===== IN-VOCABULARY: KATEGORI TEORISI (21-23) =====
    (
        "The functor F: Rep → Hakikat maps representations to structural "
        "reality. The natural transformation η: Vahidiyet ⇒ Ehadiyet "
        "ensures the diagram commutes. This is a faithful morphism.",
        [0.10, 0.00, 0.00, 0.00, 0.00, 0.90, 0.00],
    ),
    (
        "Category theory provides the backbone: objects, morphisms, and "
        "composition. The functor must be faithful and structure-preserving. "
        "An isomorphism between categories ensures equivalence.",
        [0.00, 0.00, 0.05, 0.00, 0.00, 0.90, 0.00],
    ),
    (
        "The categorical framework requires that every arrow composes "
        "associatively. Natural transformations between functors preserve "
        "the commutative diagram structure. Endofunctor self-maps.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.90, 0.00],
    ),

    # ===== IN-VOCABULARY: HOLOGRAFIK (24-25) =====
    (
        "The holographic seed structure means that the Besmele is "
        "isomorphic to the Fatiha, which is isomorphic to the full text. "
        "B01 through B22 dimensions encode compressed images of the whole. "
        "The cemali and celali aspects are both present.",
        [0.05, 0.05, 0.00, 0.00, 0.00, 0.00, 0.90],
    ),
    (
        "Self-similar fractal structure: every part contains a holographic "
        "reflection of the whole. The part-whole isomorphism means the "
        "seed encodes the tree. Holografik analysis reveals B01-B20 "
        "cemali dimensions plus B21-B22 celali dimensions.",
        [0.00, 0.10, 0.00, 0.00, 0.00, 0.00, 0.90],
    ),

    # ===== SYNONYM/PARAPHRASE: ONTOLOJI (26-28) =====
    (
        "The fundamental nature of things is determined by which governing "
        "principles express themselves through it. Every entity's identity "
        "is constituted by the set of active operational archetypes.",
        [0.80, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "What something truly is differs from what it appears to be. "
        "The apparent specification is merely a shadow of the deeper "
        "constitutive pattern that defines the thing's structural identity.",
        [0.70, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "Continuous expression of foundational principles in created "
        "entities reveals an infinite hierarchy of degrees — no entity "
        "achieves the absolute maximum, which belongs to the source alone.",
        [0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: MEREOLOJI (29-30) =====
    (
        "Every subsystem exists not as a disconnected fragment but as a "
        "purposeful contributor to the larger assembly. The combination "
        "of subordinate units yields an integrated organism.",
        [0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "The organizational hierarchy shows that lower-level modules "
        "serve higher-level goals. Decomposition reveals nested layers "
        "of functional specialization working toward a unified end.",
        [0.00, 0.70, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: FOL (31-32) =====
    (
        "The argument proceeds deductively: given the premises, the "
        "conclusion follows with logical necessity. The chain of "
        "inference is truth-preserving from start to finish.",
        [0.00, 0.00, 0.75, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "Formal derivation establishes the theorem through a sequence "
        "of valid steps. Each proposition is justified by the rules "
        "of the deductive system, ensuring soundness and completeness.",
        [0.00, 0.00, 0.70, 0.00, 0.00, 0.00, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: BAYES (33-34) =====
    (
        "As new information arrives, our confidence in the claim shifts "
        "proportionally. Strong corroboration dramatically increases "
        "our rational expectation of the hypothesis being true.",
        [0.00, 0.00, 0.00, 0.70, 0.00, 0.00, 0.00],
    ),
    (
        "The weight of accumulated observations tips the scales of "
        "judgment. Each piece of corroborating data incrementally "
        "raises the plausibility of the explanatory model.",
        [0.00, 0.00, 0.00, 0.65, 0.00, 0.00, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: OYUN TEORISI (35-36) =====
    (
        "Competing agents each optimize their individual choices while "
        "the collective outcome depends on the interaction of all "
        "decisions. The stable resting point emerges naturally.",
        [0.00, 0.00, 0.00, 0.00, 0.70, 0.00, 0.00],
    ),
    (
        "The moral development of the decision-maker transforms which "
        "outcomes they prefer. At lower stages, selfishness dominates; "
        "at higher stages, altruistic choices yield greater satisfaction.",
        [0.00, 0.00, 0.00, 0.00, 0.65, 0.00, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: KATEGORI TEORISI (37-38) =====
    (
        "The structure-preserving map between mathematical objects must "
        "respect internal composition. When we pass from one domain to "
        "another, the transformation must faithfully carry relationships.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.70, 0.00],
    ),
    (
        "Bridging two abstract systems requires a mapping that maintains "
        "all relational properties. The correspondence must be systematic "
        "and consistent across all levels of the algebraic structure.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.65, 0.00],
    ),

    # ===== SYNONYM/PARAPHRASE: HOLOGRAFIK (39-40) =====
    (
        "Every fragment of the crystal contains the complete diffraction "
        "pattern of the whole. Breaking the system into pieces preserves "
        "the global blueprint in each shard, at reduced resolution.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.75],
    ),
    (
        "The recursive nesting property means that zooming into any "
        "subunit reveals the same organizational template as the global "
        "system. Each micro-cosmos recapitulates the macro-cosmos.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.70],
    ),

    # ===== CROSS-LENS TEXTS (41-46) =====
    (
        "The ontological analysis reveals that Names manifest through "
        "a part-whole structure: el-Muhyî gives life to each component "
        "while the mereological containment preserves the whole's telos.",
        [0.50, 0.50, 0.00, 0.00, 0.00, 0.00, 0.05],
    ),
    (
        "AX17 states that Hakikat(x) = {n ∈ S_Isim | Tecelli(n,x) > 0}. "
        "The functor F: Rep → Hakikat must be faithful. This category-"
        "theoretic formulation of the ontological axiom bridges two lenses.",
        [0.50, 0.00, 0.30, 0.00, 0.00, 0.50, 0.00],
    ),
    (
        "Bayesian evidence for the holographic hypothesis: the probability "
        "that a fractal self-similar structure is coincidental decreases "
        "exponentially with each confirming instance. P(H|E) >> P(H).",
        [0.00, 0.00, 0.00, 0.50, 0.00, 0.00, 0.50],
    ),
    (
        "The game theory of nefs station ascent combined with FOL: "
        "∀agent: nefs_makam(agent) = mutmainne → dominant_strategy = "
        "cooperation. The Nash equilibrium shifts with moral development.",
        [0.00, 0.00, 0.40, 0.00, 0.50, 0.00, 0.00],
    ),
    (
        "The mereological part-whole relation is composed of morphisms "
        "in the categorical framework. Each containment arrow preserves "
        "teleological structure through functorial composition.",
        [0.00, 0.40, 0.00, 0.00, 0.00, 0.50, 0.00],
    ),
    (
        "Evidence accumulates probabilistically: the prior on structural "
        "order (from AX1) is updated by each lens observation. T6 bounds "
        "the posterior: convergence < 1.0. KV₄ constrains the composite.",
        [0.00, 0.00, 0.35, 0.45, 0.00, 0.00, 0.00],
    ),

    # ===== ADVERSARIAL / IRRELEVANT (47-50) =====
    (
        "The weather forecast for tomorrow indicates partly cloudy skies "
        "with a high of 22 degrees Celsius. Light winds from the south "
        "are expected in the afternoon.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "To make chocolate cake, preheat the oven to 180 degrees. Mix "
        "flour, sugar, cocoa powder, and eggs in a bowl. Bake for 30 "
        "minutes until a toothpick comes out clean.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "The quarterly revenue report shows a 12% increase compared to "
        "last year. Operating margins improved due to cost optimization "
        "in the supply chain and reduced overhead expenses.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
    (
        "Python 3.12 introduces several new features: improved error "
        "messages, per-interpreter GIL, and dead battery removal. The "
        "typing module gains TypeVarTuple for variadic generics.",
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ),
]

assert len(BENCHMARK) == 50, f"Expected 50 texts, got {len(BENCHMARK)}"


# =====================================================================
# Gate evaluation functions
# =====================================================================

def _compute_regex_scores(text: str) -> List[float]:
    """Run all 7 per-lens regex scorers on a text."""
    return [TEXT_SCORE_MAP[lens](text) for lens in LENS_ORDER]


def run_gate_evaluation() -> dict:
    """Run the full gate evaluation and return detailed results.

    Returns dict with:
        rho: Spearman correlation coefficient
        per_lens_rho: {lens_id: per-lens ρ}
        in_vocab_rho: ρ on in-vocabulary subset (texts 1-25)
        synonym_rho: ρ on synonym subset (texts 26-40)
        cross_rho: ρ on cross-lens subset (texts 41-46)
        decision: "SKIP" or "PROCEED"
    """
    all_gt = []
    all_regex = []
    in_vocab_gt = []
    in_vocab_regex = []
    synonym_gt = []
    synonym_regex = []
    cross_gt = []
    cross_regex = []

    # Per-lens accumulators
    per_lens_gt = {lens: [] for lens in LENS_ORDER}
    per_lens_regex = {lens: [] for lens in LENS_ORDER}

    for idx, (text, gt) in enumerate(BENCHMARK):
        regex_scores = _compute_regex_scores(text)

        for i, lens in enumerate(LENS_ORDER):
            all_gt.append(gt[i])
            all_regex.append(regex_scores[i])
            per_lens_gt[lens].append(gt[i])
            per_lens_regex[lens].append(regex_scores[i])

            if idx < 25:
                in_vocab_gt.append(gt[i])
                in_vocab_regex.append(regex_scores[i])
            elif idx < 40:
                synonym_gt.append(gt[i])
                synonym_regex.append(regex_scores[i])
            elif idx < 46:
                cross_gt.append(gt[i])
                cross_regex.append(regex_scores[i])

    rho = spearman_rho(all_gt, all_regex)
    per_lens_rho = {
        lens: spearman_rho(per_lens_gt[lens], per_lens_regex[lens])
        for lens in LENS_ORDER
    }
    in_vocab_r = spearman_rho(in_vocab_gt, in_vocab_regex) if in_vocab_gt else 0.0
    synonym_r = spearman_rho(synonym_gt, synonym_regex) if synonym_gt else 0.0
    cross_r = spearman_rho(cross_gt, cross_regex) if cross_gt else 0.0

    decision = "SKIP" if rho >= 0.5 else "PROCEED"

    return {
        "rho": rho,
        "per_lens_rho": per_lens_rho,
        "in_vocab_rho": in_vocab_r,
        "synonym_rho": synonym_r,
        "cross_rho": cross_r,
        "decision": decision,
        "n_texts": len(BENCHMARK),
        "n_pairs": len(all_gt),
    }


# =====================================================================
# Tests
# =====================================================================

class TestSpearmanHelper:
    """Unit tests for the Spearman correlation implementation."""

    def test_perfect_positive(self):
        """Identical rankings → ρ = 1.0."""
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        y = [10.0, 20.0, 30.0, 40.0, 50.0]
        assert abs(spearman_rho(x, y) - 1.0) < 1e-9

    def test_perfect_negative(self):
        """Reversed rankings → ρ = -1.0."""
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        y = [50.0, 40.0, 30.0, 20.0, 10.0]
        assert abs(spearman_rho(x, y) - (-1.0)) < 1e-9

    def test_no_correlation(self):
        """Orthogonal data → ρ near 0."""
        x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
        y = [3.0, 6.0, 1.0, 4.0, 2.0, 5.0]
        rho = spearman_rho(x, y)
        # Not exactly 0, but should be small
        assert abs(rho) < 0.5

    def test_ties_handled(self):
        """Tied values get average ranks."""
        x = [1.0, 1.0, 3.0, 4.0]
        y = [1.0, 2.0, 3.0, 4.0]
        rho = spearman_rho(x, y)
        assert 0.5 < rho < 1.0  # Strong but not perfect

    def test_all_zeros(self):
        """All zeros → ρ = 0 (no variance)."""
        x = [0.0, 0.0, 0.0, 0.0]
        y = [1.0, 2.0, 3.0, 4.0]
        assert spearman_rho(x, y) == 0.0

    def test_rank_function(self):
        """Verify rank assignment with ties."""
        ranks = _rank([3.0, 1.0, 2.0])
        assert ranks == [3.0, 1.0, 2.0]

    def test_rank_with_ties(self):
        """Verify average rank for ties."""
        ranks = _rank([1.0, 2.0, 2.0, 4.0])
        assert ranks[0] == 1.0
        assert ranks[1] == 2.5
        assert ranks[2] == 2.5
        assert ranks[3] == 4.0


class TestBenchmarkIntegrity:
    """Verify the benchmark dataset is well-formed."""

    def test_50_texts(self):
        """Benchmark has exactly 50 entries."""
        assert len(BENCHMARK) == 50

    def test_7_dimensions(self):
        """Each ground truth vector has 7 elements."""
        for i, (_, gt) in enumerate(BENCHMARK):
            assert len(gt) == 7, f"Text {i+1} has {len(gt)} dimensions"

    def test_values_in_range(self):
        """All ground truth values in [0, 1]."""
        for i, (_, gt) in enumerate(BENCHMARK):
            for j, v in enumerate(gt):
                assert 0.0 <= v <= 1.0, (
                    f"Text {i+1}, lens {j}: value {v} out of range"
                )

    def test_no_empty_texts(self):
        """No text is empty."""
        for i, (text, _) in enumerate(BENCHMARK):
            assert len(text.strip()) > 0, f"Text {i+1} is empty"

    def test_in_vocab_have_signal(self):
        """First 25 texts (in-vocabulary) each have at least one lens > 0.5."""
        for i in range(25):
            _, gt = BENCHMARK[i]
            assert max(gt) >= 0.5, (
                f"In-vocab text {i+1} has no strong signal: {gt}"
            )

    def test_adversarial_are_zero(self):
        """Last 4 texts (adversarial) have all-zero ground truth."""
        for i in range(46, 50):
            _, gt = BENCHMARK[i]
            assert all(v == 0.0 for v in gt), (
                f"Adversarial text {i+1} should be all-zero: {gt}"
            )


class TestRegexScoringSanity:
    """Verify the regex scoring functions produce expected behavior
    on a few obvious in-vocabulary texts."""

    def test_ontoloji_text_scores_high(self):
        """Text with ontology terms → Ontoloji scorer > 0."""
        text = BENCHMARK[0][0]
        score = TEXT_SCORE_MAP["Ontoloji"](text)
        assert score > 0.0, f"Ontoloji scorer returned {score}"

    def test_fol_text_scores_high(self):
        """Text with FOL terms → FOL scorer > 0."""
        text = BENCHMARK[8][0]
        score = TEXT_SCORE_MAP["FOL"](text)
        assert score > 0.0, f"FOL scorer returned {score}"

    def test_bayes_text_scores_high(self):
        """Text with Bayes terms → Bayes scorer > 0."""
        text = BENCHMARK[12][0]
        score = TEXT_SCORE_MAP["Bayes"](text)
        assert score > 0.0, f"Bayes scorer returned {score}"

    def test_irrelevant_text_scores_zero(self):
        """Weather forecast → all scorers return 0."""
        text = BENCHMARK[46][0]
        for lens in LENS_ORDER:
            score = TEXT_SCORE_MAP[lens](text)
            assert score == 0.0, f"{lens} scored {score} on irrelevant text"

    def test_no_score_exceeds_cap(self):
        """No regex score exceeds its lens cap."""
        caps = {
            "Ontoloji": 0.65,
            "Mereoloji": 0.50,
            "FOL": 0.70,
            "Bayes": 0.55,
            "OyunTeorisi": 0.50,
            "KategoriTeorisi": 0.45,
            "Topoloji + Holografik": 0.55,
        }
        for text, _ in BENCHMARK:
            for lens in LENS_ORDER:
                score = TEXT_SCORE_MAP[lens](text)
                assert score <= caps[lens] + 1e-9, (
                    f"{lens} score {score} exceeds cap {caps[lens]}"
                )


class TestGateEvaluation:
    """The actual gate evaluation — determines Phase 4 skip/proceed."""

    @pytest.fixture(scope="class")
    def gate_result(self):
        """Run the gate evaluation once (expensive)."""
        return run_gate_evaluation()

    def test_gate_runs(self, gate_result):
        """Gate evaluation completes without error."""
        assert gate_result is not None
        assert "rho" in gate_result
        assert "decision" in gate_result

    def test_rho_is_valid(self, gate_result):
        """Global ρ is in valid range [-1, 1]."""
        rho = gate_result["rho"]
        assert -1.0 <= rho <= 1.0, f"ρ = {rho} out of range"

    def test_n_pairs(self, gate_result):
        """Evaluated 350 pairs (50 texts × 7 lenses)."""
        assert gate_result["n_pairs"] == 350

    def test_in_vocab_rho_strongest(self, gate_result):
        """In-vocabulary ρ should be the strongest (regex was built for this)."""
        assert gate_result["in_vocab_rho"] >= gate_result["synonym_rho"], (
            f"In-vocab ρ ({gate_result['in_vocab_rho']:.3f}) < "
            f"synonym ρ ({gate_result['synonym_rho']:.3f})"
        )

    def test_synonym_rho_reveals_weakness(self, gate_result):
        """Synonym ρ is expected to be lower (regex can't catch paraphrases)."""
        # This is an observation, not a pass/fail — we record it
        # If synonym ρ is already good, that's great (regex is robust)
        pass  # Recorded in the report

    def test_gate_decision_recorded(self, gate_result):
        """Decision is either SKIP or PROCEED."""
        assert gate_result["decision"] in ("SKIP", "PROCEED")

    def test_gate_report(self, gate_result, capsys):
        """Print the full gate evaluation report for human review."""
        print("\n" + "=" * 70)
        print("  PHASE 4 GATE EVALUATION REPORT")
        print("=" * 70)
        print(f"\n  Global Spearman ρ:     {gate_result['rho']:.4f}")
        print(f"  In-vocabulary ρ:       {gate_result['in_vocab_rho']:.4f}")
        print(f"  Synonym/paraphrase ρ:  {gate_result['synonym_rho']:.4f}")
        print(f"  Cross-lens ρ:          {gate_result['cross_rho']:.4f}")
        print(f"\n  Per-lens ρ:")
        for lens, r in gate_result["per_lens_rho"].items():
            print(f"    {lens:30s} {r:+.4f}")
        print(f"\n  Pairs evaluated:       {gate_result['n_pairs']}")
        print(f"\n  GATE THRESHOLD:        ρ ≥ 0.5 → SKIP Phase 4")
        print(f"  ACTUAL ρ:              {gate_result['rho']:.4f}")
        print(f"  {'─' * 40}")
        print(f"  DECISION:              *** {gate_result['decision']} Phase 4 ***")
        print("=" * 70)

        # Always passes — this is a report, not a gate
        assert True


# =====================================================================
# Standalone entry point
# =====================================================================
if __name__ == "__main__":
    result = run_gate_evaluation()
    print("\n" + "=" * 70)
    print("  PHASE 4 GATE EVALUATION REPORT")
    print("=" * 70)
    print(f"\n  Global Spearman ρ:     {result['rho']:.4f}")
    print(f"  In-vocabulary ρ:       {result['in_vocab_rho']:.4f}")
    print(f"  Synonym/paraphrase ρ:  {result['synonym_rho']:.4f}")
    print(f"  Cross-lens ρ:          {result['cross_rho']:.4f}")
    print(f"\n  Per-lens ρ:")
    for lens, r in result["per_lens_rho"].items():
        print(f"    {lens:30s} {r:+.4f}")
    print(f"\n  Pairs evaluated: {result['n_pairs']}")
    print(f"\n  GATE THRESHOLD: ρ ≥ 0.5 → SKIP Phase 4")
    print(f"  ACTUAL ρ:       {result['rho']:.4f}")
    print(f"  {'─' * 40}")
    print(f"  DECISION:       *** {result['decision']} Phase 4 ***")
    print("=" * 70)
