"""Tests for agentmd eval command (D1-D4)."""

from __future__ import annotations

import json
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from agentmd.cli import app
from agentmd.eval import (
    _coderace_available,
    _get_existing_context,
    _run_coderace,
    _run_generate,
    run_eval,
)

runner = CliRunner()

# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

def make_project(tmp_path: Path) -> Path:
    """Create a minimal project structure for testing."""
    (tmp_path / "main.py").write_text("print('hello')\n")
    (tmp_path / "README.md").write_text("# Test project\n")
    return tmp_path


CODERACE_JSON_OUTPUT = json.dumps({
    "with_context": 84,
    "without_context": 67,
    "n_with": 3,
    "n_without": 3,
})

CODERACE_ALT_SHAPE = json.dumps({
    "with": {"avg": 80, "n": 5},
    "without": {"avg": 65, "n": 5},
})


# ---------------------------------------------------------------------------
# _coderace_available
# ---------------------------------------------------------------------------

def test_coderace_available_true():
    with patch("shutil.which", return_value="/usr/local/bin/coderace"):
        assert _coderace_available() is True


def test_coderace_available_false():
    with patch("shutil.which", return_value=None):
        assert _coderace_available() is False


# ---------------------------------------------------------------------------
# _run_generate
# ---------------------------------------------------------------------------

def test_run_generate_creates_file(tmp_path):
    make_project(tmp_path)
    info = _run_generate(tmp_path, force=True)
    assert info["file"] == "CLAUDE.md"
    assert (tmp_path / "CLAUDE.md").exists()
    assert info["line_count"] > 0
    assert "path" in info


def test_run_generate_returns_metadata(tmp_path):
    make_project(tmp_path)
    info = _run_generate(tmp_path)
    assert isinstance(info["line_count"], int)
    assert info["line_count"] > 0


# ---------------------------------------------------------------------------
# _get_existing_context
# ---------------------------------------------------------------------------

def test_get_existing_context_present(tmp_path):
    (tmp_path / "CLAUDE.md").write_text("# context\nline2\nline3\n")
    info = _get_existing_context(tmp_path)
    assert info is not None
    assert info["file"] == "CLAUDE.md"
    assert info["line_count"] == 3


def test_get_existing_context_absent(tmp_path):
    assert _get_existing_context(tmp_path) is None


# ---------------------------------------------------------------------------
# _run_coderace
# ---------------------------------------------------------------------------

def test_run_coderace_not_installed():
    with patch("subprocess.run", side_effect=FileNotFoundError):
        result = _run_coderace("/some/CLAUDE.md")
    assert result is None


def test_run_coderace_timeout():
    with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="coderace", timeout=600)):
        result = _run_coderace("/some/CLAUDE.md")
    assert result is not None
    assert result["error"] == "timeout"


def test_run_coderace_nonzero_exit():
    mock = MagicMock()
    mock.returncode = 1
    mock.stderr = "something broke"
    with patch("subprocess.run", return_value=mock):
        result = _run_coderace("/some/CLAUDE.md")
    assert result["error"] == "nonzero"


def test_run_coderace_invalid_json():
    mock = MagicMock()
    mock.returncode = 0
    mock.stdout = "not json at all"
    with patch("subprocess.run", return_value=mock):
        result = _run_coderace("/some/CLAUDE.md")
    assert result["error"] == "parse"


def test_run_coderace_success_shape_a():
    mock = MagicMock()
    mock.returncode = 0
    mock.stdout = CODERACE_JSON_OUTPUT
    with patch("subprocess.run", return_value=mock):
        result = _run_coderace("/some/CLAUDE.md")
    assert result is not None
    assert result["with_context"] == 84
    assert result["without_context"] == 67
    assert result["delta"] == pytest.approx(17.0)
    assert result["n_with"] == 3


def test_run_coderace_success_shape_b():
    mock = MagicMock()
    mock.returncode = 0
    mock.stdout = CODERACE_ALT_SHAPE
    with patch("subprocess.run", return_value=mock):
        result = _run_coderace("/some/CLAUDE.md")
    assert result is not None
    assert result["with_context"] == 80
    assert result["without_context"] == 65
    assert result["delta"] == pytest.approx(15.0)
    assert result["n_with"] == 5


def test_run_coderace_unexpected_shape():
    mock = MagicMock()
    mock.returncode = 0
    mock.stdout = json.dumps({"foo": "bar"})
    with patch("subprocess.run", return_value=mock):
        result = _run_coderace("/some/CLAUDE.md")
    assert result["error"] == "unexpected_shape"


# ---------------------------------------------------------------------------
# CLI: eval --no-benchmark
# ---------------------------------------------------------------------------

def test_eval_no_benchmark_generates_file(tmp_path):
    make_project(tmp_path)
    result = runner.invoke(app, ["eval", "--no-benchmark", str(tmp_path)])
    assert result.exit_code == 0, result.output
    assert (tmp_path / "CLAUDE.md").exists()
    assert "Generated" in result.output or "Generating" in result.output


