"""Tests for PyAgent.bulk_generate_tests."""

from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from py_agent.api.py_agent import PyAgent

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_item(
    file_path: str = "src/foo.py",
    testable: Any = None,
    *,
    project_path: str | None = None,
) -> dict[str, Any]:
    item: dict[str, Any] = {"file_path": file_path, "testable": testable or {}}
    if project_path is not None:
        item["project_path"] = project_path
    return item


def _make_agent_with_mocks():
    """Return a PyAgent whose internal helpers are fully mocked."""
    agent = PyAgent()

    # Mock queue
    mock_queue = MagicMock()
    mock_queue.get_idle_promise = AsyncMock(return_value=None)

    agent._get_queue = MagicMock(return_value=mock_queue)  # type: ignore[method-assign]
    agent._enqueue_generation = MagicMock()  # type: ignore[method-assign]

    return agent, mock_queue


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_empty_list_calls_idle_promise():
    """When testables_to_generate is empty, get_idle_promise is still awaited."""
    agent, mock_queue = _make_agent_with_mocks()

    await agent.bulk_generate_tests([])

    mock_queue.get_idle_promise.assert_awaited_once()
    agent._enqueue_generation.assert_not_called()


@pytest.mark.asyncio
async def test_single_item_enqueued_with_correct_progress():
    """A single item is enqueued with index=1, total=1."""
    agent, mock_queue = _make_agent_with_mocks()
    item = _make_item("src/bar.py")

    await agent.bulk_generate_tests([item])

    agent._enqueue_generation.assert_called_once_with(
        file_path="src/bar.py",
        testable=item["testable"],
        project_path=None,
        user_prompt="",
        progress={"index": 1, "total": 1},
    )


@pytest.mark.asyncio
async def test_multiple_items_enqueued_with_correct_indices():
    """Three items get index 1, 2, 3 with total=3."""
    agent, mock_queue = _make_agent_with_mocks()
    items = [_make_item(f"src/f{i}.py") for i in range(3)]

    await agent.bulk_generate_tests(items)

    assert agent._enqueue_generation.call_count == 3
    calls = agent._enqueue_generation.call_args_list
    for i, c in enumerate(calls):
        assert c.kwargs["progress"] == {"index": i + 1, "total": 3}


@pytest.mark.asyncio
async def test_on_testable_start_registers_on_start():
    """Providing on_testable_start causes queue.on_start to be called."""
    agent, mock_queue = _make_agent_with_mocks()
    start_cb = MagicMock()

    with patch("py_agent.api.py_agent._on_start") as mock_on_start_fn:
        await agent.bulk_generate_tests([_make_item()], on_testable_start=start_cb)
        mock_queue.on_start.assert_called_once_with(mock_on_start_fn)


@pytest.mark.asyncio
async def test_on_testable_start_none_does_not_register():
    """Omitting on_testable_start means queue.on_start is never called."""
    agent, mock_queue = _make_agent_with_mocks()

    await agent.bulk_generate_tests([_make_item()], on_testable_start=None)

    mock_queue.on_start.assert_not_called()


@pytest.mark.asyncio
async def test_on_testable_complete_registers_on_done():
    """Providing on_testable_complete causes queue.on_done to be called."""
    agent, mock_queue = _make_agent_with_mocks()
    complete_cb = MagicMock()

    with patch("py_agent.api.py_agent._on_done") as mock_on_done_fn:
        await agent.bulk_generate_tests([_make_item()], on_testable_complete=complete_cb)
        mock_queue.on_done.assert_called_once_with(mock_on_done_fn)


@pytest.mark.asyncio
async def test_on_testable_complete_none_does_not_register():
    """Omitting on_testable_complete means queue.on_done is never called."""
    agent, mock_queue = _make_agent_with_mocks()

    await agent.bulk_generate_tests([_make_item()], on_testable_complete=None)

    mock_queue.on_done.assert_not_called()


@pytest.mark.asyncio
async def test_both_callbacks_register_both_handlers():
    """When both callbacks are provided, both on_start and on_done are registered."""
    agent, mock_queue = _make_agent_with_mocks()
    start_cb = MagicMock()
    complete_cb = MagicMock()

    with (
        patch("py_agent.api.py_agent._on_start"),
        patch("py_agent.api.py_agent._on_done"),
    ):
        await agent.bulk_generate_tests(
            [_make_item()],
            on_testable_start=start_cb,
            on_testable_complete=complete_cb,
        )

    mock_queue.on_start.assert_called_once()
    mock_queue.on_done.assert_called_once()


@pytest.mark.asyncio
async def test_user_prompt_forwarded_to_every_item():
    """The user_prompt kwarg is passed unchanged to every _enqueue_generation call."""
    agent, mock_queue = _make_agent_with_mocks()
    items = [_make_item(f"src/x{i}.py") for i in range(3)]

    await agent.bulk_generate_tests(items, user_prompt="write good tests")

    for c in agent._enqueue_generation.call_args_list:
        assert c.kwargs["user_prompt"] == "write good tests"


@pytest.mark.asyncio
async def test_project_path_forwarded_when_present():
    """Items that include project_path have it forwarded to _enqueue_generation."""
    agent, mock_queue = _make_agent_with_mocks()
    item = _make_item("src/foo.py", project_path="/repo/myproject")

    await agent.bulk_generate_tests([item])

    agent._enqueue_generation.assert_called_once()
    assert agent._enqueue_generation.call_args.kwargs["project_path"] == "/repo/myproject"


@pytest.mark.asyncio
async def test_project_path_none_when_absent():
    """Items without project_path pass None to _enqueue_generation."""
    agent, mock_queue = _make_agent_with_mocks()
    item = _make_item("src/foo.py")  # no project_path key

    await agent.bulk_generate_tests([item])

    agent._enqueue_generation.assert_called_once()
    assert agent._enqueue_generation.call_args.kwargs["project_path"] is None


@pytest.mark.asyncio
async def test_get_queue_called_once_per_invocation():
    """_get_queue is called exactly once regardless of list length."""
    agent, mock_queue = _make_agent_with_mocks()
    items = [_make_item(f"src/f{i}.py") for i in range(5)]

    await agent.bulk_generate_tests(items)

    agent._get_queue.assert_called_once()
