"""End-to-end regression: when ``enable_js_rendering=False``, no
browser-spawning code path can run, even via the lazy imports inside
``_fetch_with_crawl4ai`` and ``_fetch_with_playwright``.

This complements the unit tests in ``test_playwright_html.py`` (which
patch ``_get_playwright_downloader`` to assert it's never called) by
mocking *the actual library symbols* — ``crawl4ai.AsyncWebCrawler`` and
``playwright.sync_api.sync_playwright``. If anyone ever adds a code
path that bypasses the ``self.enable_js_rendering`` gate and reaches
into Crawl4AI/Playwright directly, this test catches it.

Issue #3826 / PR #3971.
"""

from unittest.mock import MagicMock, patch

from local_deep_research.research_library.downloaders.html import HTMLDownloader
from local_deep_research.research_library.downloaders.playwright_html import (
    AutoHTMLDownloader,
)


LONG = "x" * 1000
SHORT_HTML = "<html><body><p>tiny</p></body></html>"
SPA_HTML = (
    '<html><body><div id="root"></div>'
    "<noscript>You need to enable JavaScript</noscript>"
    "</body></html>"
)


class TestNoBrowserSpawnsWhenJSDisabled:
    """With JS disabled, neither Crawl4AI nor Playwright must be
    instantiated, regardless of how the static fetch path resolves
    (short content / SPA signals / JSON content type)."""

    def _patch_browsers(self):
        """Return paired mocks for Crawl4AI and Playwright at their
        actual import sites. Both are lazy-imported inside the
        downloader's fetch methods (``_fetch_with_crawl4ai`` line 112,
        ``_fetch_with_playwright`` line 216), so patching them here
        intercepts the import."""
        crawl4ai_mock = MagicMock(name="AsyncWebCrawler")
        playwright_mock = MagicMock(name="sync_playwright")
        return (
            patch("crawl4ai.AsyncWebCrawler", crawl4ai_mock),
            patch("playwright.sync_api.sync_playwright", playwright_mock),
            crawl4ai_mock,
            playwright_mock,
        )

    def test_no_browser_when_static_returns_short_content(self):
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )

        cm_crawl, cm_pw, crawl_mock, pw_mock = self._patch_browsers()
        with (
            cm_crawl,
            cm_pw,
            patch.object(
                HTMLDownloader, "_fetch_html", return_value=SHORT_HTML
            ),
        ):
            dl.download("https://example.com")

        assert crawl_mock.call_count == 0
        assert pw_mock.call_count == 0
        assert dl._playwright_downloader is None
        dl.close()

    def test_no_browser_when_spa_signals_present(self):
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )
        # Simulate a 403 SPA shell response (matches the path that
        # would normally trigger JS rendering)
        mock_resp = MagicMock(status_code=403, text=SPA_HTML)
        dl.session = MagicMock()
        dl.session.get.return_value = mock_resp

        cm_crawl, cm_pw, crawl_mock, pw_mock = self._patch_browsers()
        with cm_crawl, cm_pw:
            dl.download("https://spa.example.com")

        assert crawl_mock.call_count == 0
        assert pw_mock.call_count == 0
        assert dl._playwright_downloader is None
        dl.close()

    def test_no_browser_when_static_returns_none(self):
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )

        cm_crawl, cm_pw, crawl_mock, pw_mock = self._patch_browsers()
        with (
            cm_crawl,
            cm_pw,
            patch.object(HTMLDownloader, "_fetch_html", return_value=None),
        ):
            dl.download("https://example.com")

        assert crawl_mock.call_count == 0
        assert pw_mock.call_count == 0
        dl.close()

    def test_no_browser_in_download_with_result_path(self):
        """Both ``download()`` and ``download_with_result()`` must
        honor the gate. The latter is the path the agent actually
        uses via ``ContentFetcher.fetch``."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )

        cm_crawl, cm_pw, crawl_mock, pw_mock = self._patch_browsers()
        with (
            cm_crawl,
            cm_pw,
            patch.object(
                HTMLDownloader, "_fetch_html", return_value=SHORT_HTML
            ),
        ):
            dl.download_with_result("https://example.com")

        assert crawl_mock.call_count == 0
        assert pw_mock.call_count == 0
        dl.close()


class TestContentFetcherEndToEndNoBrowser:
    """Same guarantee at the public ``ContentFetcher`` boundary, which
    is what the agent fetch tool and the MCP download tool actually use.
    """

    def test_no_browser_when_content_fetcher_disabled(self):
        from local_deep_research.content_fetcher import ContentFetcher
        from local_deep_research.content_fetcher.url_classifier import (
            URLType,
        )

        crawl4ai_mock = MagicMock(name="AsyncWebCrawler")
        playwright_mock = MagicMock(name="sync_playwright")

        with (
            patch("crawl4ai.AsyncWebCrawler", crawl4ai_mock),
            patch("playwright.sync_api.sync_playwright", playwright_mock),
            ContentFetcher(timeout=5, enable_js_rendering=False) as fetcher,
        ):
            downloader = fetcher._get_downloader(URLType.HTML)
            assert downloader is not None
            # Drive the static path and force "no content" to give the
            # downloader the strongest motivation to fall back to JS:
            with patch.object(
                HTMLDownloader, "_fetch_html", return_value=SHORT_HTML
            ):
                downloader.download_with_result("https://example.com")

        assert crawl4ai_mock.call_count == 0
        assert playwright_mock.call_count == 0
