"""Tests for evals.importers.base — shared importer utilities.

Uses synthetic data only (no HuggingFace downloads).
"""

from __future__ import annotations

from evals.importers import debugbench_importer, fixup_expected_exceptions, pyresbugs_importer
from evals.importers.base import (
    count_patch_lines,
    generate_oracle_patch,
    has_external_deps,
    validate_generated_case,
    write_case_files,
)
from llmdebug import _json as json

# ---------------------------------------------------------------------------
# generate_oracle_patch
# ---------------------------------------------------------------------------


class TestGenerateOraclePatch:
    def test_simple_change(self) -> None:
        buggy = "def f():\n    return 1\n"
        fixed = "def f():\n    return 2\n"
        patch = generate_oracle_patch(buggy, fixed)
        assert "diff --git" in patch
        assert "--- a/buggy.py" in patch
        assert "+++ b/buggy.py" in patch
        assert "-    return 1" in patch
        assert "+    return 2" in patch

    def test_custom_filename(self) -> None:
        buggy = "x = 1\n"
        fixed = "x = 2\n"
        patch = generate_oracle_patch(buggy, fixed, filename="module.py")
        assert "a/module.py" in patch
        assert "b/module.py" in patch

    def test_no_diff_returns_empty(self) -> None:
        code = "def f():\n    return 1\n"
        patch = generate_oracle_patch(code, code)
        assert patch == ""

    def test_multiline_change(self) -> None:
        buggy = "a = 1\nb = 2\nc = 3\n"
        fixed = "a = 1\nb = 20\nc = 3\n"
        patch = generate_oracle_patch(buggy, fixed)
        assert "-b = 2" in patch
        assert "+b = 20" in patch

    def test_addition_only(self) -> None:
        buggy = "a = 1\n"
        fixed = "a = 1\nb = 2\n"
        patch = generate_oracle_patch(buggy, fixed)
        assert "+b = 2" in patch


# ---------------------------------------------------------------------------
# write_case_files
# ---------------------------------------------------------------------------


class TestWriteCaseFiles:
    def test_creates_all_files(self, tmp_path) -> None:
        case_dir = tmp_path / "test_case"
        write_case_files(
            case_dir,
            buggy_code="def f():\n    return 1\n",
            test_code="def test_f():\n    assert True\n",
            oracle_patch="--- a/buggy.py\n+++ b/buggy.py\n",
            metadata={"category": "logic_error", "difficulty": "easy"},
        )

        assert (case_dir / "buggy.py").exists()
        assert (case_dir / "test_buggy.py").exists()
        assert (case_dir / "oracle.patch").exists()
        assert (case_dir / "case.json").exists()

        meta = json.loads((case_dir / "case.json").read_text())
        assert meta["category"] == "logic_error"
        assert meta["difficulty"] == "easy"

    def test_creates_nested_directories(self, tmp_path) -> None:
        case_dir = tmp_path / "nested" / "deep" / "case"
        write_case_files(
            case_dir,
            buggy_code="x = 1\n",
            test_code="assert True\n",
            oracle_patch="patch\n",
            metadata={},
        )
        assert case_dir.exists()

    def test_file_contents_match(self, tmp_path) -> None:
        case_dir = tmp_path / "case"
        write_case_files(
            case_dir,
            buggy_code="CODE",
            test_code="TEST",
            oracle_patch="PATCH",
            metadata={"key": "value"},
        )
        assert (case_dir / "buggy.py").read_text() == "CODE"
        assert (case_dir / "test_buggy.py").read_text() == "TEST"
        assert (case_dir / "oracle.patch").read_text() == "PATCH"


# ---------------------------------------------------------------------------
# validate_generated_case
# ---------------------------------------------------------------------------