def test_eval_no_benchmark_shows_saved(tmp_path):
    make_project(tmp_path)
    result = runner.invoke(app, ["eval", "--no-benchmark", str(tmp_path)])
    assert result.exit_code == 0
    assert "Saved" in result.output


def test_eval_no_benchmark_no_coderace_mention(tmp_path):
    make_project(tmp_path)
    result = runner.invoke(app, ["eval", "--no-benchmark", str(tmp_path)])
    assert result.exit_code == 0
    # benchmark section should not appear
    assert "coderace" not in result.output.lower() or "no-benchmark" in result.output.lower()


# ---------------------------------------------------------------------------
# CLI: eval --json --no-benchmark
# ---------------------------------------------------------------------------

def test_eval_json_no_benchmark_valid_json(tmp_path):
    make_project(tmp_path)
    result = runner.invoke(app, ["eval", "--json", "--no-benchmark", str(tmp_path)])
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert "generated" in data
    assert data["eval"] is None
    assert "eval_missing" in data


def test_eval_json_no_benchmark_generated_keys(tmp_path):
    make_project(tmp_path)
    result = runner.invoke(app, ["eval", "--json", "--no-benchmark", str(tmp_path)])
    data = json.loads(result.output)
    gen = data["generated"]
    assert "file" in gen
    assert "line_count" in gen
    assert "path" in gen


# ---------------------------------------------------------------------------
# CLI: eval --json coderace not installed
# ---------------------------------------------------------------------------

def test_eval_json_coderace_not_installed(tmp_path):
    make_project(tmp_path)
    with patch("agentmd.eval._coderace_available", return_value=False):
        result = runner.invoke(app, ["eval", "--json", str(tmp_path)])
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert data["eval"] is None
    assert "coderace" in data.get("eval_missing", "").lower()


def test_eval_human_coderace_not_installed(tmp_path):
    make_project(tmp_path)
    with patch("agentmd.eval._coderace_available", return_value=False):
        result = runner.invoke(app, ["eval", str(tmp_path)])
    assert result.exit_code == 0
    assert "pip install coderace" in result.output


# ---------------------------------------------------------------------------
# CLI: eval --json coderace installed (mocked)
# ---------------------------------------------------------------------------

def test_eval_json_with_coderace_success(tmp_path):
    make_project(tmp_path)
    mock_cr = MagicMock()
    mock_cr.returncode = 0
    mock_cr.stdout = CODERACE_JSON_OUTPUT

    with patch("agentmd.eval._coderace_available", return_value=True), \
         patch("subprocess.run", return_value=mock_cr):
        result = runner.invoke(app, ["eval", "--json", str(tmp_path)])

    assert result.exit_code == 0
    data = json.loads(result.output)
    assert data["eval"]["with_context"] == 84
    assert data["eval"]["without_context"] == 67
    assert data["eval"]["delta"] == pytest.approx(17.0)


def test_eval_human_with_coderace_success(tmp_path):
    make_project(tmp_path)
    mock_cr = MagicMock()
    mock_cr.returncode = 0
    mock_cr.stdout = CODERACE_JSON_OUTPUT

    with patch("agentmd.eval._coderace_available", return_value=True), \
         patch("subprocess.run", return_value=mock_cr):
        result = runner.invoke(app, ["eval", str(tmp_path)])

    assert result.exit_code == 0
    assert "Context File Impact Report" in result.output
    assert "84" in result.output
    assert "67" in result.output


# ---------------------------------------------------------------------------
# CLI: eval --existing
# ---------------------------------------------------------------------------

def test_eval_existing_uses_current_file(tmp_path):
    (tmp_path / "CLAUDE.md").write_text("# existing context\n" * 5)
    with patch("agentmd.eval._coderace_available", return_value=False):
        result = runner.invoke(app, ["eval", "--existing", str(tmp_path)])
    assert result.exit_code == 0
    assert "existing" in result.output.lower() or "Saved" in result.output


def test_eval_existing_missing_file(tmp_path):
    result = runner.invoke(app, ["eval", "--existing", str(tmp_path)])
    assert result.exit_code != 0


def test_eval_existing_json(tmp_path):
    (tmp_path / "CLAUDE.md").write_text("# ctx\nline2\n")
    with patch("agentmd.eval._coderace_available", return_value=False):
        result = runner.invoke(app, ["eval", "--existing", "--json", str(tmp_path)])
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert data["generated"]["line_count"] == 2
    assert data["eval"] is None


# ---------------------------------------------------------------------------
# CLI: error path — invalid directory
# ---------------------------------------------------------------------------

def test_eval_invalid_path():
    result = runner.invoke(app, ["eval", "/nonexistent/path/xyz"])
    assert result.exit_code != 0


# ---------------------------------------------------------------------------
# CLI: help text
# ---------------------------------------------------------------------------

def test_eval_help():
    result = runner.invoke(app, ["eval", "--help"])
    assert result.exit_code == 0
    assert "eval" in result.output.lower() or "context" in result.output.lower()
