"""Self-tests for the evaluation framework.

These tests validate that the harness modules load, parse, and compute
correctly — without requiring an OpenAI key or actual LLM calls.
"""

from __future__ import annotations

import json
import math
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

import pytest

# ── corpus ────────────────────────────────────────────────────────

def test_corpus_schema_is_valid_json():
    """schema.json is well-formed JSON Schema."""
    schema_path = Path(__file__).resolve().parent.parent / "evaluation" / "corpus" / "schema.json"
    data = json.loads(schema_path.read_text(encoding="utf-8"))
    assert data.get("type") == "object"
    assert "properties" in data
    assert "texts" in data["properties"]


def test_corpus_loader():
    """load_corpus returns a non-empty list of TestText."""
    from evaluation.corpus.loader import load_corpus
    corpus = load_corpus()
    assert len(corpus) == 54
    assert all(hasattr(t, "id") for t in corpus)
    assert all(hasattr(t, "domain") for t in corpus)


def test_corpus_filter():
    from evaluation.corpus.loader import filter_corpus, load_corpus
    corpus = load_corpus()
    math_texts = filter_corpus(corpus, domain="MATH")
    assert all(t.domain == "MATH" for t in math_texts)
    assert len(math_texts) == 9  # 3 difficulties × 3 each


# ── config ────────────────────────────────────────────────────────

def test_config_loads():
    from evaluation.config import EXPERIMENT_CONFIG
    assert EXPERIMENT_CONFIG["version"] == "1.0"
    assert EXPERIMENT_CONFIG["seed"] == 42
    assert len(EXPERIMENT_CONFIG["conditions"]) == 3


# ── cache ─────────────────────────────────────────────────────────

def test_cache_round_trip(tmp_path):
    from evaluation.cache import ResultCache
    cache = ResultCache(tmp_path / "cache")
    cache.set("test_key", {"score": 42})
    result = cache.get("test_key")
    assert result == {"score": 42}


def test_cache_miss(tmp_path):
    from evaluation.cache import ResultCache
    cache = ResultCache(tmp_path / "cache")
    assert cache.get("nonexistent") is None


# ── layer 1 programmatic judge ────────────────────────────────────

def test_layer1_empty_output():
    from evaluation.judges.layer1_programmatic import evaluate_layer1
    scores = evaluate_layer1("", "some input text")
    assert scores.lens_coverage == 0
    assert scores.term_precision == 0.0


def test_layer1_detects_lenses():
    from evaluation.judges.layer1_programmatic import evaluate_layer1
    output = (
        "## Ontolojik Analiz\n"
        "Ontological entity hierarchy of concepts is explored. "
        "Entity relations and entities matter here.\n\n"
        "## Bayes Analizi\n"
        "Prior probability posterior update with likelihood estimation. "
        "Bayesian inference and P(A|B)…\n"
    )
    scores = evaluate_layer1(output, "test text")
    assert scores.lens_coverage >= 2
    assert "Ontoloji" in scores.per_lens_detected
    assert "Bayes" in scores.per_lens_detected


# ── layer 2 (mocked LLM) ─────────────────────────────────────────

@pytest.mark.asyncio
async def test_layer2_with_mock():
    from evaluation.judges.layer2_lens_depth import evaluate_layer2
    mock_judge = AsyncMock()
    mock_judge.judge = AsyncMock(return_value="Score: 4\nReasoning: Good depth.")
    result = await evaluate_layer2(
        mock_judge, "some output", "some input",
        {"Ontoloji": True, "Bayes": False},
    )
    assert "Ontoloji" in result
    assert 1 <= result["Ontoloji"] <= 5
    assert result["Bayes"] == 0.0


# ── layer 3 (mocked LLM) ─────────────────────────────────────────

@pytest.mark.asyncio
async def test_layer3_with_mock():
    from evaluation.judges.layer3_integration import evaluate_layer3
    mock_judge = AsyncMock()
    mock_judge.judge = AsyncMock(return_value="Score: 3\nGood integration.")
    result = await evaluate_layer3(mock_judge, "some output", "some input")
    assert 1 <= result <= 5


# ── layer 4 (mocked LLM) ─────────────────────────────────────────

@pytest.mark.asyncio
async def test_layer4_with_mock():
    from evaluation.judges.layer4_discovery import evaluate_layer4
    mock_judge = AsyncMock()
    mock_judge.judge = AsyncMock(
        side_effect=[
            "Count: 2\nNovel connections found.",
            "Score: 4\nStrong error prevention.",
        ]
    )
    result = await evaluate_layer4(mock_judge, "some output", "some input", repeat=1)
    assert result.discovery_count >= 0
    assert result.error_prevention >= 0


