"""Tests for PlaywrightHTMLDownloader and AutoHTMLDownloader.

Covers: SPA signal detection, inheritance, fallback logic.
Playwright browser tests are not run here (require browser install);
these test the logic around when to use Playwright.
"""

from unittest.mock import Mock, patch


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


LONG_PARA = (
    "This is a sufficiently long paragraph for content extraction "
    "testing purposes that needs to be over two hundred characters long "
    "to pass the minimum content length threshold used by the auto downloader."
)


class TestPlaywrightHTMLDownloaderInheritance:
    """Verify PlaywrightHTMLDownloader inherits from HTMLDownloader."""

    def test_is_subclass_of_html_downloader(self):
        assert issubclass(PlaywrightHTMLDownloader, HTMLDownloader)

    def test_has_language(self):
        dl = PlaywrightHTMLDownloader(timeout=5)
        assert dl.language == "English"
        dl.close()

    def test_can_handle_http(self):
        dl = PlaywrightHTMLDownloader(timeout=5)
        assert dl.can_handle("https://example.com") is True
        assert dl.can_handle("ftp://example.com") is False
        dl.close()


class TestAutoHTMLDownloaderInheritance:
    """Verify AutoHTMLDownloader inherits from HTMLDownloader."""

    def test_is_subclass_of_html_downloader(self):
        assert issubclass(AutoHTMLDownloader, HTMLDownloader)

    def test_has_language(self):
        dl = AutoHTMLDownloader(timeout=5)
        assert dl.language == "English"
        dl.close()


class TestSPASignalDetection:
    """Tests for SPA/JS framework detection heuristics."""

    def test_detects_react_root(self):
        html = '<html><body><div id="root"></div></body></html>'
        assert AutoHTMLDownloader._has_spa_signals(html) is True

    def test_detects_vue_app(self):
        html = '<html><body><div id="app"></div></body></html>'
        assert AutoHTMLDownloader._has_spa_signals(html) is True

    def test_detects_nextjs(self):
        html = '<html><body><div id="__next"><script>__NEXT_DATA__</script></div></body></html>'
        assert AutoHTMLDownloader._has_spa_signals(html) is True

    def test_detects_noscript_warning(self):
        html = "<html><body><noscript>You need to enable JavaScript to run this app.</noscript></body></html>"
        assert AutoHTMLDownloader._has_spa_signals(html) is True

    def test_no_signals_in_normal_html(self):
        html = (
            f"<html><body><article><p>{LONG_PARA}</p></article></body></html>"
        )
        assert AutoHTMLDownloader._has_spa_signals(html) is False

    def test_no_signals_in_empty_html(self):
        assert AutoHTMLDownloader._has_spa_signals("") is False

    def test_all_known_signals_detected(self):
        for signal in SPA_SIGNALS:
            html = f"<html><body>{signal}</body></html>"
            assert AutoHTMLDownloader._has_spa_signals(html) is True, (
                f"Signal not detected: {signal}"
            )


