"""Expanded tests for metadata_extractor — covers format functions and edge cases
not reached by the existing test_metadata_extractor.py.

Targets: _format_software (JSON-LD), _format_generic, _format_article with
list authors, _format_product with list offers, _has_type with list @type,
_format_opengraph with price tags, extruct ImportError path.
"""

from unittest.mock import MagicMock


from local_deep_research.research_library.downloaders.extraction.metadata_extractor import (
    _format_article,
    _format_generic,
    _format_opengraph,
    _format_product,
    _format_software,
    _has_type,
    extract_metadata,
    metadata_to_text,
)


# ---------------------------------------------------------------------------
# _has_type
# ---------------------------------------------------------------------------


class TestHasType:
    def test_simple_match(self):
        assert _has_type([{"@type": "Product"}], "Product") is True

    def test_no_match(self):
        assert _has_type([{"@type": "Article"}], "Product") is False

    def test_list_type_match(self):
        assert _has_type([{"@type": ["Product", "Thing"]}], "Product") is True

    def test_list_type_no_match(self):
        assert _has_type([{"@type": ["Article", "Thing"]}], "Product") is False

    def test_empty_list(self):
        assert _has_type([], "Product") is False

    def test_missing_type_key(self):
        assert _has_type([{"name": "Widget"}], "Product") is False

    def test_empty_type_list(self):
        assert _has_type([{"@type": []}], "Product") is False


# ---------------------------------------------------------------------------
# _format_product — edge cases
# ---------------------------------------------------------------------------


class TestFormatProduct:
    def test_list_offers(self):
        item = {
            "name": "Widget",
            "offers": [
                {"price": "9.99", "priceCurrency": "EUR"},
                {"price": "19.99", "priceCurrency": "USD"},
            ],
        }
        parts = _format_product(item)
        text = "\n".join(parts)
        assert "9.99" in text  # Uses first offer
        assert "EUR" in text

    def test_empty_offers(self):
        item = {"name": "Widget", "offers": []}
        parts = _format_product(item)
        assert any("Widget" in p for p in parts)

    def test_brand_as_string(self):
        item = {"name": "Widget", "brand": "TestBrand"}
        parts = _format_product(item)
        text = "\n".join(parts)
        assert "TestBrand" in text

    def test_no_name(self):
        item = {"description": "A very useful widget for testing purposes"}
        parts = _format_product(item)
        assert any("widget" in p.lower() for p in parts)

    def test_short_description_skipped(self):
        item = {"name": "Widget", "description": "Short"}
        parts = _format_product(item)
        assert not any("Description" in p for p in parts)

    def test_price_without_currency(self):
        item = {"name": "Widget", "offers": {"price": "29.99"}}
        parts = _format_product(item)
        text = "\n".join(parts)
        assert "29.99" in text

    def test_availability(self):
        item = {
            "name": "Widget",
            "offers": {"price": "10", "availability": "InStock"},
        }
        parts = _format_product(item)
        text = "\n".join(parts)
        assert "InStock" in text

    def test_rating_with_rating_count(self):
        item = {
            "name": "Widget",
            "aggregateRating": {"ratingValue": "3.5", "ratingCount": "42"},
        }
        parts = _format_product(item)
        text = "\n".join(parts)
        assert "3.5" in text
        assert "42" in text

    def test_non_dict_offers_ignored(self):
        item = {"name": "Widget", "offers": "see website"}
        parts = _format_product(item)
        # Should not crash, just skip offers
        assert any("Widget" in p for p in parts)


# ---------------------------------------------------------------------------
# _format_article — edge cases
# ---------------------------------------------------------------------------


