"""
Integration tests for new search adapters with real API calls.

These tests verify that the adapters work correctly against live APIs.
They are marked with @pytest.mark.integration and make real network requests.

The TestFactoryIntegration class verifies that engines can be created through
the production factory path (create_search_engine → get_safe_module_class),
which requires correct entries in the security whitelist.
"""

import pytest

from local_deep_research.web_search_engines.search_engine_base import (
    BaseSearchEngine,
)
from local_deep_research.web_search_engines.search_engine_factory import (
    create_search_engine,
)


@pytest.mark.integration
class TestOpenLibraryIntegration:
    """Integration tests for Open Library with real API calls."""

    def test_create_engine(self):
        """Test that Open Library engine can be instantiated."""
        from local_deep_research.web_search_engines.engines.search_engine_openlibrary import (
            OpenLibrarySearchEngine,
        )

        engine = OpenLibrarySearchEngine(max_results=5)
        assert engine is not None
        assert engine.max_results == 5

    def test_search_and_get_results(self):
        """Test full search flow with real API."""
        from local_deep_research.web_search_engines.engines.search_engine_openlibrary import (
            OpenLibrarySearchEngine,
        )

        engine = OpenLibrarySearchEngine(max_results=5)
        results = engine.run("lord of the rings")

        assert len(results) > 0, "Should return search results"

        for r in results:
            assert "title" in r, "Each result should have a title"
            assert "link" in r, "Each result should have a link"
            assert r.get("source") == "Open Library"

        titles = [r["title"].lower() for r in results]
        assert any("lord" in t or "ring" in t for t in titles), (
            f"Should find Lord of the Rings, got: {titles}"
        )

    def test_search_returns_authors(self):
        """Test that search returns author information."""
        from local_deep_research.web_search_engines.engines.search_engine_openlibrary import (
            OpenLibrarySearchEngine,
        )

        engine = OpenLibrarySearchEngine(max_results=3)
        results = engine.run("1984 orwell")

        assert len(results) > 0
        has_authors = any(r.get("authors") for r in results)
        assert has_authors, "At least one result should have authors"


@pytest.mark.integration
class TestGutenbergIntegration:
    """Integration tests for Gutenberg with real API calls."""

    def test_create_engine(self):
        """Test that Gutenberg engine can be instantiated."""
        from local_deep_research.web_search_engines.engines.search_engine_gutenberg import (
            GutenbergSearchEngine,
        )

        engine = GutenbergSearchEngine(max_results=5)
        assert engine is not None
        assert engine.max_results == 5

    def test_search_and_get_results(self):
        """Test full search flow with real API."""
        from local_deep_research.web_search_engines.engines.search_engine_gutenberg import (
            GutenbergSearchEngine,
        )

        engine = GutenbergSearchEngine(max_results=5)
        results = engine.run("sherlock holmes")

        assert len(results) > 0, "Should return search results"

        for r in results:
            assert "title" in r, "Each result should have a title"
            assert "link" in r, "Each result should have a link"
            assert r.get("source") == "Project Gutenberg"
            assert "gutenberg.org" in r["link"]

        titles = [r["title"].lower() for r in results]
        assert any("sherlock" in t or "holmes" in t for t in titles), (
            f"Should find Sherlock Holmes, got: {titles}"
        )

    def test_search_returns_authors(self):
        """Test that search returns author information."""
        from local_deep_research.web_search_engines.engines.search_engine_gutenberg import (
            GutenbergSearchEngine,
        )

        engine = GutenbergSearchEngine(max_results=3)
        results = engine.run("pride and prejudice")

        assert len(results) > 0
        found_austen = False
        for r in results:
            for author in r.get("authors", []):
                if "austen" in author.lower():
                    found_austen = True
                    break
        assert found_austen, (
            f"Should find Austen, got: {[r.get('authors') for r in results]}"
        )


@pytest.mark.integration
class TestZenodoIntegration:
    """Integration tests for Zenodo with real API calls.

    Note: These tests depend on external Zenodo API availability.
    They handle empty results gracefully since API may be rate-limited in CI.
    """

    def test_create_engine(self):
        """Test that Zenodo engine can be instantiated."""
        from local_deep_research.web_search_engines.engines.search_engine_zenodo import (
            ZenodoSearchEngine,
        )

        engine = ZenodoSearchEngine(max_results=5)
        assert engine is not None
        assert engine.max_results == 5

    def test_search_and_get_results(self):
        """Test full search flow with real API."""
        from local_deep_research.web_search_engines.engines.search_engine_zenodo import (
            ZenodoSearchEngine,
        )

        engine = ZenodoSearchEngine(max_results=5)
        results = engine.run("climate data")

        # API may be rate-limited; verify structure if results returned
        if len(results) > 0:
            for r in results:
                assert "title" in r, "Each result should have a title"
                assert "link" in r, "Each result should have a link"
                assert r.get("source") == "Zenodo"
                assert "zenodo.org" in r["link"]

    def test_search_returns_doi(self):
        """Test that search returns DOI information."""
        from local_deep_research.web_search_engines.engines.search_engine_zenodo import (
            ZenodoSearchEngine,
        )

        engine = ZenodoSearchEngine(max_results=5)
        results = engine.run("neural network dataset")

        # API may be rate-limited; verify DOI if results returned
        if len(results) > 0:
            has_doi = any(r.get("doi") for r in results)
            assert has_doi, "At least one result should have a DOI"