class TestAutoDownloaderFallbackLogic:
    """Tests for the static-first, Playwright-fallback logic."""

    def test_static_success_skips_playwright(self):
        """When static fetch returns enough content, Playwright is not used."""
        dl = AutoHTMLDownloader(timeout=5, min_content_length=50)
        html = f"<html><head><title>Test</title></head><body><p>{LONG_PARA}</p></body></html>"

        with patch.object(HTMLDownloader, "_fetch_html", return_value=html):
            result = dl.download("https://example.com")

        assert result is not None
        assert len(result) > 50
        # Playwright downloader should not have been initialized
        assert dl._playwright_downloader is None
        dl.close()

    def test_short_content_with_spa_triggers_playwright(self):
        """When content is short and SPA signals present, tries Playwright."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=True
        )
        spa_html = '<html><body><div id="root"></div><noscript>You need to enable JavaScript</noscript></body></html>'

        pw_content = f"<html><body><p>{LONG_PARA}</p></body></html>"

        # Mock the session.get to return a SPA challenge page
        mock_response = Mock()
        mock_response.status_code = 403
        mock_response.text = spa_html
        dl.session = Mock()
        dl.session.get.return_value = mock_response

        mock_pw = Mock()
        mock_pw.download.return_value = pw_content.encode("utf-8")
        dl._playwright_downloader = mock_pw

        result = dl.download("https://spa-app.com")

        assert result is not None
        assert LONG_PARA.encode("utf-8") in result
        mock_pw.download.assert_called_once()
        dl.close()


class TestPlaywrightDownloaderClose:
    """Tests for resource cleanup."""

    def test_close_without_browser(self):
        """close() works when browser was never started."""
        dl = PlaywrightHTMLDownloader(timeout=5)
        dl.close()  # Should not raise

    def test_auto_close_without_playwright(self):
        """close() works when Playwright fallback was never used."""
        dl = AutoHTMLDownloader(timeout=5)
        dl.close()  # Should not raise


class TestAutoDownloaderJSRenderingToggle:
    """Regression tests for ``enable_js_rendering`` (issue #3826).

    The default Docker production image ships without Chromium, so the
    JS-rendering fallback must be skippable to avoid wasted work and
    confusing tracebacks on every fetch. With the toggle off, the
    downloader must never construct or invoke its Playwright fallback,
    even when content is short or SPA signals are present.
    """

    def test_disabled_skips_js_when_content_short(self):
        """JS off + short content → static result returned, no Playwright."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )
        short_html = "<html><body><p>tiny</p></body></html>"

        with patch.object(
            HTMLDownloader, "_fetch_html", return_value=short_html
        ):
            with patch.object(
                AutoHTMLDownloader,
                "_get_playwright_downloader",
                side_effect=AssertionError(
                    "JS fallback must not run when disabled"
                ),
            ):
                result = dl.download("https://example.com")

        # Static result is returned (or None) without raising
        assert dl._playwright_downloader is None
        # Result is the static (short) extraction result
        assert result is None or len(result) < 200
        dl.close()

    def test_disabled_skips_js_with_spa_signals(self):
        """JS off + SPA signals → static result returned, no Playwright."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )
        spa_html = (
            '<html><body><div id="root"></div>'
            "<noscript>You need to enable JavaScript</noscript>"
            "</body></html>"
        )
        mock_response = Mock()
        mock_response.status_code = 403
        mock_response.text = spa_html
        dl.session = Mock()
        dl.session.get.return_value = mock_response

        with patch.object(
            AutoHTMLDownloader,
            "_get_playwright_downloader",
            side_effect=AssertionError(
                "JS fallback must not run when disabled"
            ),
        ):
            result = dl.download("https://spa-app.com")

        # Static path returned None (SPA shell), but no Playwright was used
        assert dl._playwright_downloader is None
        assert result is None or len(result) < 200
        dl.close()

    def test_disabled_also_skips_in_download_with_result(self):
        """download_with_result must honor the toggle as well."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=False
        )
        short_html = "<html><body><p>tiny</p></body></html>"

        with patch.object(
            HTMLDownloader, "_fetch_html", return_value=short_html
        ):
            with patch.object(
                AutoHTMLDownloader,
                "_get_playwright_downloader",
                side_effect=AssertionError(
                    "JS fallback must not run when disabled"
                ),
            ):
                result = dl.download_with_result("https://example.com")

        # Static DownloadResult was returned without invoking JS
        assert dl._playwright_downloader is None
        assert result is not None
        dl.close()

    def test_enabled_still_falls_back_to_js(self):
        """Regression guard: when explicitly enabled, the JS fallback
        is still triggered for short SPA pages (existing behaviour)."""
        dl = AutoHTMLDownloader(
            timeout=5, min_content_length=200, enable_js_rendering=True
        )
        spa_html = (
            '<html><body><div id="root"></div>'
            "<noscript>You need to enable JavaScript</noscript>"
            "</body></html>"
        )
        pw_content = f"<html><body><p>{LONG_PARA}</p></body></html>"
        mock_response = Mock()
        mock_response.status_code = 403
        mock_response.text = spa_html
        dl.session = Mock()
        dl.session.get.return_value = mock_response

        mock_pw = Mock()
        mock_pw.download.return_value = pw_content.encode("utf-8")
        dl._playwright_downloader = mock_pw

        result = dl.download("https://spa-app.com")

        assert result is not None
        mock_pw.download.assert_called_once()
        dl.close()

    def test_default_is_false_for_direct_callers(self):
        """The constructor defaults to ``enable_js_rendering=False``,
        consistent with every other layer in the stack
        (``ContentFetcher``, ``fetch_and_extract``, ``FullSearchResults``,
        and the user-facing setting). Callers that explicitly need JS
        rendering opt in by passing ``enable_js_rendering=True``.
        """
        dl = AutoHTMLDownloader(timeout=5)
        assert dl.enable_js_rendering is False
        dl.close()