class TestFormatArticle:
    def test_list_authors(self):
        item = {
            "headline": "Breaking News",
            "author": [
                {"name": "Alice"},
                {"name": "Bob"},
            ],
        }
        parts = _format_article(item)
        text = "\n".join(parts)
        assert "Alice" in text
        assert "Bob" in text

    def test_author_list_with_strings(self):
        item = {
            "headline": "Story",
            "author": [
                {"name": "Alice"},
                "Bob Smith",
            ],
        }
        parts = _format_article(item)
        text = "\n".join(parts)
        assert "Alice" in text
        assert "Bob Smith" in text

    def test_string_author(self):
        item = {"headline": "Story", "author": "Jane Doe"}
        parts = _format_article(item)
        text = "\n".join(parts)
        assert "Jane Doe" in text

    def test_name_fallback_when_no_headline(self):
        item = {"name": "The Article Title"}
        parts = _format_article(item)
        text = "\n".join(parts)
        assert "The Article Title" in text

    def test_article_body_included(self):
        body = "A " * 100  # > 50 chars
        item = {"headline": "Story", "articleBody": body}
        parts = _format_article(item)
        text = "\n".join(parts)
        assert "Content:" in text

    def test_short_article_body_skipped(self):
        item = {"headline": "Story", "articleBody": "Short"}
        parts = _format_article(item)
        assert not any("Content:" in p for p in parts)

    def test_article_body_truncated(self):
        body = "X" * 5000
        item = {"headline": "Story", "articleBody": body}
        parts = _format_article(item)
        content_parts = [p for p in parts if "Content:" in p]
        assert len(content_parts) == 1
        assert len(content_parts[0]) <= 2100  # "Content: " + 2000


# ---------------------------------------------------------------------------
# _format_software
# ---------------------------------------------------------------------------


class TestFormatSoftware:
    def test_basic(self):
        item = {
            "name": "my-repo",
            "author": {"name": "dev123"},
            "description": "A cool project",
        }
        parts = _format_software(item)
        text = "\n".join(parts)
        assert "my-repo" in text
        assert "dev123" in text
        assert "A cool project" in text

    def test_list_authors(self):
        item = {
            "name": "my-repo",
            "author": [{"name": "Alice"}, {"name": "Bob"}],
        }
        parts = _format_software(item)
        text = "\n".join(parts)
        assert "Alice" in text
        assert "Bob" in text

    def test_text_field(self):
        long_text = "Readme content " * 20
        item = {"name": "my-repo", "text": long_text}
        parts = _format_software(item)
        text = "\n".join(parts)
        assert "Content:" in text

    def test_short_text_skipped(self):
        item = {"name": "my-repo", "text": "Short"}
        parts = _format_software(item)
        assert not any("Content:" in p for p in parts)

    def test_string_author(self):
        item = {"name": "my-repo", "author": "contributor"}
        parts = _format_software(item)
        text = "\n".join(parts)
        assert "contributor" in text


# ---------------------------------------------------------------------------
# _format_generic
# ---------------------------------------------------------------------------


class TestFormatGeneric:
    def test_dataset(self):
        item = {
            "name": "My Dataset",
            "description": "A comprehensive dataset for testing machine learning models",
        }
        parts = _format_generic(item)
        text = "\n".join(parts)
        assert "My Dataset" in text
        assert "comprehensive" in text

    def test_headline_fallback(self):
        item = {"headline": "Headline Title"}
        parts = _format_generic(item)
        text = "\n".join(parts)
        assert "Headline Title" in text

    def test_short_description_skipped(self):
        item = {"name": "Test", "description": "Short"}
        parts = _format_generic(item)
        assert not any("Description" in p for p in parts)

    def test_empty_item(self):
        parts = _format_generic({})
        assert parts == []


# ---------------------------------------------------------------------------
# _format_opengraph
# ---------------------------------------------------------------------------


