import platform

import pytest

from gsearch_mcp_cli.core.browser import (
    _clear_profile_locks,
    _drain_stderr,
    build_chrome_args,
    connect_existing,
    find_browser,
    parse_port_from_stderr,
)
from gsearch_mcp_cli.exceptions import BrowserNotFoundError, ChromeLaunchError


def test_parse_port_from_stderr_standard():
    line = "DevTools listening on ws://127.0.0.1:9345/devtools/browser/abc-123"
    assert parse_port_from_stderr(line) == 9345


def test_parse_port_from_stderr_different_port():
    line = "DevTools listening on ws://127.0.0.1:12345/devtools/browser/xyz"
    assert parse_port_from_stderr(line) == 12345


def test_parse_port_from_stderr_no_match():
    assert parse_port_from_stderr("some other output") is None
    assert parse_port_from_stderr("") is None
    assert parse_port_from_stderr("Starting Chrome...") is None


def test_build_chrome_args_includes_headless():
    args = build_chrome_args("/usr/bin/chrome", "/tmp/profile")
    assert "--headless=new" in args
    assert "--remote-debugging-port=0" in args
    assert "--remote-allow-origins=http://localhost" in args
    assert args[0] == "/usr/bin/chrome"
    assert "--user-data-dir=/tmp/profile" in args


def test_build_chrome_args_includes_platform_flags():
    args = build_chrome_args("/usr/bin/chrome", "/tmp/profile")
    assert "--disable-gpu" in args
    if platform.system() == "Linux":
        assert "--disable-dev-shm-usage" in args
        assert "--no-sandbox" in args


def test_find_browser_auto():
    """Should find at least one browser on the test system."""
    try:
        path = find_browser("auto")
        assert path and len(path) > 0
    except BrowserNotFoundError:
        pytest.skip("No Chromium browser installed on this system")


def test_find_browser_invalid():
    with pytest.raises(BrowserNotFoundError):
        find_browser("nonexistent_browser_xyz")


# ---------------------------------------------------------------------------
# New tests: Chrome launch hardening
# ---------------------------------------------------------------------------


def test_drain_stderr_captures_output():
    """_drain_stderr reads remaining lines from a dead process's stderr."""
    from io import StringIO
    from unittest.mock import MagicMock

    proc = MagicMock()
    proc.stderr = StringIO("line1\nline2\nprofile appears to be in use\n")
    result = _drain_stderr(proc)
    assert "profile appears to be in use" in result
    assert "line1" in result


def test_drain_stderr_handles_empty():
    """_drain_stderr returns empty string when stderr is empty."""
    from io import StringIO
    from unittest.mock import MagicMock

    proc = MagicMock()
    proc.stderr = StringIO("")
    assert _drain_stderr(proc) == ""


def test_drain_stderr_handles_exception():
    """_drain_stderr returns empty on error (e.g. closed pipe)."""
    from unittest.mock import MagicMock

    proc = MagicMock()
    proc.stderr.readline.side_effect = OSError("broken pipe")
    assert _drain_stderr(proc) == ""


def test_clear_profile_locks(tmp_path):
    """_clear_profile_locks removes Singleton* files."""
    for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"):
        (tmp_path / name).touch()
    (tmp_path / "Default").mkdir()  # should not be removed

    removed = _clear_profile_locks(tmp_path)
    assert removed is True
    assert not (tmp_path / "SingletonLock").exists()
    assert not (tmp_path / "SingletonSocket").exists()
    assert not (tmp_path / "SingletonCookie").exists()
    assert (tmp_path / "Default").exists()


def test_clear_profile_locks_none_present(tmp_path):
    """_clear_profile_locks returns False when no lock files exist."""
    assert _clear_profile_locks(tmp_path) is False


@pytest.mark.asyncio
async def test_launch_chrome_raises_chrome_launch_error_with_stderr():
    """launch_chrome raises ChromeLaunchError with exit_code and stderr when Chrome crashes."""
    from io import StringIO
    from unittest.mock import MagicMock, patch

    from gsearch_mcp_cli.core.browser import launch_chrome

    mock_proc = MagicMock()
    mock_proc.stderr = StringIO("")  # empty = process dead
    mock_proc.poll.return_value = 21
    mock_proc.returncode = 21

    with patch("gsearch_mcp_cli.core.browser.find_browser", return_value="/usr/bin/chrome"), \
         patch("gsearch_mcp_cli.core.browser.subprocess.Popen", return_value=mock_proc), \
         patch("gsearch_mcp_cli.core.browser._clear_profile_locks", return_value=False):
        with pytest.raises(ChromeLaunchError) as exc_info:
            await launch_chrome(profile_dir="/tmp/test-profile")

    assert exc_info.value.exit_code == 21
    assert "21" in str(exc_info.value)


