"""Tests for the shared backend (gsearch_mcp_cli.shared)."""

from __future__ import annotations

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from gsearch_mcp_cli.exceptions import SearchBlockedError
from gsearch_mcp_cli.models import AIResult, FetchResult, OrganicResult, SearchResult, Source

# ---------------------------------------------------------------------------
# Helpers to reset module-level globals between tests
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
async def _reset_shared_state():
    """Reset shared module globals before and after each test."""
    import gsearch_mcp_cli.shared as mod
    mod._browser_cdp = None
    mod._chrome_proc = None
    mod._rate_limiter = None
    mod._init_lock = None
    yield
    mod._browser_cdp = None
    mod._chrome_proc = None
    mod._rate_limiter = None
    mod._init_lock = None


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def mock_tab():
    tab = AsyncMock()
    tab.navigate = AsyncMock()
    tab.evaluate = AsyncMock(return_value={})
    tab.__aenter__ = AsyncMock(return_value=tab)
    tab.__aexit__ = AsyncMock(return_value=False)
    return tab


@pytest.fixture
def mock_browser_cdp(mock_tab):
    browser = AsyncMock()
    browser.create_tab = MagicMock(return_value=mock_tab)
    browser.close = AsyncMock()
    browser.is_healthy = AsyncMock(return_value=True)
    return browser


@pytest.fixture
def patch_get_browser(mock_browser_cdp):
    """Patch _get_browser to return the mock BrowserCDP."""
    with patch("gsearch_mcp_cli.shared._get_browser", return_value=mock_browser_cdp) as p:
        yield p


@pytest.fixture
def patch_rate_limiter():
    """Patch the rate limiter so acquire() is instant."""
    rl = AsyncMock()
    rl.acquire = AsyncMock()
    with patch("gsearch_mcp_cli.shared._get_rate_limiter", return_value=rl) as p:
        yield p


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


