"""Tests for PyAgent.generate_tests"""

import asyncio
import os
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from py_agent.api.py_agent import PyAgent, TestableCompleteResult
from py_agent.models.testable import Location, Position, Testable

# ── Helpers ──────────────────────────────────────────────────────────────────


def _make_location(line: int = 1, column: int = 0, index: int = 0) -> Location:
    return Location(line=line, column=column, index=index)


def _make_position() -> Position:
    return Position(start=_make_location(1, 0, 0), end=_make_location(2, 0, 10))


def _make_testable(
    name: str = "my_func",
    type_: str = "function",
    parent_name: str | None = None,
) -> Testable:
    return Testable(
        type=type_,  # type: ignore[arg-type]
        name=name,
        position=_make_position(),
        parent_name=parent_name,
    )


def _make_result(
    test_file_path: str = "/tmp/test_my_func.py",
    request_id: str = "req-123",
    cost_usd: float | None = 0.05,
) -> TestableCompleteResult:
    return TestableCompleteResult(
        test_file_path=test_file_path,
        request_id=request_id,
        cost_usd=cost_usd,
    )


def _build_agent_with_result(
    testable: Testable,
    result: TestableCompleteResult | None,
    file_path: str = "/some/file.py",
) -> tuple[PyAgent, str]:
    """Return (agent, key) with _test_results pre-seeded and _enqueue_generation mocked."""
    agent = PyAgent()
    abs_path = str(os.path.abspath(file_path))
    key = PyAgent._get_queue_item_key(
        __import__(
            "py_agent.api.py_agent", fromlist=["GenerateTestsQueueData"]
        ).GenerateTestsQueueData(
            file_path=abs_path,
            method_type=testable.type,
            method_name=testable.name,
            parent_name=testable.parent_name or "",
        )
    )
    agent._test_results[key] = result

    # Mock _enqueue_generation to be a no-op that just seeds the result
    agent._enqueue_generation = MagicMock(return_value=None)  # type: ignore[method-assign]
    return agent, key


# ── Tests ─────────────────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_returns_dict_with_expected_keys() -> None:
    """Happy path: result dict has exactly test_file_path, request_id, cost_usd."""
    testable = _make_testable()
    agent, _ = _build_agent_with_result(testable, _make_result())

    out = await agent.generate_tests("/some/file.py", testable)

    assert set(out.keys()) == {"test_file_path", "request_id", "cost_usd"}


@pytest.mark.asyncio
async def test_returns_correct_values_from_result() -> None:
    """Return dict values mirror the TestableCompleteResult fields."""
    result = _make_result(
        test_file_path="/tests/test_foo.py",
        request_id="req-abc",
        cost_usd=0.12,
    )
    testable = _make_testable()
    agent, _ = _build_agent_with_result(testable, result)

    out = await agent.generate_tests("/some/file.py", testable)

    assert out["test_file_path"] == "/tests/test_foo.py"
    assert out["request_id"] == "req-abc"
    assert out["cost_usd"] == 0.12


@pytest.mark.asyncio
async def test_cost_usd_none_passes_through() -> None:
    """cost_usd=None is forwarded as-is in the return dict."""
    result = _make_result(cost_usd=None)
    testable = _make_testable()
    agent, _ = _build_agent_with_result(testable, result)

    out = await agent.generate_tests("/some/file.py", testable)

    assert out["cost_usd"] is None


@pytest.mark.asyncio
async def test_raises_runtime_error_when_key_missing() -> None:
    """RuntimeError raised when _test_results has no entry for the key."""
    testable = _make_testable(name="missing_func")
    agent = PyAgent()
    agent._enqueue_generation = MagicMock(return_value=None)  # type: ignore[method-assign]

    with pytest.raises(RuntimeError, match="missing_func"):
        await agent.generate_tests("/some/file.py", testable)


@pytest.mark.asyncio
async def test_raises_runtime_error_when_result_is_none() -> None:
    """RuntimeError raised when _test_results entry is explicitly None."""
    testable = _make_testable(name="broken_func")
    agent, _ = _build_agent_with_result(testable, result=None)

    with pytest.raises(RuntimeError, match="broken_func"):
        await agent.generate_tests("/some/file.py", testable)


