"""Tests for gfp_mcp.utils module.

These tests verify the utility functions in the new gfp_mcp package.
"""

from __future__ import annotations

import asyncio
import time
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from gfp_mcp.utils import (
    CellBuildStatus,
    poll_cell_build_status,
    wait_for_build_completion,
    wait_for_build_completion_via_api,
    wait_for_gds_file,
)


class TestWaitForGdsFile:
    """Tests for wait_for_gds_file function."""

    @pytest.mark.anyio
    async def test_file_exists_immediately(self, tmp_path: Path) -> None:
        """Test waiting for a file that already exists."""
        gds_file = tmp_path / "test.gds"
        gds_file.write_bytes(b"test content")
        time.sleep(0.2)

        result = await wait_for_gds_file(gds_file, timeout=5.0, min_age=0.1)

        assert result is True

    @pytest.mark.anyio
    async def test_file_does_not_exist_timeout(self, tmp_path: Path) -> None:
        """Test timeout when file never appears."""
        gds_file = tmp_path / "nonexistent.gds"

        result = await wait_for_gds_file(gds_file, timeout=0.5, poll_interval=0.1)

        assert result is False

    @pytest.mark.anyio
    async def test_file_appears_during_polling(self, tmp_path: Path) -> None:
        """Test file appearing while polling."""
        gds_file = tmp_path / "delayed.gds"

        async def create_file_later() -> None:
            await asyncio.sleep(0.3)
            gds_file.write_bytes(b"delayed content")

        asyncio.create_task(create_file_later())

        result = await wait_for_gds_file(
            gds_file, timeout=2.0, poll_interval=0.1, min_age=0.0
        )

        assert result is True
        assert gds_file.exists()

    @pytest.mark.anyio
    async def test_file_too_new(self, tmp_path: Path) -> None:
        """Test file exists but is too new (still being written)."""
        gds_file = tmp_path / "new.gds"
        gds_file.write_bytes(b"content")

        result = await wait_for_gds_file(
            gds_file, timeout=0.3, poll_interval=0.1, min_age=10.0
        )

        assert result is False

    @pytest.mark.anyio
    async def test_default_parameters(self, tmp_path: Path) -> None:
        """Test with default parameters."""
        gds_file = tmp_path / "default.gds"
        gds_file.write_bytes(b"content")
        time.sleep(0.2)

        result = await wait_for_gds_file(gds_file, timeout=1.0)

        assert result is True


