"""Tests for PyAgent.run_tests method."""

import os
from pathlib import Path
from unittest.mock import patch

import pytest

from py_agent.api.py_agent import PyAgent

FAKE_PYTEST_RESULT: dict = {
    "stdout": "collected 3 items\n",
    "stderr": "",
    "pytestReport": {"summary": {"passed": 3, "failed": 0}},
    "command": "python -m pytest /abs/path/test_foo.py",
}


@pytest.fixture()
def agent() -> PyAgent:
    return PyAgent()


# ---------------------------------------------------------------------------
# Helper: patch run_pytest inside the py_agent module
# ---------------------------------------------------------------------------
PATCH_TARGET = "py_agent.api.py_agent.run_pytest"


# ---------------------------------------------------------------------------
# 1. Happy path — str path + str project_path
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_returns_run_pytest_result(agent: PyAgent) -> None:
    """run_tests returns whatever run_pytest returns."""
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        result = await agent.run_tests("/abs/path/test_foo.py", "/my/project")
    assert result == FAKE_PYTEST_RESULT
    mock_run.assert_called_once()


# ---------------------------------------------------------------------------
# 2. test_file_path as Path object is accepted and stringified
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_accepts_path_object_for_test_file(agent: PyAgent) -> None:
    """Path objects for test_file_path are converted to an absolute str."""
    test_path = Path("/abs/path/test_bar.py")
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests(test_path, "/my/project")
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["test_file_path"] == str(os.path.abspath(test_path))


# ---------------------------------------------------------------------------
# 3. project_path as Path object is converted to str
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_accepts_path_object_for_project_path(agent: PyAgent) -> None:
    """Path objects for project_path are converted to str."""
    project = Path("/my/project")
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests("/abs/path/test_foo.py", project)
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["repository_root_path"] == str(project)


# ---------------------------------------------------------------------------
# 4. project_path=None → os.getcwd() used as repository_root_path
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_uses_cwd_when_project_path_is_none(agent: PyAgent) -> None:
    """When project_path is None, os.getcwd() is passed as repository_root_path."""
    fake_cwd = "/fake/cwd"
    with (
        patch("py_agent.api.py_agent.os.getcwd", return_value=fake_cwd),
        patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run,
    ):
        await agent.run_tests("/abs/path/test_foo.py", None)
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["repository_root_path"] == fake_cwd


# ---------------------------------------------------------------------------
# 5. project_path omitted (default None) → os.getcwd() used
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_default_project_path_uses_cwd(agent: PyAgent) -> None:
    """Omitting project_path defaults to None and uses os.getcwd()."""
    fake_cwd = "/another/cwd"
    with (
        patch("py_agent.api.py_agent.os.getcwd", return_value=fake_cwd),
        patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run,
    ):
        await agent.run_tests("/abs/path/test_foo.py")
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["repository_root_path"] == fake_cwd


# ---------------------------------------------------------------------------
# 6. Relative test_file_path is converted to absolute path
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_converts_relative_path_to_absolute(agent: PyAgent) -> None:
    """A relative test_file_path is resolved to an absolute path via abspath."""
    relative = "tests/test_something.py"
    expected_abs = os.path.abspath(relative)
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests(relative, "/my/project")
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["test_file_path"] == expected_abs
    assert os.path.isabs(call_kwargs.kwargs["test_file_path"])


# ---------------------------------------------------------------------------
# 7. Already-absolute test_file_path passes through unchanged
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_absolute_path_passes_through(agent: PyAgent) -> None:
    """An absolute test_file_path is passed to run_pytest unchanged."""
    abs_path = "/abs/path/test_foo.py"
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests(abs_path, "/my/project")
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["test_file_path"] == abs_path


# ---------------------------------------------------------------------------
# 8. Explicit str project_path is forwarded as-is
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_str_project_path_forwarded(agent: PyAgent) -> None:
    """A str project_path is forwarded verbatim as repository_root_path."""
    project = "/explicit/project/root"
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests("/abs/path/test_foo.py", project)
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["repository_root_path"] == project


# ---------------------------------------------------------------------------
# 9. run_pytest is called with correct keyword argument names
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_calls_run_pytest_with_correct_kwargs(agent: PyAgent) -> None:
    """run_pytest is invoked with test_file_path and repository_root_path kwargs."""
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests("/abs/test.py", "/proj")
    mock_run.assert_called_once_with(
        test_file_path=os.path.abspath("/abs/test.py"),
        repository_root_path="/proj",
    )


# ---------------------------------------------------------------------------
# 10. run_pytest exception propagates
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_propagates_run_pytest_exception(agent: PyAgent) -> None:
    """If run_pytest raises, the exception propagates from run_tests."""
    with patch(PATCH_TARGET, side_effect=RuntimeError("pytest not found")):
        with pytest.raises(RuntimeError, match="pytest not found"):
            await agent.run_tests("/abs/path/test_foo.py", "/proj")


# ---------------------------------------------------------------------------
# 11. PyAgent with custom config still works
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_run_tests_works_with_custom_config() -> None:
    """PyAgent instantiated with a custom config still delegates to run_pytest."""
    from py_agent.config import PyAgentConfig

    config = PyAgentConfig(api_key="test-key", concurrency=2)
    agent = PyAgent(config=config)
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        result = await agent.run_tests("/abs/path/test_foo.py", "/my/project")
    assert result == FAKE_PYTEST_RESULT
    mock_run.assert_called_once()


# ---------------------------------------------------------------------------
# 12. Parametrize: various str project_path values are forwarded correctly
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@pytest.mark.parametrize(
    "project_path",
    [
        "/home/user/project",
        "/tmp/build",
        "/srv/app",
    ],
)
async def test_run_tests_various_project_paths(agent: PyAgent, project_path: str) -> None:
    """Various str project_path values are passed through to run_pytest."""
    with patch(PATCH_TARGET, return_value=FAKE_PYTEST_RESULT) as mock_run:
        await agent.run_tests("/abs/path/test_foo.py", project_path)
    call_kwargs = mock_run.call_args
    assert call_kwargs.kwargs["repository_root_path"] == project_path
