"""Tests for evals doc_sync: changelog consistency and case inventory rendering."""

from __future__ import annotations

import pytest

import evals.doc_sync as doc_sync
from evals.doc_sync import (
    BEGIN_CASE_INVENTORY_MARKER,
    END_CASE_INVENTORY_MARKER,
    CategorySummary,
    InventorySummary,
    build_inventory_summary,
    render_case_inventory_block,
    replace_case_inventory_block,
    validate_changelog_consistency,
)
from llmdebug import _json as json


def _write_case(
    cases_dir,
    *,
    case_id: str,
    category: str,
    difficulty: str,
    snapshot_dependency: str,
    bug_source: str | None = None,
) -> None:
    case_dir = cases_dir / case_id
    case_dir.mkdir(parents=True, exist_ok=True)
    metadata = {
        "description": f"Synthetic case: {case_id}",
        "category": category,
        "difficulty": difficulty,
        "snapshot_dependency": snapshot_dependency,
        "expected_exception": "AssertionError",
        "tags": ["synthetic"],
        "python_args": ["-m", "pytest", "-q"],
        "timeout_sec": 30,
    }
    if bug_source is not None:
        metadata["bug_source"] = bug_source
    (case_dir / "case.json").write_text(json.dumps(metadata), encoding="utf-8")


def test_build_inventory_summary_from_synthetic_cases(tmp_path) -> None:
    _write_case(
        tmp_path,
        case_id="case_a",
        category="logic_error",
        difficulty="easy",
        snapshot_dependency="required",
        bug_source=None,
    )
    _write_case(
        tmp_path,
        case_id="case_b",
        category="logic_error",
        difficulty="hard",
        snapshot_dependency="helpful",
        bug_source="debugbench",
    )
    _write_case(
        tmp_path,
        case_id="case_c",
        category="hidden_state",
        difficulty="medium",
        snapshot_dependency="required",
        bug_source="condefects",
    )

    summary = build_inventory_summary(tmp_path)
    assert summary.total_cases == 3
    assert summary.source_counts == (("condefects", 1), ("debugbench", 1), ("hand_crafted", 1))
    assert summary.categories[0].name == "logic_error"
    assert summary.categories[0].count == 2
    assert summary.categories[0].difficulty_counts == (("easy", 1), ("hard", 1))
    assert summary.categories[1].name == "hidden_state"
    assert summary.categories[1].difficulty_counts == (("medium", 1),)


def test_render_case_inventory_block_is_deterministic() -> None:
    summary = InventorySummary(
        total_cases=2,
        source_counts=(("hand_crafted", 1), ("debugbench", 1)),
        categories=(
            CategorySummary(name="logic_error", count=1, difficulty_counts=(("easy", 1),)),
            CategorySummary(name="hidden_state", count=1, difficulty_counts=(("hard", 1),)),
        ),
    )

    rendered = render_case_inventory_block(summary)
    expected = "\n".join(
        [
            BEGIN_CASE_INVENTORY_MARKER,
            "_This section is auto-generated from `evals/cases` by "
            "`python -m evals.doc_sync --write`._",
            "",
            "2 validated cases across 2 categories and 2 sources:",
            "",
            "| Source | Cases |",
            "|--------|-------|",
            "| Hand-crafted | 1 |",
            "| DebugBench | 1 |",
            "| **Total** | **2** |",
            "",
            "Category distribution:",
            "",
            "| Category | Count | Difficulty breakdown |",
            "|----------|-------|---------------------|",
            "| logic_error | 1 | 1 easy |",
            "| hidden_state | 1 | 1 hard |",
            END_CASE_INVENTORY_MARKER,
        ]
    )
    assert rendered == expected