@pytest.mark.integration
class TestStackExchangeIntegration:
    """Integration tests for Stack Exchange with real API calls."""

    def test_create_engine(self):
        """Test that Stack Exchange engine can be instantiated."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        assert engine is not None
        assert engine.max_results == 5

    def test_search_and_get_results(self):
        """Test full search flow with real API."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine.run("python list comprehension")

        assert len(results) > 0, "Should return search results"

        for r in results:
            assert "title" in r, "Each result should have a title"
            assert "link" in r, "Each result should have a link"
            assert r.get("source") == "Stack Overflow"
            assert "stackoverflow.com" in r["link"]

    def test_search_returns_scores(self):
        """Test that search returns score information."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine.run("javascript async await")

        assert len(results) > 0
        for r in results:
            assert "score" in r, "Each result should have a score"
            assert isinstance(r["score"], int), "Score should be integer"

    def test_search_returns_tags(self):
        """Test that search returns tag information."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine.run("react hooks useState")

        assert len(results) > 0
        has_tags = any(r.get("tags") for r in results)
        assert has_tags, "At least one result should have tags"


@pytest.mark.integration
class TestPubChemIntegration:
    """Integration tests for PubChem with real API calls.

    Note: These tests depend on external PubChem API availability.
    They handle empty results gracefully since API may be rate-limited in CI.
    """

    def test_create_engine(self):
        """Test that PubChem engine can be instantiated."""
        from local_deep_research.web_search_engines.engines.search_engine_pubchem import (
            PubChemSearchEngine,
        )

        engine = PubChemSearchEngine(max_results=5)
        assert engine is not None
        assert engine.max_results == 5

    def test_search_and_get_results(self):
        """Test full search flow with real API."""
        from local_deep_research.web_search_engines.engines.search_engine_pubchem import (
            PubChemSearchEngine,
        )

        engine = PubChemSearchEngine(max_results=3)
        results = engine.run("caffeine")

        # API may be rate-limited; verify structure if results returned
        if len(results) > 0:
            first = results[0]
            assert "title" in first, "Should have title"
            assert "link" in first, "Should have link"
            assert first.get("source") == "PubChem"
            assert "pubchem.ncbi.nlm.nih.gov" in first["link"]

    def test_search_returns_molecular_properties(self):
        """Test that search returns molecular properties."""
        from local_deep_research.web_search_engines.engines.search_engine_pubchem import (
            PubChemSearchEngine,
        )

        engine = PubChemSearchEngine(max_results=1)
        results = engine.run("ibuprofen")

        # API may be rate-limited; verify properties if results returned
        if len(results) > 0:
            first = results[0]
            assert first.get("molecular_formula"), (
                "Should have molecular formula"
            )
            assert first.get("molecular_weight"), "Should have molecular weight"
            assert first.get("cid"), "Should have PubChem CID"

    def test_search_returns_smiles(self):
        """Test that search returns SMILES notation."""
        from local_deep_research.web_search_engines.engines.search_engine_pubchem import (
            PubChemSearchEngine,
        )

        engine = PubChemSearchEngine(max_results=1)
        results = engine.run("glucose")

        # API may be rate-limited; verify SMILES if results returned
        if len(results) > 0:
            first = results[0]
            assert first.get("smiles"), "Should have SMILES notation"