# ── pairwise (mocked) ────────────────────────────────────────────

@pytest.mark.asyncio
async def test_pairwise_compare():
    from evaluation.judges.pairwise import compare_pair
    mock_judge = AsyncMock()
    mock_judge.judge = AsyncMock(
        return_value="Winner: A\nReasoning: A is better.\n"
                     "Structural depth: A\nMulti-perspective: A"
    )
    result = await compare_pair(
        mock_judge, "T1", "input", "out_a", "COND_A", "out_b", "COND_B",
    )
    assert result.winner in ("A", "B", "TIE")
    assert result.true_winner in ("COND_A", "COND_B", "TIE")


# ── Bradley-Terry ─────────────────────────────────────────────────

def test_bradley_terry_basic():
    from evaluation.stats.bradley_terry import fit_bradley_terry, strengths_to_elo
    records = [
        {"winner": "A", "loser": "B", "cond_a": "A", "cond_b": "B"},
        {"winner": "A", "loser": "B", "cond_a": "A", "cond_b": "B"},
        {"winner": "B", "loser": "A", "cond_a": "A", "cond_b": "B"},
    ]
    strengths = fit_bradley_terry(records, ["A", "B"])
    assert strengths["A"] > strengths["B"]  # A wins 2/3

    elo = strengths_to_elo(strengths)
    assert elo["A"] > elo["B"]


def test_bradley_terry_tie():
    from evaluation.stats.bradley_terry import fit_bradley_terry
    records = [
        {"winner": "TIE", "cond_a": "X", "cond_b": "Y"},
        {"winner": "TIE", "cond_a": "X", "cond_b": "Y"},
    ]
    strengths = fit_bradley_terry(records, ["X", "Y"])
    assert abs(strengths["X"] - strengths["Y"]) < 0.05


# ── hypothesis tests ──────────────────────────────────────────────

def test_wilcoxon_paired():
    from evaluation.stats.hypothesis_tests import wilcoxon_paired
    a = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    b = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
    result = wilcoxon_paired(a, b)
    assert result["direction"] == "A>B"
    assert result["p_value"] < 0.05


def test_cliffs_delta():
    from evaluation.stats.hypothesis_tests import cliffs_delta
    a = [10.0, 11.0, 12.0, 13.0, 14.0]
    b = [1.0, 2.0, 3.0, 4.0, 5.0]
    result = cliffs_delta(a, b)
    assert result["delta"] == 1.0
    assert result["interpretation"] == "large"


def test_bonferroni():
    from evaluation.stats.hypothesis_tests import bonferroni_correction
    p_values = [0.01, 0.03, 0.05]
    corrected = bonferroni_correction(p_values)
    assert corrected == pytest.approx([0.03, 0.09, 0.15])


def test_coefficient_of_variation():
    from evaluation.stats.hypothesis_tests import coefficient_of_variation
    cv = coefficient_of_variation([10.0, 10.0, 10.0])
    assert cv == 0.0


# ── power analysis ────────────────────────────────────────────────

def test_posthoc_power():
    from evaluation.stats.power_analysis import posthoc_power
    # Large effect + large n → high power
    power = posthoc_power(n_pairs=50, effect_size_d=0.8)
    assert power > 0.9


def test_required_sample_size():
    from evaluation.stats.power_analysis import required_sample_size
    n = required_sample_size(target_delta=0.5, target_power=0.80)
    assert isinstance(n, int)
    assert n > 0


# ── sanitize ──────────────────────────────────────────────────────

def test_sanitize_strips_mcp_artifacts():
    from evaluation.sanitize import sanitize_for_judge
    text = 'Analysis\n"ax57_compact": "value"\nMore text\nAX57 reference'
    sanitized = sanitize_for_judge(text, "SCAFFOLD_FULL")
    assert "ax57_compact" not in sanitized
    assert "AX57" not in sanitized
    assert "Analysis" in sanitized


# ── condition runners (import only) ───────────────────────────────

def test_condition_runners_importable():
    from evaluation.conditions.base import ConditionOutput, ConditionRunner
    from evaluation.conditions.naked import NakedRunner
    from evaluation.conditions.scaffold_full import ScaffoldFullRunner
    from evaluation.conditions.scaffold_partial import ScaffoldPartialRunner
    from evaluation.conditions.vocabulary_only import VocabularyOnlyRunner
    assert ConditionOutput is not None
    assert ConditionRunner is not None


# ── report (import only) ──────────────────────────────────────────

def test_report_importable():
    from evaluation.report import generate_report, regenerate_report
    assert callable(generate_report)
    assert callable(regenerate_report)