def test_replace_case_inventory_block_updates_marked_content() -> None:
    readme_text = (
        f"prefix\n{BEGIN_CASE_INVENTORY_MARKER}\nold-content\n{END_CASE_INVENTORY_MARKER}\nsuffix\n"
    )
    replacement = f"{BEGIN_CASE_INVENTORY_MARKER}\nnew-content\n{END_CASE_INVENTORY_MARKER}"
    updated = replace_case_inventory_block(readme_text, replacement)
    assert "old-content" not in updated
    assert "new-content" in updated
    assert updated.startswith("prefix\n")
    assert updated.endswith("suffix\n")


def test_replace_case_inventory_block_requires_markers() -> None:
    with pytest.raises(ValueError, match="Case inventory markers were not found"):
        replace_case_inventory_block("missing markers", "replacement")


def test_validate_changelog_consistency_reports_missing_tag_and_current_section() -> None:
    changelog = "# CHANGELOG\n\n## v1.0.0\n\n- Initial release\n"
    errors = validate_changelog_consistency(
        changelog,
        tagged_versions=["1.0.0", "1.1.0"],
        project_version="1.2.0",
    )
    assert any("v1.1.0" in error for error in errors)
    assert any("Unreleased" in error or "Current" in error for error in errors)


def test_validate_changelog_consistency_passes_when_current_section_is_present() -> None:
    changelog = (
        "# CHANGELOG\n\n"
        "## Current (1.2.0)\n\n"
        "- Pending release notes.\n\n"
        "## v1.1.0\n\n"
        "- Patch release.\n\n"
        "## v1.0.0\n\n"
        "- Initial release.\n"
    )
    errors = validate_changelog_consistency(
        changelog,
        tagged_versions=["1.0.0", "1.1.0"],
        project_version="1.2.0",
    )
    assert errors == []


def test_load_project_version_uses_regex_fallback_without_tomllib(tmp_path, monkeypatch) -> None:
    pyproject_path = tmp_path / "pyproject.toml"
    pyproject_path.write_text(
        "\n".join(
            [
                "[build-system]",
                'requires = ["hatchling"]',
                "",
                "[project]",
                'name = "llmdebug"',
                'version = "9.8.7"',
            ]
        ),
        encoding="utf-8",
    )

    monkeypatch.setattr(doc_sync, "tomllib", None)

    version = doc_sync.load_project_version(pyproject_path)
    assert version == "9.8.7"


def test_run_checks_fails_when_case_inventory_block_is_stale(tmp_path, monkeypatch) -> None:
    cases_dir = tmp_path / "cases"
    _write_case(
        cases_dir,
        case_id="case_a",
        category="logic_error",
        difficulty="easy",
        snapshot_dependency="required",
        bug_source=None,
    )

    readme_path = tmp_path / "README.md"
    readme_path.write_text(
        "\n".join(
            [
                "# evals",
                BEGIN_CASE_INVENTORY_MARKER,
                "stale block",
                END_CASE_INVENTORY_MARKER,
                "",
            ]
        ),
        encoding="utf-8",
    )

    changelog_path = tmp_path / "CHANGELOG.md"
    changelog_path.write_text(
        "# CHANGELOG\n\n## Current (1.2.0)\n\n- Pending.\n\n## v1.1.0\n\n- Notes.\n",
        encoding="utf-8",
    )

    pyproject_path = tmp_path / "pyproject.toml"
    pyproject_path.write_text(
        '[project]\nname = "llmdebug"\nversion = "1.2.0"\n',
        encoding="utf-8",
    )

    monkeypatch.setattr(doc_sync, "CASES_DIR", cases_dir)
    monkeypatch.setattr(doc_sync, "EVALS_README_PATH", readme_path)
    monkeypatch.setattr(doc_sync, "CHANGELOG_PATH", changelog_path)
    monkeypatch.setattr(doc_sync, "PYPROJECT_PATH", pyproject_path)
    monkeypatch.setattr(doc_sync, "get_git_tag_versions", lambda repo_root=tmp_path: ["1.1.0"])

    errors = doc_sync.run_checks()
    assert any("out of date" in error for error in errors)