@pytest.mark.integration
class TestAllAdaptersRoundTrip:
    """Test all adapters in a single parametrized test.

    Note: These tests depend on external API availability.
    They handle empty results gracefully since APIs may be rate-limited in CI.
    """

    @pytest.mark.parametrize(
        "engine_class_path,engine_class_name,query,expected_source",
        [
            (
                "local_deep_research.web_search_engines.engines.search_engine_openlibrary",
                "OpenLibrarySearchEngine",
                "frankenstein shelley",
                "Open Library",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_gutenberg",
                "GutenbergSearchEngine",
                "moby dick",
                "Project Gutenberg",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_zenodo",
                "ZenodoSearchEngine",
                "astronomy dataset",
                "Zenodo",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_stackexchange",
                "StackExchangeSearchEngine",
                "docker container",
                "Stack Overflow",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_pubchem",
                "PubChemSearchEngine",
                "ethanol",
                "PubChem",
            ),
        ],
    )
    def test_adapter_full_flow(
        self, engine_class_path, engine_class_name, query, expected_source
    ):
        """Test each adapter's full flow."""
        import importlib

        module = importlib.import_module(engine_class_path)
        engine_class = getattr(module, engine_class_name)
        engine = engine_class(max_results=3)

        assert engine is not None, f"Failed to create {engine_class_name}"

        results = engine.run(query)

        # API may be rate-limited; verify structure if results returned
        if len(results) > 0:
            for r in results:
                assert r.get("source") == expected_source, (
                    f"Source should be {expected_source}, got {r.get('source')}"
                )
                assert "title" in r, (
                    f"{engine_class_name} results should have title"
                )
                assert "link" in r, (
                    f"{engine_class_name} results should have link"
                )

    @pytest.mark.parametrize(
        "engine_class_path,engine_class_name,query",
        [
            (
                "local_deep_research.web_search_engines.engines.search_engine_openlibrary",
                "OpenLibrarySearchEngine",
                "frankenstein shelley",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_gutenberg",
                "GutenbergSearchEngine",
                "moby dick",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_zenodo",
                "ZenodoSearchEngine",
                "astronomy dataset",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_stackexchange",
                "StackExchangeSearchEngine",
                "docker container",
            ),
            (
                "local_deep_research.web_search_engines.engines.search_engine_pubchem",
                "PubChemSearchEngine",
                "ethanol",
            ),
        ],
    )
    def test_adapter_full_content(
        self, engine_class_path, engine_class_name, query
    ):
        """Test each adapter's _get_full_content() path (search_snippets_only=False)."""
        import importlib

        module = importlib.import_module(engine_class_path)
        engine_class = getattr(module, engine_class_name)
        engine = engine_class(max_results=2, search_snippets_only=False)

        results = engine.run(query)

        # API may be rate-limited; verify content field if results returned
        if len(results) > 0:
            for r in results:
                assert "content" in r, (
                    f"{engine_class_name} full content results should have 'content' field"
                )
                assert "_raw" not in r, (
                    f"{engine_class_name} should clean up _raw in full content"
                )


def _make_snapshot(engine_key: str, module_path: str, class_name: str) -> dict:
    """Build a minimal flat settings snapshot for a single engine.

    Uses ``ui_element: "checkbox"`` for boolean fields so that
    ``get_typed_setting_value`` preserves the boolean type.
    """
    return {
        f"search.engine.web.{engine_key}.module_path": {
            "value": module_path,
        },
        f"search.engine.web.{engine_key}.class_name": {
            "value": class_name,
        },
        f"search.engine.web.{engine_key}.requires_api_key": {
            "value": False,
            "ui_element": "checkbox",
        },
    }


class TestFactoryIntegration:
    """Verify that create_search_engine() can instantiate each new engine.

    These tests exercise the full production path: settings snapshot →
    search_config() → get_safe_module_class() (security whitelist) →
    engine class instantiation. No mocking of the whitelist or factory.
    """

    @pytest.mark.parametrize(
        "engine_key,module_path,class_name",
        [
            (
                "openlibrary",
                ".engines.search_engine_openlibrary",
                "OpenLibrarySearchEngine",
            ),
            (
                "gutenberg",
                ".engines.search_engine_gutenberg",
                "GutenbergSearchEngine",
            ),
            (
                "zenodo",
                ".engines.search_engine_zenodo",
                "ZenodoSearchEngine",
            ),
            (
                "stackexchange",
                ".engines.search_engine_stackexchange",
                "StackExchangeSearchEngine",
            ),
            (
                "pubchem",
                ".engines.search_engine_pubchem",
                "PubChemSearchEngine",
            ),
        ],
    )
    def test_factory_creates_engine(self, engine_key, module_path, class_name):
        """Engine is created via the factory without SecurityError.

        If this fails with ``engine is None``, the most likely cause is a
        missing entry in ``module_whitelist.py`` (ALLOWED_MODULE_PATHS or
        ALLOWED_CLASS_NAMES).
        """
        snapshot = _make_snapshot(engine_key, module_path, class_name)
        engine = create_search_engine(
            engine_key,
            settings_snapshot=snapshot,
            programmatic_mode=True,
        )
        assert engine is not None, (
            f"create_search_engine returned None for '{engine_key}'. "
            f"Check that {module_path} and {class_name} are in "
            f"security/module_whitelist.py"
        )
        assert isinstance(engine, BaseSearchEngine)