class TestFormatOpengraph:
    def test_with_site_name(self):
        item = {
            "og:title": "Product Name",
            "og:site_name": "ShopSite",
            "og:description": "A really great product for your needs",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        assert "ShopSite" in text
        assert "Product Name" in text

    def test_generic_og_type_not_used_as_prefix(self):
        item = {
            "og:title": "My Page",
            "og:type": "website",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        assert text == "My Page"

    def test_specific_og_type_used_as_prefix(self):
        item = {
            "og:title": "My Video",
            "@type": "video.movie",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        assert "video.movie" in text

    def test_price_tags(self):
        item = {
            "og:title": "Widget",
            "product:price:amount": "49.99",
            "product:price:currency": "USD",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        assert "49.99" in text
        assert "USD" in text

    def test_og_price_tags(self):
        item = {
            "og:title": "Widget",
            "og:price:amount": "29.99",
            "og:price:currency": "EUR",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        assert "29.99" in text

    def test_short_description_skipped(self):
        item = {"og:title": "Test", "og:description": "Short"}
        parts = _format_opengraph(item)
        assert not any("Description" in p for p in parts)

    def test_empty_item(self):
        parts = _format_opengraph({})
        assert parts == []


# ---------------------------------------------------------------------------
# metadata_to_text — complex routing
# ---------------------------------------------------------------------------


class TestMetadataToTextRouting:
    def test_software_in_json_ld(self):
        metadata = {
            "json_ld": [{"@type": "SoftwareSourceCode", "name": "cool-lib"}],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "cool-lib" in result

    def test_dataset_in_json_ld(self):
        metadata = {
            "json_ld": [
                {
                    "@type": "Dataset",
                    "name": "ML Dataset",
                    "description": "A comprehensive ML benchmark dataset",
                }
            ],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "ML Dataset" in result

    def test_creative_work_in_json_ld(self):
        metadata = {
            "json_ld": [
                {
                    "@type": "CreativeWork",
                    "name": "My Work",
                    "description": "A really interesting creative work piece",
                }
            ],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "My Work" in result

    def test_news_article_type(self):
        metadata = {
            "json_ld": [
                {
                    "@type": "NewsArticle",
                    "headline": "Big News",
                    "author": "Reporter",
                }
            ],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "Big News" in result

    def test_blog_posting_type(self):
        metadata = {
            "json_ld": [{"@type": "BlogPosting", "headline": "My Post"}],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "My Post" in result

    def test_scholarly_article_type(self):
        metadata = {
            "json_ld": [
                {"@type": "ScholarlyArticle", "headline": "Research Paper"}
            ],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "Research Paper" in result

    def test_list_type_in_json_ld(self):
        metadata = {
            "json_ld": [
                {"@type": ["Product", "Thing"], "name": "Multi-type Widget"}
            ],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "Multi-type Widget" in result

    def test_microdata_product_not_duplicated(self):
        """Product in both JSON-LD and microdata — only JSON-LD should be used."""
        metadata = {
            "json_ld": [{"@type": "Product", "name": "JSON Widget"}],
            "opengraph": [],
            "microdata": [{"@type": "Product", "name": "Micro Widget"}],
        }
        result = metadata_to_text(metadata)
        assert "JSON Widget" in result
        assert "Micro Widget" not in result

    def test_microdata_product_when_no_json_ld(self):
        metadata = {
            "json_ld": [],
            "opengraph": [],
            "microdata": [{"@type": "Product", "name": "Micro Only Widget"}],
        }
        result = metadata_to_text(metadata)
        assert "Micro Only Widget" in result

    def test_microdata_software_not_duplicated(self):
        metadata = {
            "json_ld": [{"@type": "SoftwareSourceCode", "name": "json-lib"}],
            "opengraph": [],
            "microdata": [{"@type": "SoftwareSourceCode", "name": "micro-lib"}],
        }
        result = metadata_to_text(metadata)
        assert "json-lib" in result
        assert "micro-lib" not in result

    def test_opengraph_only_first_block(self):
        metadata = {
            "json_ld": [],
            "opengraph": [
                {
                    "og:title": "First Block",
                    "og:description": "A sufficiently long first block description",
                },
                {
                    "og:title": "Second Block",
                    "og:description": "A sufficiently long second block description",
                },
            ],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert "First Block" in result
        assert "Second Block" not in result

    def test_unknown_json_ld_type_ignored(self):
        metadata = {
            "json_ld": [{"@type": "UnknownType", "name": "Something"}],
            "opengraph": [],
            "microdata": [],
        }
        result = metadata_to_text(metadata)
        assert result is None


# ---------------------------------------------------------------------------
# extract_metadata — edge cases
# ---------------------------------------------------------------------------


class TestExtractMetadataEdgeCases:
    def test_none_html(self):
        result = extract_metadata(None)
        assert result["json_ld"] == []

    def test_whitespace_only(self):
        result = extract_metadata("   ")
        assert result["json_ld"] == []

    def test_extruct_import_error(self, monkeypatch):
        import sys

        monkeypatch.delitem(sys.modules, "extruct", raising=False)

        original_import = __import__

        def fake_import(name, *args, **kwargs):
            if name == "extruct":
                raise ImportError("no extruct")
            return original_import(name, *args, **kwargs)

        monkeypatch.setattr("builtins.__import__", fake_import)
        result = extract_metadata("<html><body>test</body></html>")
        assert result["json_ld"] == []
        assert result["opengraph"] == []

    def test_extruct_extraction_exception(self, monkeypatch):
        mock_extruct = MagicMock()
        mock_extruct.extract.side_effect = RuntimeError("parse failed")
        monkeypatch.setitem(__import__("sys").modules, "extruct", mock_extruct)

        result = extract_metadata("<html><body>test</body></html>")
        assert result["json_ld"] == []


class TestFieldPreferenceFallbacks:
    """Several formatter functions check a preferred field first, then
    fall back to an alternative. The existing tests covered the
    fallback paths; this class pins the *preferred* paths so reversing
    the priority would surface a regression.
    """

    def test_format_product_rating_prefers_review_count(self):
        """``aggregateRating`` lookup tries ``reviewCount`` before
        ``ratingCount``. If both are present, the review count must win
        — pinning the order documented at ``_format_product`` where
        ``rating.get("reviewCount", rating.get("ratingCount", ""))`` is
        used.
        """
        from local_deep_research.research_library.downloaders.extraction.metadata_extractor import (
            _format_product,
        )

        item = {
            "name": "Widget",
            "aggregateRating": {
                "ratingValue": "4.2",
                "reviewCount": "987",
                "ratingCount": "1",
            },
        }
        parts = _format_product(item)
        text = "\n".join(parts)
        # reviewCount wins; ratingCount must NOT be the visible number.
        assert "987" in text
        assert "(1 reviews)" not in text

    def test_format_opengraph_price_prefers_product_namespace(self):
        """``_format_opengraph`` checks ``product:price:amount`` before
        ``og:price:amount`` (similarly for currency). If both are
        present, the product-namespace value wins — pinning the lookup
        order.
        """
        from local_deep_research.research_library.downloaders.extraction.metadata_extractor import (
            _format_opengraph,
        )

        item = {
            "og:title": "Widget Page",
            "product:price:amount": "29.99",
            "product:price:currency": "EUR",
            "og:price:amount": "999.00",
            "og:price:currency": "JPY",
        }
        parts = _format_opengraph(item)
        text = "\n".join(parts)
        # product:price wins; og:price values must NOT appear.
        assert "29.99" in text
        assert "EUR" in text
        assert "999.00" not in text
        assert "JPY" not in text


class TestMetadataToTextEmptyTypeList:
    """``metadata_to_text`` extracts the dispatch type from each JSON-LD
    or microdata item via ``item_type[0] if item_type else ""`` for list
    forms. An empty-list ``@type`` must collapse to ``""`` and fall
    through every branch cleanly — no IndexError, no formatting.
    """

    def test_empty_type_list_in_json_ld_is_ignored(self):
        from local_deep_research.research_library.downloaders.extraction.metadata_extractor import (
            metadata_to_text,
        )

        metadata = {
            "json_ld": [
                {
                    "@type": [],  # empty list — collapses to ""
                    "name": "Should-not-appear",
                    "headline": "Should-also-not-appear",
                }
            ],
            "opengraph": [],
            "microdata": [],
        }
        # No matching branch → returns None (no useful metadata).
        assert metadata_to_text(metadata) is None

    def test_empty_type_list_in_microdata_is_ignored(self):
        from local_deep_research.research_library.downloaders.extraction.metadata_extractor import (
            metadata_to_text,
        )

        metadata = {
            "json_ld": [],
            "opengraph": [],
            "microdata": [
                {
                    "@type": [],
                    "name": "Should-not-appear",
                }
            ],
        }
        assert metadata_to_text(metadata) is None