@pytest.mark.asyncio
async def test_search_returns_search_result(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() navigates, extracts, and returns a SearchResult."""
    expected = SearchResult(
        query="python tutorial",
        results=[
            OrganicResult(
                position=1,
                title="Python Docs",
                url="https://docs.python.org",
                snippet="Official docs",
                source="docs.python.org",
            )
        ],
    )

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search

        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            result = await search("python tutorial")

    assert isinstance(result, SearchResult)
    assert result.query == "python tutorial"
    assert len(result.results) == 1
    assert result.results[0].title == "Python Docs"
    mock_tab.navigate.assert_called_once()
    nav_url = mock_tab.navigate.call_args[0][0]
    assert "q=python+tutorial" in nav_url


@pytest.mark.asyncio
async def test_search_with_time_filter(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() passes time_filter as tbs param in URL."""
    expected = SearchResult(query="AI news", results=[])

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("AI news", time_filter="day")

    nav_url = mock_tab.navigate.call_args[0][0]
    assert "tbs=qdr" in nav_url


@pytest.mark.asyncio
async def test_search_with_12h_filter(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() with 12h time filter generates qdr:h12."""
    expected = SearchResult(query="news", results=[])

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("news", time_filter="12h")

    nav_url = mock_tab.navigate.call_args[0][0]
    assert "qdr%3Ah12" in nav_url or "qdr:h12" in nav_url


@pytest.mark.asyncio
async def test_search_with_arbitrary_hours(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() with '6h' generates qdr:h6."""
    expected = SearchResult(query="news", results=[])

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("news", time_filter="6h")

    nav_url = mock_tab.navigate.call_args[0][0]
    assert "qdr%3Ah6" in nav_url or "qdr:h6" in nav_url


@pytest.mark.asyncio
async def test_search_with_date_range(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() passes date_from/date_to as tbs cdr param."""
    expected = SearchResult(query="earnings", results=[])

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("earnings", date_from="2026-01-01", date_to="2026-03-21")

    nav_url = mock_tab.navigate.call_args[0][0]
    assert "tbs=cdr" in nav_url
    assert "2026" in nav_url


@pytest.mark.asyncio
async def test_search_with_type_news(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() with search_type="news" adds tbm=nws and passes search_type to extractor."""
    expected = SearchResult(query="AI news", results=[])
    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ) as mock_extract:
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("AI news", search_type="news")
    nav_url = mock_tab.navigate.call_args[0][0]
    assert "tbm=nws" in nav_url
    mock_extract.assert_called_once()
    _, kwargs = mock_extract.call_args
    assert kwargs.get("search_type") == "news" or (
        len(mock_extract.call_args[0]) > 2 and mock_extract.call_args[0][2] == "news"
    )


@pytest.mark.asyncio
async def test_search_with_type_forums(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() with search_type="forums" adds udm=18 to URL."""
    expected = SearchResult(query="k8s error", results=[])
    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import search
        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            await search("k8s error", search_type="forums")
    nav_url = mock_tab.navigate.call_args[0][0]
    assert "udm=18" in nav_url


@pytest.mark.asyncio
async def test_ai_search_returns_ai_result(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """ai_search() navigates with udm=50 and returns an AIResult."""
    expected = AIResult(
        query="what is python",
        synthesis="Python is a programming language.",
        sources=[
            Source(title="Wikipedia", url="https://en.wikipedia.org/wiki/Python", domain="wikipedia.org")
        ],
    )

    with patch(
        "gsearch_mcp_cli.shared.extract_ai_mode",
        new_callable=AsyncMock,
        return_value=expected,
    ):
        from gsearch_mcp_cli.shared import ai_search

        with patch("gsearch_mcp_cli.shared.asyncio.sleep", new_callable=AsyncMock):
            result = await ai_search("what is python")

    assert isinstance(result, AIResult)
    assert result.query == "what is python"
    assert "Python is a programming language" in result.synthesis
    assert len(result.sources) == 1
    nav_url = mock_tab.navigate.call_args[0][0]
    assert "udm=50" in nav_url


@pytest.mark.asyncio
async def test_fetch_returns_fetch_result(patch_get_browser, patch_rate_limiter):
    """fetch() validates URL and returns a FetchResult."""
    expected = FetchResult(
        url="https://example.com",
        title="Example",
        content="Hello world",
        word_count=2,
    )

    with patch("gsearch_mcp_cli.shared.validate_url", return_value="https://example.com"), \
         patch(
             "gsearch_mcp_cli.shared.fetch_page",
             new_callable=AsyncMock,
             return_value=expected,
         ):
        from gsearch_mcp_cli.shared import fetch

        result = await fetch("https://example.com")

    assert isinstance(result, FetchResult)
    assert result.url == "https://example.com"
    assert result.content == "Hello world"
    assert result.word_count == 2


@pytest.mark.asyncio
async def test_search_retries_on_blocked(
    mock_browser_cdp, mock_tab, patch_get_browser, patch_rate_limiter
):
    """search() catches SearchBlockedError, warms up, and retries once."""
    ok_result = SearchResult(query="test", results=[])
    call_count = 0

    async def _extract_side_effect(tab, query, search_type=None):
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            raise SearchBlockedError("blocked")
        return ok_result

    with patch(
        "gsearch_mcp_cli.shared.extract_search_results",
        side_effect=_extract_side_effect,
    ), patch(
        "gsearch_mcp_cli.shared.warmup_profile",
        new_callable=AsyncMock,
    ) as mock_warmup, patch(
        "gsearch_mcp_cli.shared.mark_warmed_up",
    ), patch(
        "gsearch_mcp_cli.shared.asyncio.sleep",
        new_callable=AsyncMock,
    ):
        from gsearch_mcp_cli.shared import search

        result = await search("test")

    assert isinstance(result, SearchResult)
    assert call_count == 2
    mock_warmup.assert_awaited_once()


@pytest.mark.asyncio
async def test_shutdown_cleans_up(mock_browser_cdp):
    """shutdown() closes BrowserCDP, terminates Chrome, and resets globals."""
    import gsearch_mcp_cli.shared as mod

    mock_proc = MagicMock()
    mock_proc.poll = MagicMock(return_value=None)
    mock_proc.terminate = MagicMock()
    mock_proc.wait = MagicMock()

    mod._browser_cdp = mock_browser_cdp
    mod._chrome_proc = mock_proc
    mod._rate_limiter = MagicMock()
    mod._init_lock = asyncio.Lock()

    with patch("gsearch_mcp_cli.shared.shutdown_chrome") as mock_shutdown_chrome:
        await mod.shutdown()

    mock_browser_cdp.close.assert_awaited_once()
    mock_shutdown_chrome.assert_called_once_with(mock_proc)
    assert mod._browser_cdp is None
    assert mod._chrome_proc is None
    assert mod._rate_limiter is None
    assert mod._init_lock is None