class TestValidateGeneratedCase:
    def test_valid_case_passes(self, tmp_path) -> None:
        case_dir = tmp_path / "valid"
        write_case_files(
            case_dir,
            buggy_code="def f():\n    return 1\n",
            test_code="def test_f():\n    assert True\n",
            oracle_patch="diff --git a/buggy.py b/buggy.py\n-    return 1\n+    return 2\n",
            metadata={
                "category": "logic_error",
                "difficulty": "easy",
                "snapshot_dependency": "helpful",
                "expected_exception": "AssertionError",
            },
        )
        ok, msg = validate_generated_case(case_dir, schema_only=True)
        assert ok, msg

    def test_missing_file_fails(self, tmp_path) -> None:
        case_dir = tmp_path / "bad"
        case_dir.mkdir()
        (case_dir / "buggy.py").write_text("x = 1\n")
        (case_dir / "case.json").write_text("{}")
        # Missing test_buggy.py and oracle.patch
        ok, msg = validate_generated_case(case_dir, schema_only=True)
        assert not ok
        assert "missing required file" in msg

    def test_invalid_category_fails(self, tmp_path) -> None:
        case_dir = tmp_path / "bad_cat"
        write_case_files(
            case_dir,
            buggy_code="x = 1\n",
            test_code="assert True\n",
            oracle_patch="patch content\n",
            metadata={
                "category": "nonexistent_category",
                "difficulty": "easy",
                "snapshot_dependency": "helpful",
                "expected_exception": "AssertionError",
            },
        )
        ok, msg = validate_generated_case(case_dir, schema_only=True)
        assert not ok
        assert "invalid category" in msg

    def test_empty_patch_fails(self, tmp_path) -> None:
        case_dir = tmp_path / "empty_patch"
        write_case_files(
            case_dir,
            buggy_code="x = 1\n",
            test_code="assert True\n",
            oracle_patch="",
            metadata={
                "category": "logic_error",
                "difficulty": "easy",
                "snapshot_dependency": "helpful",
                "expected_exception": "AssertionError",
            },
        )
        ok, msg = validate_generated_case(case_dir, schema_only=True)
        assert not ok
        assert "empty" in msg


# ---------------------------------------------------------------------------
# count_patch_lines
# ---------------------------------------------------------------------------


class TestCountPatchLines:
    def test_simple_replacement(self) -> None:
        patch = (
            "--- a/buggy.py\n"
            "+++ b/buggy.py\n"
            "@@ -1,3 +1,3 @@\n"
            " def f():\n"
            "-    return 1\n"
            "+    return 2\n"
        )
        assert count_patch_lines(patch) == 2  # 1 addition + 1 deletion

    def test_addition_only(self) -> None:
        patch = "+new_line\n+another_line\n"
        assert count_patch_lines(patch) == 2

    def test_excludes_header_lines(self) -> None:
        patch = "--- a/file.py\n+++ b/file.py\n-old\n+new\n"
        assert count_patch_lines(patch) == 2  # Only -old and +new

    def test_empty_patch(self) -> None:
        assert count_patch_lines("") == 0


class TestHasExternalDeps:
    def test_stdlib_only_imports_are_allowed(self) -> None:
        code = "import math\nfrom collections import deque\n"
        assert has_external_deps(code) is False

    def test_multiple_imports_detect_external(self) -> None:
        code = "import math, requests\n"
        assert has_external_deps(code) is True

    def test_from_import_detects_external(self) -> None:
        code = "from pydantic import BaseModel\n"
        assert has_external_deps(code) is True

    def test_syntax_error_fallback_still_detects_imports(self) -> None:
        code = "import requests\nx = (\n"
        assert has_external_deps(code) is True


class TestDebugBenchFallbackTestGeneration:
    def test_reference_error_fallback_uses_zero_arg_method(self) -> None:
        fixed_code = "class Solution:\n    def solve(self):\n        return 1\n"
        generated = debugbench_importer._generate_test(
            fixed_code, examples=None, category="reference error"
        )
        assert generated is not None
        assert "sol.solve()" in generated
        assert "except TypeError" not in generated

    def test_reference_error_fallback_skips_required_args(self) -> None:
        fixed_code = "class Solution:\n    def solve(self, nums):\n        return nums\n"
        generated = debugbench_importer._generate_test(
            fixed_code, examples=None, category="reference error"
        )
        assert generated is None


class TestPyResBugsPatchVerification:
    def test_verify_patch_fails_closed_on_patch_tool_error(self, tmp_path, monkeypatch) -> None:
        case_dir = tmp_path / "case"
        case_dir.mkdir()
        (case_dir / "oracle.patch").write_text(
            "diff --git a/buggy.py b/buggy.py\n", encoding="utf-8"
        )

        def _raise(*args, **kwargs):
            raise FileNotFoundError

        monkeypatch.setattr(pyresbugs_importer.subprocess, "run", _raise)
        assert pyresbugs_importer._verify_patch(case_dir) is False


class TestFixupExpectedExceptions:
    def test_delete_skipped_cases_is_opt_in_helper(self, tmp_path) -> None:
        cases_dir = tmp_path / "cases"
        skip_case = cases_dir / "case_skip"
        keep_case = cases_dir / "case_keep"
        skip_case.mkdir(parents=True)
        keep_case.mkdir(parents=True)

        deleted = fixup_expected_exceptions._delete_skipped_cases(
            cases_dir,
            {
                "case_skip": "skip: no exception found",
                "case_keep": "fixed: AssertionError -> NameError",
            },
        )

        assert deleted == 1
        assert not skip_case.exists()
        assert keep_case.exists()