@pytest.mark.asyncio
async def test_error_message_contains_testable_name() -> None:
    """RuntimeError message includes the testable name for diagnostics."""
    testable = _make_testable(name="unique_name_xyz")
    agent = PyAgent()
    agent._enqueue_generation = MagicMock(return_value=None)  # type: ignore[method-assign]

    with pytest.raises(RuntimeError) as exc_info:
        await agent.generate_tests("/some/file.py", testable)

    assert "unique_name_xyz" in str(exc_info.value)


@pytest.mark.asyncio
async def test_result_is_popped_from_test_results() -> None:
    """After a successful call the key is removed from _test_results."""
    testable = _make_testable()
    agent, key = _build_agent_with_result(testable, _make_result())

    await agent.generate_tests("/some/file.py", testable)

    assert key not in agent._test_results


@pytest.mark.asyncio
async def test_awaits_task_when_enqueue_returns_coroutine() -> None:
    """When _enqueue_generation returns an awaitable Task, it is awaited."""
    testable = _make_testable()
    agent = PyAgent()

    awaited = []

    async def _fake_coro() -> None:
        awaited.append(True)

    abs_path = str(os.path.abspath("/some/file.py"))
    from py_agent.api.py_agent import GenerateTestsQueueData

    key = PyAgent._get_queue_item_key(
        GenerateTestsQueueData(
            file_path=abs_path,
            method_type=testable.type,
            method_name=testable.name,
            parent_name=testable.parent_name or "",
        )
    )
    # Pre-seed result so it doesn't raise
    agent._test_results[key] = _make_result()

    task = asyncio.ensure_future(_fake_coro())
    agent._enqueue_generation = MagicMock(return_value=task)  # type: ignore[method-assign]

    await agent.generate_tests("/some/file.py", testable)

    assert awaited == [True]


@pytest.mark.asyncio
async def test_file_path_as_path_object_succeeds() -> None:
    """file_path accepted as a pathlib.Path (not just str)."""
    testable = _make_testable()
    agent, _ = _build_agent_with_result(testable, _make_result(), file_path="/some/file.py")

    out = await agent.generate_tests(Path("/some/file.py"), testable)

    assert "test_file_path" in out


@pytest.mark.asyncio
async def test_parent_name_none_uses_empty_string_in_key() -> None:
    """Testable with parent_name=None builds key using '' not 'None'."""
    testable = _make_testable(name="func", parent_name=None)
    agent, key = _build_agent_with_result(testable, _make_result())

    # Key must not contain the string "None"
    assert "None" not in key
    await agent.generate_tests("/some/file.py", testable)  # must not raise


@pytest.mark.asyncio
async def test_enqueue_generation_called_with_correct_args() -> None:
    """_enqueue_generation is called with the original arguments forwarded."""
    testable = _make_testable(name="check_args")
    agent, _ = _build_agent_with_result(testable, _make_result(), file_path="/src/module.py")
    mock_enqueue = agent._enqueue_generation  # already a MagicMock

    await agent.generate_tests(
        "/src/module.py",
        testable,
        project_path="/project",
        user_prompt="write thorough tests",
    )

    mock_enqueue.assert_called_once_with(
        file_path="/src/module.py",
        testable=testable,
        project_path="/project",
        user_prompt="write thorough tests",
    )


@pytest.mark.asyncio
async def test_method_testable_type_included_in_key() -> None:
    """Key is type-sensitive: 'method' and 'function' testables generate different keys."""
    abs_path = str(os.path.abspath("/some/file.py"))
    from py_agent.api.py_agent import GenerateTestsQueueData

    func_key = PyAgent._get_queue_item_key(
        GenerateTestsQueueData(
            file_path=abs_path, method_type="function", method_name="do_it", parent_name=""
        )
    )
    method_key = PyAgent._get_queue_item_key(
        GenerateTestsQueueData(
            file_path=abs_path, method_type="method", method_name="do_it", parent_name=""
        )
    )

    assert func_key != method_key