class TestWaitForBuildCompletion:
    """Tests for wait_for_build_completion function."""

    @pytest.mark.anyio
    async def test_empty_cell_names(self) -> None:
        """Test with empty cell list returns empty dict."""
        result = await wait_for_build_completion([])
        assert result == {}

    @pytest.mark.anyio
    async def test_no_servers_in_registry(self) -> None:
        """Test returns empty dict when no servers found."""
        mock_registry = MagicMock()
        mock_registry.get_server_by_project.return_value = None
        mock_registry.list_servers.return_value = []

        with patch("gfp_mcp.registry.ServerRegistry", return_value=mock_registry):
            result = await wait_for_build_completion(["cell1"])

        assert result == {}

    @pytest.mark.anyio
    async def test_all_cells_ready(self, tmp_path: Path) -> None:
        """Test all cells found within timeout."""
        gds_dir = tmp_path / "build" / "gds"
        gds_dir.mkdir(parents=True)
        (gds_dir / "cell1.gds").write_bytes(b"gds1")
        (gds_dir / "cell2.gds").write_bytes(b"gds2")
        time.sleep(0.2)

        mock_server = MagicMock()
        mock_server.project_path = str(tmp_path)
        mock_registry = MagicMock()
        mock_registry.get_server_by_project.return_value = None
        mock_registry.list_servers.return_value = [mock_server]

        with patch("gfp_mcp.registry.ServerRegistry", return_value=mock_registry):
            result = await wait_for_build_completion(
                ["cell1", "cell2"], timeout=2.0, poll_interval=0.1
            )

        assert result == {"cell1": True, "cell2": True}

    @pytest.mark.anyio
    async def test_some_cells_timeout(self, tmp_path: Path) -> None:
        """Test some cells ready and some timed out."""
        gds_dir = tmp_path / "build" / "gds"
        gds_dir.mkdir(parents=True)
        (gds_dir / "cell1.gds").write_bytes(b"gds1")
        time.sleep(0.2)

        mock_server = MagicMock()
        mock_server.project_path = str(tmp_path)
        mock_registry = MagicMock()
        mock_registry.get_server_by_project.return_value = None
        mock_registry.list_servers.return_value = [mock_server]

        with patch("gfp_mcp.registry.ServerRegistry", return_value=mock_registry):
            result = await wait_for_build_completion(
                ["cell1", "missing_cell"], timeout=0.5, poll_interval=0.1
            )

        assert result["cell1"] is True
        assert result["missing_cell"] is False

    @pytest.mark.anyio
    async def test_project_lookup(self, tmp_path: Path) -> None:
        """Test that project name is used for registry lookup."""
        gds_dir = tmp_path / "build" / "gds"
        gds_dir.mkdir(parents=True)
        (gds_dir / "cell1.gds").write_bytes(b"gds1")
        time.sleep(0.2)

        mock_server = MagicMock()
        mock_server.project_path = str(tmp_path)
        mock_registry = MagicMock()
        mock_registry.get_server_by_project.return_value = mock_server

        with patch("gfp_mcp.registry.ServerRegistry", return_value=mock_registry):
            result = await wait_for_build_completion(
                ["cell1"], project="my_project", timeout=2.0, poll_interval=0.1
            )

        mock_registry.get_server_by_project.assert_called_once_with("my_project")
        assert result == {"cell1": True}


class TestPollCellBuildStatus:
    """Tests for poll_cell_build_status function."""

    @pytest.mark.anyio
    async def test_success_immediately(self) -> None:
        """Test cell that succeeds on first poll."""
        client = AsyncMock()
        client.request = AsyncMock(
            return_value={"status": "success", "cell_name": "cell1"}
        )

        result = await poll_cell_build_status(client, "cell1", timeout=5.0)

        assert result.status == CellBuildStatus.READY
        assert result.error_message is None

    @pytest.mark.anyio
    async def test_failed_immediately(self) -> None:
        """Test cell that fails on first poll with error message."""
        client = AsyncMock()
        client.request = AsyncMock(
            return_value={
                "status": "failed",
                "cell_name": "cell1",
                "error": "ValueError: invalid width",
            }
        )

        result = await poll_cell_build_status(client, "cell1", timeout=5.0)

        assert result.status == CellBuildStatus.FAILED
        assert result.error_message == "ValueError: invalid width"

    @pytest.mark.anyio
    async def test_building_then_success(self) -> None:
        """Test cell that is building first, then succeeds."""
        client = AsyncMock()
        client.request = AsyncMock(
            side_effect=[
                {"status": "building", "cell_name": "cell1"},
                {"status": "success", "cell_name": "cell1"},
            ]
        )

        result = await poll_cell_build_status(
            client, "cell1", timeout=5.0, poll_interval=0.05
        )

        assert result.status == CellBuildStatus.READY
        assert client.request.call_count == 2

    @pytest.mark.anyio
    async def test_building_then_failed(self) -> None:
        """Test cell that is building first, then fails."""
        client = AsyncMock()
        client.request = AsyncMock(
            side_effect=[
                {"status": "building", "cell_name": "cell1"},
                {
                    "status": "failed",
                    "cell_name": "cell1",
                    "error": "Compilation error",
                },
            ]
        )

        result = await poll_cell_build_status(
            client, "cell1", timeout=5.0, poll_interval=0.05
        )

        assert result.status == CellBuildStatus.FAILED
        assert result.error_message == "Compilation error"

    @pytest.mark.anyio
    async def test_timeout(self) -> None:
        """Test timeout when cell stays in building state."""
        client = AsyncMock()
        client.request = AsyncMock(
            return_value={"status": "building", "cell_name": "cell1"}
        )

        result = await poll_cell_build_status(
            client, "cell1", timeout=0.2, poll_interval=0.05
        )

        assert result.status == CellBuildStatus.TIMED_OUT

    @pytest.mark.anyio
    async def test_404_then_success(self) -> None:
        """Test 404 race condition: cell not found initially, then succeeds."""
        mock_response = MagicMock()
        mock_response.status_code = 404

        client = AsyncMock()
        client.request = AsyncMock(
            side_effect=[
                httpx.HTTPStatusError(
                    "Not Found", request=MagicMock(), response=mock_response
                ),
                {"status": "success", "cell_name": "cell1"},
            ]
        )

        result = await poll_cell_build_status(
            client, "cell1", timeout=5.0, poll_interval=0.05
        )

        assert result.status == CellBuildStatus.READY

    @pytest.mark.anyio
    async def test_http_error_then_success(self) -> None:
        """Test transient network error followed by success."""
        client = AsyncMock()
        client.request = AsyncMock(
            side_effect=[
                httpx.ConnectError("Connection refused"),
                {"status": "success", "cell_name": "cell1"},
            ]
        )

        result = await poll_cell_build_status(
            client, "cell1", timeout=5.0, poll_interval=0.05
        )

        assert result.status == CellBuildStatus.READY