@pytest.mark.asyncio
async def test_launch_chrome_retries_on_profile_lock():
    """launch_chrome clears locks and retries on exit code 21."""
    from io import StringIO
    from unittest.mock import AsyncMock, MagicMock, patch

    from gsearch_mcp_cli.core.browser import launch_chrome

    # First call: Chrome exits with 21
    fail_proc = MagicMock()
    fail_proc.stderr = StringIO("")
    fail_proc.poll.return_value = 21
    fail_proc.returncode = 21

    # Second call: Chrome succeeds
    ok_proc = MagicMock()
    ok_proc.stderr = StringIO("DevTools listening on ws://127.0.0.1:9999/devtools/browser/abc\n")
    ok_proc.poll.return_value = None
    ok_proc.pid = 12345

    call_count = 0

    def mock_popen(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        return fail_proc if call_count == 1 else ok_proc

    with patch("gsearch_mcp_cli.core.browser.find_browser", return_value="/usr/bin/chrome"), \
         patch("gsearch_mcp_cli.core.browser.subprocess.Popen", side_effect=mock_popen), \
         patch("gsearch_mcp_cli.core.browser._clear_profile_locks", return_value=False), \
         patch("gsearch_mcp_cli.core.browser._write_port_map"), \
         patch("gsearch_mcp_cli.core.browser.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client_cls.return_value = mock_client

        proc, port = await launch_chrome(profile_dir="/tmp/test-profile")

    assert call_count == 2  # first fail, then retry
    assert port == 9999
    assert proc is ok_proc


@pytest.mark.asyncio
async def test_connect_existing_returns_live_port():
    """connect_existing returns (None, port) for a healthy Chrome."""
    from unittest.mock import AsyncMock, MagicMock, patch

    with patch("gsearch_mcp_cli.core.browser._read_port_map",
               return_value={"8888": {"pid": 999}}), \
         patch("gsearch_mcp_cli.core.browser.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client_cls.return_value = mock_client

        result = await connect_existing()

    assert result == (None, 8888)


@pytest.mark.asyncio
async def test_connect_existing_returns_none_when_no_chrome():
    """connect_existing returns None when port map is empty."""
    from unittest.mock import patch

    with patch("gsearch_mcp_cli.core.browser._read_port_map", return_value={}):
        result = await connect_existing()

    assert result is None


@pytest.mark.asyncio
async def test_connect_or_launch_fallback_flow():
    """connect_or_launch fails launch, polls backoff, then connects to peer Chrome."""
    from unittest.mock import AsyncMock, patch

    from gsearch_mcp_cli.core.browser import connect_or_launch
    from gsearch_mcp_cli.exceptions import ChromeLaunchError

    call_count = 0

    async def mock_connect_existing(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        # 1st call: inside lock (no Chrome yet) → None
        # 2nd call: first backoff poll → peer Chrome appeared
        return None if call_count == 1 else (None, 9999)

    async def mock_launch_chrome(*args, **kwargs):
        raise ChromeLaunchError("fake crash", exit_code=21, stderr="crashed")

    with patch("gsearch_mcp_cli.core.browser.connect_existing", side_effect=mock_connect_existing), \
         patch("gsearch_mcp_cli.core.browser.launch_chrome", side_effect=mock_launch_chrome), \
         patch("gsearch_mcp_cli.core.browser.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:

        proc, port = await connect_or_launch()

    assert call_count == 2
    assert port == 9999
    assert proc is None
    # First backoff delay is 1s
    mock_sleep.assert_awaited_once_with(1)


@pytest.mark.asyncio
async def test_connect_or_launch_propagates_non_21():
    """connect_or_launch raises immediately on non-lock-related exit codes."""
    from unittest.mock import patch

    from gsearch_mcp_cli.core.browser import connect_or_launch
    from gsearch_mcp_cli.exceptions import ChromeLaunchError

    async def mock_launch_chrome(*args, **kwargs):
        raise ChromeLaunchError("fake crash", exit_code=127, stderr="crashed")

    with patch("gsearch_mcp_cli.core.browser.connect_existing", return_value=None), \
         patch("gsearch_mcp_cli.core.browser.launch_chrome", side_effect=mock_launch_chrome):

        with pytest.raises(ChromeLaunchError) as exc:
            await connect_or_launch()
        assert exc.value.exit_code == 127