class TestWaitForBuildCompletionViaApi:
    """Tests for wait_for_build_completion_via_api function."""

    @pytest.mark.anyio
    async def test_empty_cell_names(self) -> None:
        """Test with empty cell list returns empty dict."""
        client = AsyncMock()

        result = await wait_for_build_completion_via_api(client, [])

        assert result == {}

    @pytest.mark.anyio
    async def test_all_cells_succeed(self) -> None:
        """Test multiple cells all succeeding."""
        client = AsyncMock()
        client.request = AsyncMock(
            side_effect=[
                {"status": "success", "cell_name": "cell1"},
                {"status": "success", "cell_name": "cell2"},
            ]
        )

        result = await wait_for_build_completion_via_api(
            client, ["cell1", "cell2"], timeout=5.0
        )

        assert result["cell1"].status == CellBuildStatus.READY
        assert result["cell2"].status == CellBuildStatus.READY

    @pytest.mark.anyio
    async def test_mixed_results(self) -> None:
        """Test one ready, one failed, one timed out."""
        call_count = 0

        async def mock_request(method, path, project=None, **kwargs):
            nonlocal call_count
            call_count += 1
            if "cell_ok" in path:
                return {"status": "success", "cell_name": "cell_ok"}
            if "cell_fail" in path:
                return {
                    "status": "failed",
                    "cell_name": "cell_fail",
                    "error": "Bad param",
                }
            # cell_slow always returns building
            return {"status": "building", "cell_name": "cell_slow"}

        client = AsyncMock()
        client.request = AsyncMock(side_effect=mock_request)

        result = await wait_for_build_completion_via_api(
            client,
            ["cell_ok", "cell_fail", "cell_slow"],
            timeout=0.2,
            poll_interval=0.05,
        )

        assert result["cell_ok"].status == CellBuildStatus.READY
        assert result["cell_fail"].status == CellBuildStatus.FAILED
        assert result["cell_fail"].error_message == "Bad param"
        assert result["cell_slow"].status == CellBuildStatus.TIMED_OUT

    @pytest.mark.anyio
    async def test_parallel_execution(self) -> None:
        """Verify cells are polled concurrently via asyncio.gather."""
        timestamps: list[float] = []

        async def mock_request(method, path, project=None, **kwargs):
            timestamps.append(time.monotonic())
            await asyncio.sleep(0.1)
            return {"status": "success"}

        client = AsyncMock()
        client.request = AsyncMock(side_effect=mock_request)

        await wait_for_build_completion_via_api(client, ["a", "b", "c"], timeout=5.0)

        # All three should start nearly simultaneously (within 50ms)
        assert len(timestamps) == 3
        assert max(timestamps) - min(timestamps) < 0.05


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
