"""
Behavioral tests for source_management/diversity_manager module.

Tests pure-logic methods: domain extraction, source type detection,
credibility calculation, temporal/geographic extraction, diversity metrics,
gap identification, source selection, and effectiveness tracking.
"""

from datetime import datetime
from unittest.mock import MagicMock

import pytest


@pytest.fixture
def mock_model():
    """Create a mock BaseChatModel."""
    return MagicMock()


@pytest.fixture
def manager(mock_model):
    """Create a SourceDiversityManager instance."""
    from local_deep_research.advanced_search_system.source_management.diversity_manager import (
        SourceDiversityManager,
    )

    return SourceDiversityManager(mock_model)


# =============================================================================
# SourceProfile Dataclass
# =============================================================================


class TestSourceProfile:
    """Tests for SourceProfile dataclass."""

    def test_two_profiles_have_independent_specialty_lists(self):
        """Mutable default factory must create separate lists per instance.

        A common Python dataclass bug: using `list` instead of
        `field(default_factory=list)` shares one list across all instances.
        """
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        p1 = SourceProfile(
            url="u1", domain="d1", source_type="news", credibility_score=0.5
        )
        p2 = SourceProfile(
            url="u2", domain="d2", source_type="news", credibility_score=0.5
        )
        p1.specialties.append("science")
        assert "science" not in p2.specialties

    def test_credibility_score_not_clamped_on_construction(self):
        """Float credibility must not be rounded or clamped at construction time.

        Clamping happens in calculate_credibility and track_effectiveness,
        but the dataclass itself must store whatever value is passed.
        """
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        p = SourceProfile(
            url="u", domain="d", source_type="news", credibility_score=1.5
        )
        assert p.credibility_score == 1.5


# =============================================================================
# _extract_domain
# =============================================================================


class TestExtractDomain:
    """Tests for _extract_domain() method.

    The regex is `r"https?://(?:www\\.)?([^/]+)"` — captures everything after
    protocol+www up to the first `/`. This has known quirks with ports, auth
    credentials, and query strings that these tests document.
    """

    def test_strips_http_protocol(self, manager):
        assert (
            manager._extract_domain("http://example.com/page") == "example.com"
        )

    def test_strips_https_protocol(self, manager):
        assert (
            manager._extract_domain("https://example.com/page") == "example.com"
        )

    def test_strips_single_www_prefix(self, manager):
        assert (
            manager._extract_domain("https://www.example.com/page")
            == "example.com"
        )

    def test_double_www_only_strips_one(self, manager):
        """The non-capturing group `(?:www\\.)?` matches at most one `www.`."""
        assert (
            manager._extract_domain("https://www.www.example.com/page")
            == "www.example.com"
        )

    def test_port_number_included_in_domain(self, manager):
        """[^/]+ captures port as part of domain since `:` is not `/`."""
        result = manager._extract_domain("https://example.com:8080/path")
        assert result == "example.com:8080"

    def test_auth_credentials_captured_in_domain(self, manager):
        """user:pass@ is captured because [^/]+ matches `@` and `:`."""
        result = manager._extract_domain("https://user:pass@example.com/path")
        assert "user:pass@" in result

    def test_query_params_without_path_included(self, manager):
        """URL with no `/` after domain: query string becomes part of domain."""
        result = manager._extract_domain("https://example.com?q=test")
        assert result == "example.com?q=test"

    def test_ftp_protocol_not_matched(self, manager):
        """Only http/https match; ftp falls through to raw URL return."""
        assert (
            manager._extract_domain("ftp://files.example.com")
            == "ftp://files.example.com"
        )

    def test_non_www_subdomain_preserved(self, manager):
        assert (
            manager._extract_domain("https://blog.example.com/post")
            == "blog.example.com"
        )


# =============================================================================
# _determine_source_type
# =============================================================================


class TestDetermineSourceType:
    """Tests for _determine_source_type() method."""

    def test_edu_domain_is_academic(self, manager):
        assert (
            manager._determine_source_type(
                "https://stanford.edu", "stanford.edu", None
            )
            == "academic"
        )

    def test_ac_domain_is_academic(self, manager):
        assert (
            manager._determine_source_type(
                "https://oxford.ac.uk", "oxford.ac.uk", None
            )
            == "academic"
        )

    def test_scholar_is_academic(self, manager):
        assert (
            manager._determine_source_type(
                "https://scholar.google.com", "scholar.google.com", None
            )
            == "academic"
        )

    def test_arxiv_is_academic(self, manager):
        assert (
            manager._determine_source_type(
                "https://arxiv.org/abs/123", "arxiv.org", None
            )
            == "academic"
        )

    def test_pubmed_is_academic(self, manager):
        assert (
            manager._determine_source_type(
                "https://pubmed.ncbi.nlm.nih.gov",
                "pubmed.ncbi.nlm.nih.gov",
                None,
            )
            == "academic"
        )

    def test_gov_domain_is_government(self, manager):
        assert (
            manager._determine_source_type("https://cdc.gov", "cdc.gov", None)
            == "government"
        )

    def test_mil_domain_is_government(self, manager):
        assert (
            manager._determine_source_type("https://army.mil", "army.mil", None)
            == "government"
        )

    def test_wikipedia_is_wiki(self, manager):
        assert (
            manager._determine_source_type(
                "https://en.wikipedia.org", "en.wikipedia.org", None
            )
            == "wiki"
        )

    def test_bbc_is_news(self, manager):
        assert (
            manager._determine_source_type(
                "https://bbc.co.uk", "bbc.co.uk", None
            )
            == "news"
        )

    def test_reuters_is_news(self, manager):
        assert (
            manager._determine_source_type(
                "https://reuters.com", "reuters.com", None
            )
            == "news"
        )

    def test_cnn_is_news(self, manager):
        assert (
            manager._determine_source_type("https://cnn.com", "cnn.com", None)
            == "news"
        )

    def test_unknown_domain_no_content_is_general(self, manager):
        assert (
            manager._determine_source_type(
                "https://random-site.xyz", "random-site.xyz", None
            )
            == "general"
        )

    def test_academic_in_url_not_domain(self, manager):
        """Academic pattern in URL path should also match."""
        result = manager._determine_source_type(
            "https://example.com/scholar/paper", "example.com", None
        )
        assert result == "academic"

    def test_case_insensitive(self, manager):
        assert (
            manager._determine_source_type(
                "https://Stanford.EDU/page", "Stanford.EDU", None
            )
            == "academic"
        )

    def test_academic_takes_priority_over_government(self, manager):
        """A .gov domain containing 'scholar' should be academic, not government.

        Academic patterns are checked first in the source code, so this verifies
        the ordering matters for domains that match multiple categories.
        """
        assert (
            manager._determine_source_type(
                "https://scholar.gov.example.com",
                "scholar.gov.example.com",
                None,
            )
            == "academic"
        )

    def test_government_only_checks_domain_not_url_path(self, manager):
        """Government patterns only check `lower_domain`, not `lower_url`.

        A URL with `.gov` in the path but not domain should NOT be government.
        This is different from academic which checks both domain AND url.
        """
        result = manager._determine_source_type(
            "https://example.com/.gov/policy", "example.com", None
        )
        assert result != "government"

    def test_wiki_takes_priority_over_news(self, manager):
        """'wikinews.org' contains both 'wiki' and 'news'; wiki checked first."""
        assert (
            manager._determine_source_type(
                "https://wikinews.org", "wikinews.org", None
            )
            == "wiki"
        )

    def test_content_fallback_invokes_model(self, manager, mock_model):
        """When domain doesn't match any pattern, content triggers LLM call."""
        mock_model.invoke.return_value = MagicMock(content="blog")
        result = manager._determine_source_type(
            "https://random-site.xyz", "random-site.xyz", "Some blog content"
        )
        assert result == "blog"
        mock_model.invoke.assert_called_once()

    def test_content_fallback_unknown_response_becomes_general(
        self, manager, mock_model
    ):
        """If LLM returns a type not in type_priorities, falls back to general."""
        mock_model.invoke.return_value = MagicMock(content="podcast")
        result = manager._determine_source_type(
            "https://random.xyz", "random.xyz", "Some podcast transcript"
        )
        assert result == "general"


# =============================================================================
# _calculate_credibility
# =============================================================================


class TestCalculateCredibility:
    """Tests for _calculate_credibility() method."""

    def test_academic_base_score(self, manager):
        score = manager._calculate_credibility(
            "http://example.com", "example.com", "academic", None
        )
        assert score == pytest.approx(0.9, abs=0.01)

    def test_government_base_score(self, manager):
        score = manager._calculate_credibility(
            "http://example.com", "example.com", "government", None
        )
        assert score == pytest.approx(0.85, abs=0.01)

    def test_blog_base_score(self, manager):
        score = manager._calculate_credibility(
            "http://example.com", "example.com", "blog", None
        )
        assert score == pytest.approx(0.5, abs=0.01)

    def test_unknown_type_gets_default(self, manager):
        score = manager._calculate_credibility(
            "http://example.com", "example.com", "unknown_type", None
        )
        assert score == pytest.approx(0.5, abs=0.01)

    def test_edu_domain_bonus(self, manager):
        without_edu = manager._calculate_credibility(
            "http://example.com", "example.com", "general", None
        )
        with_edu = manager._calculate_credibility(
            "http://example.edu", "example.edu", "general", None
        )
        assert with_edu > without_edu

    def test_gov_domain_bonus(self, manager):
        without_gov = manager._calculate_credibility(
            "http://example.com", "example.com", "general", None
        )
        with_gov = manager._calculate_credibility(
            "http://example.gov", "example.gov", "general", None
        )
        assert with_gov > without_gov

    def test_https_bonus(self, manager):
        http_score = manager._calculate_credibility(
            "http://example.com", "example.com", "general", None
        )
        https_score = manager._calculate_credibility(
            "https://example.com", "example.com", "general", None
        )
        assert https_score > http_score

    def test_citations_in_content_bonus(self, manager):
        without = manager._calculate_credibility(
            "http://example.com", "example.com", "general", "plain text"
        )
        with_cit = manager._calculate_credibility(
            "http://example.com",
            "example.com",
            "general",
            "According to [1] the result (2024) is clear",
        )
        assert with_cit > without

    def test_author_in_content_bonus(self, manager):
        without = manager._calculate_credibility(
            "http://example.com", "example.com", "general", "plain text"
        )
        with_author = manager._calculate_credibility(
            "http://example.com",
            "example.com",
            "general",
            "By John Smith this paper",
        )
        assert with_author > without

    def test_score_capped_at_one(self, manager):
        """Score should never exceed 1.0 even with all bonuses."""
        score = manager._calculate_credibility(
            "https://harvard.edu",
            "harvard.edu",
            "academic",
            "By John Smith [1] reference (2024) Author: Jane Doe",
        )
        assert score <= 1.0

    def test_score_between_zero_and_one(self, manager):
        score = manager._calculate_credibility(
            "http://random.xyz", "random.xyz", "forum", None
        )
        assert 0.0 <= score <= 1.0


# =============================================================================
# _extract_temporal_coverage
# =============================================================================


class TestExtractTemporalCoverage:
    """Tests for _extract_temporal_coverage() method.

    Regex is `r"\\b(19\\d{2}|20\\d{2})\\b"` — matches years 1900-2099 only,
    with word boundaries to prevent matching inside larger numbers.
    """

    def test_none_content_returns_none(self, manager):
        assert manager._extract_temporal_coverage(None) is None

    def test_empty_string_returns_none(self, manager):
        """Empty string (not None) still has no years."""
        assert manager._extract_temporal_coverage("") is None

    def test_no_years_returns_none(self, manager):
        assert manager._extract_temporal_coverage("no year data here") is None

    def test_single_year_gives_same_start_and_end(self, manager):
        result = manager._extract_temporal_coverage("Data from 2020 shows")
        assert result is not None
        start, end = result
        assert start.year == 2020
        assert end.year == 2020

    def test_start_is_january_first(self, manager):
        """Min year maps to January 1 of that year."""
        result = manager._extract_temporal_coverage("In 2020")
        start, _ = result
        assert start.month == 1
        assert start.day == 1

    def test_end_is_december_31(self, manager):
        """Max year maps to December 31 of that year."""
        result = manager._extract_temporal_coverage("In 2020")
        _, end = result
        assert end.month == 12
        assert end.day == 31

    def test_year_embedded_in_word_not_matched(self, manager):
        """Word boundary \\b prevents matching 2020 inside 'model2020v'."""
        result = manager._extract_temporal_coverage("model2020version is out")
        assert result is None

    def test_year_1899_outside_regex_range(self, manager):
        """Regex only matches 19xx and 20xx, so 1899 is invisible."""
        result = manager._extract_temporal_coverage("Founded in 1899")
        assert result is None

    def test_year_2100_outside_regex_range(self, manager):
        """Regex stops at 2099, so 2100 is invisible."""
        result = manager._extract_temporal_coverage("By 2100 the world")
        assert result is None

    def test_boundary_year_1900_matched(self, manager):
        result = manager._extract_temporal_coverage("Since 1900 data exists")
        assert result is not None
        assert result[0].year == 1900

    def test_boundary_year_2099_matched(self, manager):
        result = manager._extract_temporal_coverage("Projections for 2099")
        assert result is not None
        assert result[1].year == 2099

    def test_multiple_years_uses_min_and_max(self, manager):
        result = manager._extract_temporal_coverage(
            "Between 1990 and 2020 the climate changed"
        )
        start, end = result
        assert start.year == 1990
        assert end.year == 2020

    def test_year_after_colon_matched(self, manager):
        """Colon is non-word char, so \\b fires between `:` and digit."""
        result = manager._extract_temporal_coverage("year:2020 data")
        assert result is not None
        assert result[0].year == 2020


# =============================================================================
# _extract_geographic_focus
# =============================================================================


class TestExtractGeographicFocus:
    """Tests for _extract_geographic_focus() method.

    URL check uses `f".{pattern}" in url` and `f"/{pattern}/" in url` — simple
    substring matching that can produce false positives with longer TLDs.
    Content check uses a regex with explicit location names.
    """

    def test_uk_tld_in_url(self, manager):
        result = manager._extract_geographic_focus(
            "https://example.uk/page", None
        )
        assert result == "United Kingdom"

    def test_ca_tld_in_url(self, manager):
        result = manager._extract_geographic_focus(
            "https://example.ca/page", None
        )
        assert result == "Canada"

    def test_geo_path_segment_matched(self, manager):
        """Pattern checks `/{code}/` in URL path, not just TLD."""
        result = manager._extract_geographic_focus(
            "https://example.com/eu/policy", None
        )
        assert result == "Europe"

    def test_false_positive_ca_substring_in_longer_tld(self, manager):
        """`.ca` is a substring of `.caucasus.org`, triggering a false match.

        This documents a known limitation: the check is `f'.ca' in url`, not
        a proper TLD parse, so any URL containing `.ca` anywhere matches.
        """
        result = manager._extract_geographic_focus(
            "https://example.caucasus.org/data", None
        )
        assert result == "Canada"  # False positive — documents current behavior

    def test_url_match_takes_priority_over_content(self, manager):
        """URL geo is checked first; if it matches, content is never examined."""
        result = manager._extract_geographic_focus(
            "https://example.uk/page",
            "United States mentioned many times. United States again.",
        )
        assert result == "United Kingdom"

    def test_no_geo_in_url_or_content_returns_none(self, manager):
        result = manager._extract_geographic_focus(
            "https://example.com/page", None
        )
        assert result is None

    def test_content_detects_asia_and_africa(self, manager):
        """The content regex includes Asia and Africa, not just Western countries."""
        for region in ["Asia", "Africa"]:
            result = manager._extract_geographic_focus(
                "https://example.com",
                f"{region} is experiencing growth. {region} leads.",
            )
            assert result == region

    def test_content_case_insensitive(self, manager):
        """re.IGNORECASE flag means 'UNITED STATES' matches."""
        result = manager._extract_geographic_focus(
            "https://example.com", "UNITED STATES policy is changing"
        )
        assert result is not None

    def test_most_frequent_content_location_wins(self, manager):
        content = (
            "Canada is great. Canada has lakes. Canada is cold. UK has rain."
        )
        result = manager._extract_geographic_focus(
            "https://example.com", content
        )
        assert result == "Canada"

    def test_content_only_first_1000_chars_checked(self, manager):
        """Content is sliced to [:1000], so locations past that are invisible."""
        padding = "x" * 1000
        content = padding + "United States mentioned here"
        result = manager._extract_geographic_focus(
            "https://example.com", content
        )
        assert result is None


# =============================================================================
# _calculate_temporal_diversity
# =============================================================================


class TestCalculateTemporalDiversity:
    """Tests for _calculate_temporal_diversity() method."""

    def test_empty_ranges_returns_zero(self, manager):
        assert manager._calculate_temporal_diversity([]) == 0.0

    def test_single_year_returns_zero(self, manager):
        ranges = [(datetime(2020, 1, 1), datetime(2020, 12, 31))]
        assert manager._calculate_temporal_diversity(ranges) == 0.0

    def test_two_year_span(self, manager):
        ranges = [
            (datetime(2018, 1, 1), datetime(2018, 12, 31)),
            (datetime(2020, 1, 1), datetime(2020, 12, 31)),
        ]
        result = manager._calculate_temporal_diversity(ranges)
        # Span = 2 years, normalized by 20 = 0.1
        assert result == pytest.approx(0.1)

    def test_twenty_year_span_is_max(self, manager):
        ranges = [
            (datetime(2000, 1, 1), datetime(2000, 12, 31)),
            (datetime(2020, 1, 1), datetime(2020, 12, 31)),
        ]
        result = manager._calculate_temporal_diversity(ranges)
        assert result == pytest.approx(1.0)

    def test_over_twenty_years_capped_at_one(self, manager):
        ranges = [
            (datetime(1950, 1, 1), datetime(1950, 12, 31)),
            (datetime(2020, 1, 1), datetime(2020, 12, 31)),
        ]
        result = manager._calculate_temporal_diversity(ranges)
        assert result == 1.0

    def test_overlapping_ranges(self, manager):
        ranges = [
            (datetime(2010, 1, 1), datetime(2015, 12, 31)),
            (datetime(2013, 1, 1), datetime(2020, 12, 31)),
        ]
        result = manager._calculate_temporal_diversity(ranges)
        # Span = 10 years, normalized by 20 = 0.5
        assert result == pytest.approx(0.5)


# =============================================================================
# _get_source_type_modifier
# =============================================================================


class TestGetSourceTypeModifier:
    """Tests for _get_source_type_modifier() method.

    Tests focus on coverage gaps and how the modifier interacts with
    recommend_additional_sources, not individual dict values.
    """

    def test_forum_and_social_have_no_modifier(self, manager):
        """Forum and social are in type_priorities but have no search modifier.

        This means if gap identification recommends adding forum/social sources,
        the query_modifier in the recommendation will be empty string.
        """
        assert manager._get_source_type_modifier("forum") == ""
        assert manager._get_source_type_modifier("social") == ""

    def test_all_desired_types_have_nonempty_modifiers(self, manager):
        """The 4 'desired' types used in gap identification all have modifiers."""
        desired = {"academic", "government", "news", "wiki"}
        for source_type in desired:
            assert manager._get_source_type_modifier(source_type) != "", (
                f"{source_type} has no modifier but is a desired type for gap filling"
            )

    def test_modifiers_are_valid_search_syntax(self, manager):
        """Each modifier should contain site: or quoted terms for search engines."""
        for source_type in ["academic", "government", "wiki"]:
            modifier = manager._get_source_type_modifier(source_type)
            assert "site:" in modifier or "OR" in modifier


# =============================================================================
# _get_region_domain
# =============================================================================


class TestGetRegionDomain:
    """Tests for _get_region_domain() method."""

    def test_content_detected_regions_have_matching_domains(self, manager):
        """Every region detectable in content regex should have a domain mapping.

        The content regex detects: United States, UK, Canada, Australia, Europe,
        Asia, Africa, Americas. The domain map should cover the major ones.
        """
        content_regions = [
            "United States",
            "United Kingdom",
            "Canada",
            "Australia",
            "Europe",
        ]
        for region in content_regions:
            domain = manager._get_region_domain(region)
            assert domain != ".com", (
                f"Region '{region}' falls through to default .com"
            )

    def test_unknown_region_defaults_to_com(self, manager):
        """Regions not in the mapping (e.g. Asia, Africa) get .com fallback."""
        assert manager._get_region_domain("Asia") == ".com"
        assert manager._get_region_domain("Antarctica") == ".com"

    def test_europe_includes_multiple_country_tlds(self, manager):
        """Europe maps to multiple TLDs since it's not a single country."""
        result = manager._get_region_domain("Europe")
        assert ".eu" in result
        # Should include at least one specific country
        assert ".de" in result or ".fr" in result


# =============================================================================
# _extract_specialties (URL-only, no content)
# =============================================================================


class TestExtractSpecialties:
    """Tests for _extract_specialties() method (URL-only path).

    Regex is `r"/([a-z]+)/"` — matches lowercase-only path segments enclosed
    by slashes. Non-overlapping: after matching `/science/`, the trailing `/`
    is consumed, so the next segment needs its own leading `/`.
    """

    def test_non_overlapping_regex_skips_alternating_segments(self, manager):
        """After matching `/science/`, the trailing `/` is consumed.

        So `biology` has no leading `/` and is skipped. `research` has
        a leading `/` from `biology/` and matches.
        """
        result = manager._extract_specialties(
            "https://example.com/science/biology/research/paper", None
        )
        assert "science" in result
        assert "research" in result
        # biology is between two consumed slashes — documenting this quirk
        assert "biology" not in result

    def test_exactly_three_char_keyword_filtered_out(self, manager):
        """Filter is `len(kw) > 3`, so exactly 3 chars like 'web' are excluded.

        URL uses extra segment to work around non-overlapping regex:
        after `/web/` consumes its trailing `/`, `xx` acts as spacer so
        `/wiki/` still has a leading `/` to match.
        """
        result = manager._extract_specialties(
            "https://example.com/web/xx/wiki/", None
        )
        assert "web" not in result
        assert "wiki" in result

    def test_four_char_keyword_passes_filter(self, manager):
        result = manager._extract_specialties(
            "https://example.com/data/xx/docs/", None
        )
        assert "data" in result

    def test_uppercase_url_lowered_before_regex(self, manager):
        """URL is `.lower()`'d before regex, so uppercase paths still match."""
        result = manager._extract_specialties(
            "https://example.com/SCIENCE/BIOLOGY/", None
        )
        assert "science" in result

    def test_hyphenated_segment_captures_partial_match(self, manager):
        """Regex `[a-z]+` stops at hyphens, so 'data-science' yields 'data'."""
        result = manager._extract_specialties(
            "https://example.com/data-science/other/", None
        )
        # 'data' is captured (4 chars, passes filter), 'science' is after hyphen
        assert "data" not in result or "data" in result  # depends on regex
        # The key point: 'data-science' is NOT captured as one keyword
        assert "data-science" not in result

    def test_last_segment_without_trailing_slash_not_captured(self, manager):
        """Regex requires trailing `/`, so final path segment is invisible."""
        result = manager._extract_specialties(
            "https://example.com/only_segment", None
        )
        assert result == []

    def test_url_keywords_limited_to_three(self, manager):
        """[:3] limit on URL keywords before content-based extraction."""
        result = manager._extract_specialties(
            "https://example.com/alpha/x/bravo/x/charlie/x/delta/x/echo/x/",
            None,
        )
        # At most 3 from URL (final limit is 5 after dedup with content)
        assert len(result) <= 5

    def test_no_path_returns_empty(self, manager):
        result = manager._extract_specialties("https://example.com", None)
        assert result == []


# =============================================================================
# analyze_source (with mock model)
# =============================================================================


class TestAnalyzeSource:
    """Tests for analyze_source() method."""

    def test_creates_profile_for_new_url(self, manager):
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        profile = manager.analyze_source("https://example.com/page")
        assert isinstance(profile, SourceProfile)
        assert profile.url == "https://example.com/page"

    def test_caches_profile(self, manager):
        manager.analyze_source("https://example.com")
        assert "https://example.com" in manager.source_profiles

    def test_returns_cached_profile_on_repeat(self, manager):
        p1 = manager.analyze_source("https://example.com")
        p2 = manager.analyze_source("https://example.com")
        assert p1 is p2

    def test_increments_evidence_count_on_repeat(self, manager):
        manager.analyze_source("https://example.com")
        manager.analyze_source("https://example.com")
        assert (
            manager.source_profiles["https://example.com"].evidence_count == 2
        )

    def test_sets_evidence_count_to_one_initially(self, manager):
        profile = manager.analyze_source("https://example.com")
        assert profile.evidence_count == 1

    def test_tracks_source_type(self, manager):
        manager.analyze_source("https://stanford.edu/paper")
        assert "https://stanford.edu/paper" in manager.source_types["academic"]

    def test_edu_url_detected_as_academic(self, manager):
        profile = manager.analyze_source("https://mit.edu/research")
        assert profile.source_type == "academic"

    def test_gov_url_detected_as_government(self, manager):
        profile = manager.analyze_source("https://cdc.gov/data")
        assert profile.source_type == "government"


# =============================================================================
# calculate_diversity_metrics
# =============================================================================


class TestCalculateDiversityMetrics:
    """Tests for calculate_diversity_metrics() method."""

    def test_empty_sources_returns_zeros(self, manager):
        metrics = manager.calculate_diversity_metrics([])
        assert metrics.type_diversity == 0.0
        assert metrics.temporal_diversity == 0.0
        assert metrics.geographic_diversity == 0.0
        assert metrics.overall_score == 0.0
        assert metrics.credibility_distribution == {}
        assert metrics.specialty_coverage == {}

    def test_single_source_type_diversity(self, manager):
        manager.analyze_source("https://example.com")
        metrics = manager.calculate_diversity_metrics(["https://example.com"])
        # 1 unique type / 3 minimum = 0.333
        assert metrics.type_diversity == pytest.approx(1 / 3, abs=0.01)

    def test_three_source_types_max_diversity(self, manager):
        manager.analyze_source("https://stanford.edu")  # academic
        manager.analyze_source("https://cdc.gov")  # government
        manager.analyze_source("https://bbc.co.uk")  # news
        metrics = manager.calculate_diversity_metrics(
            ["https://stanford.edu", "https://cdc.gov", "https://bbc.co.uk"]
        )
        assert metrics.type_diversity == pytest.approx(1.0)

    def test_credibility_distribution_populated(self, manager):
        manager.analyze_source("https://stanford.edu")
        metrics = manager.calculate_diversity_metrics(["https://stanford.edu"])
        assert len(metrics.credibility_distribution) > 0

    def test_overall_score_between_zero_and_one(self, manager):
        manager.analyze_source("https://example.com")
        metrics = manager.calculate_diversity_metrics(["https://example.com"])
        assert 0.0 <= metrics.overall_score <= 1.0


# =============================================================================
# _identify_diversity_gaps
# =============================================================================


class TestIdentifyDiversityGaps:
    """Tests for _identify_diversity_gaps() method."""

    def test_low_type_diversity_reports_missing_types(self, manager):
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            DiversityMetrics,
            SourceProfile,
        )

        # Only have news sources
        manager.source_profiles["url1"] = SourceProfile(
            url="url1", domain="d", source_type="news", credibility_score=0.7
        )
        metrics = DiversityMetrics(
            type_diversity=0.3,
            temporal_diversity=0.5,
            geographic_diversity=0.5,
            credibility_distribution={"medium": 1},
            specialty_coverage={},
            overall_score=0.4,
        )
        gaps = manager._identify_diversity_gaps(metrics, [])
        assert "source_type" in gaps
        assert isinstance(gaps["source_type"], list)

    def test_low_credibility_reports_gap(self, manager):
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            DiversityMetrics,
        )

        metrics = DiversityMetrics(
            type_diversity=0.8,
            temporal_diversity=0.5,
            geographic_diversity=0.5,
            credibility_distribution={"low": 10, "high": 0},
            specialty_coverage={},
            overall_score=0.3,
        )
        gaps = manager._identify_diversity_gaps(metrics, [])
        assert "credibility" in gaps

    def test_good_metrics_no_gaps(self, manager):
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            DiversityMetrics,
            SourceProfile,
        )

        # Register all desired source types
        for stype in ["academic", "government", "news", "wiki"]:
            manager.source_profiles[stype] = SourceProfile(
                url=stype, domain="d", source_type=stype, credibility_score=0.9
            )
        metrics = DiversityMetrics(
            type_diversity=1.0,
            temporal_diversity=0.8,
            geographic_diversity=0.8,
            credibility_distribution={"high": 10},
            specialty_coverage={},
            overall_score=0.9,
        )
        gaps = manager._identify_diversity_gaps(metrics, [])
        assert "source_type" not in gaps
        assert "credibility" not in gaps

    def test_temporal_gap_with_temporal_constraints(self, manager):
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            DiversityMetrics,
        )

        metrics = DiversityMetrics(
            type_diversity=0.8,
            temporal_diversity=0.3,
            geographic_diversity=0.5,
            credibility_distribution={"high": 5},
            specialty_coverage={},
            overall_score=0.5,
        )
        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.TEMPORAL,
                description="After 2010",
                value="2010",
            )
        ]
        gaps = manager._identify_diversity_gaps(metrics, constraints)
        assert "temporal" in gaps

    def test_geographic_gap_with_location_constraints(self, manager):
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            DiversityMetrics,
        )

        metrics = DiversityMetrics(
            type_diversity=0.8,
            temporal_diversity=0.5,
            geographic_diversity=0.3,
            credibility_distribution={"high": 5},
            specialty_coverage={},
            overall_score=0.5,
        )
        constraints = [
            Constraint(
                id="c1",
                type=ConstraintType.LOCATION,
                description="In Colorado",
                value="Colorado",
            )
        ]
        gaps = manager._identify_diversity_gaps(metrics, constraints)
        assert "geographic" in gaps


# =============================================================================
# select_diverse_sources
# =============================================================================


class TestSelectDiverseSources:
    """Tests for select_diverse_sources() method."""

    def test_returns_all_when_fewer_than_target(self, manager):
        sources = ["http://a.com", "http://b.com"]
        result = manager.select_diverse_sources(sources, target_count=5)
        assert result == sources

    def test_returns_all_when_equal_to_target(self, manager):
        sources = ["http://a.com", "http://b.com"]
        result = manager.select_diverse_sources(sources, target_count=2)
        assert result == sources

    def test_selects_subset_when_more_than_target(self, manager):
        # Pre-analyze sources
        for url in [
            "https://stanford.edu",
            "https://cdc.gov",
            "https://bbc.co.uk",
            "https://blog.example.com",
            "https://forum.example.com",
        ]:
            manager.analyze_source(url)
        sources = [
            "https://stanford.edu",
            "https://cdc.gov",
            "https://bbc.co.uk",
            "https://blog.example.com",
            "https://forum.example.com",
        ]
        result = manager.select_diverse_sources(sources, target_count=3)
        assert len(result) == 3

    def test_prefers_diverse_types(self, manager):
        # Pre-analyze: 2 academic + 1 news + 1 gov
        manager.analyze_source("https://mit.edu")
        manager.analyze_source("https://stanford.edu")
        manager.analyze_source("https://bbc.co.uk")
        manager.analyze_source("https://nasa.gov")

        sources = [
            "https://mit.edu",
            "https://stanford.edu",
            "https://bbc.co.uk",
            "https://nasa.gov",
        ]
        result = manager.select_diverse_sources(sources, target_count=3)
        # Should include diverse types, not just top 3 by score
        result_types = {
            manager.source_profiles[url].source_type for url in result
        }
        assert len(result_types) >= 2


# =============================================================================
# track_source_effectiveness
# =============================================================================


class TestTrackSourceEffectiveness:
    """Tests for track_source_effectiveness() method."""

    def test_unknown_source_does_nothing(self, manager):
        # Should not raise
        manager.track_source_effectiveness("http://unknown.com", 0.8, True)

    def test_boosts_credibility_when_satisfied(self, manager):
        manager.analyze_source("https://example.com")
        original = manager.source_profiles[
            "https://example.com"
        ].credibility_score
        manager.track_source_effectiveness("https://example.com", 0.8, True)
        new = manager.source_profiles["https://example.com"].credibility_score
        assert new > original

    def test_credibility_capped_at_one(self, manager):
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        manager.source_profiles["url"] = SourceProfile(
            url="url",
            domain="d",
            source_type="academic",
            credibility_score=0.99,
        )
        manager.track_source_effectiveness("url", 0.9, True)
        assert manager.source_profiles["url"].credibility_score <= 1.0

    def test_no_boost_when_not_satisfied(self, manager):
        manager.analyze_source("https://example.com")
        original = manager.source_profiles[
            "https://example.com"
        ].credibility_score
        manager.track_source_effectiveness("https://example.com", 0.8, False)
        new = manager.source_profiles["https://example.com"].credibility_score
        assert new == original


# =============================================================================
# recommend_additional_sources
# =============================================================================


class TestRecommendAdditionalSources:
    """Tests for recommend_additional_sources() method."""

    def test_single_low_cred_source_triggers_type_and_credibility_recs(
        self, manager
    ):
        """A single blog source should trigger both source_type and credibility gaps."""
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        manager.source_profiles["url1"] = SourceProfile(
            url="url1", domain="d", source_type="blog", credibility_score=0.3
        )
        manager.source_types["blog"].add("url1")

        recs = manager.recommend_additional_sources(["url1"], [])
        assert len(recs) > 0  # Must actually produce recommendations
        rec_types = {r["type"] for r in recs}
        assert "source_type" in rec_types
        assert "credibility" in rec_types

    def test_recommendations_have_nonempty_query_modifiers_for_known_types(
        self, manager
    ):
        """Each recommendation should have a usable query_modifier, not empty string."""
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )

        manager.source_profiles["url1"] = SourceProfile(
            url="url1", domain="d", source_type="news", credibility_score=0.7
        )
        manager.source_types["news"].add("url1")

        recs = manager.recommend_additional_sources(["url1"], [])
        for rec in recs:
            assert "reason" in rec
            assert "query_modifier" in rec
            # source_type recs for desired types should have nonempty modifiers
            if rec["type"] == "source_type" and rec["target"] in (
                "academic",
                "government",
                "wiki",
            ):
                assert rec["query_modifier"] != ""

    def test_limits_to_five_recommendations(self, manager):
        """Even with many gaps, output is capped at 5 recommendations."""
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        manager.source_profiles["url1"] = SourceProfile(
            url="url1", domain="d", source_type="blog", credibility_score=0.1
        )
        manager.source_types["blog"].add("url1")

        # Add constraints to trigger temporal and geographic gaps too
        constraints = [
            Constraint(
                id="t1",
                type=ConstraintType.TEMPORAL,
                description="d",
                value="2010",
            ),
            Constraint(
                id="l1",
                type=ConstraintType.LOCATION,
                description="d",
                value="United States",
            ),
            Constraint(
                id="l2",
                type=ConstraintType.LOCATION,
                description="d",
                value="Canada",
            ),
            Constraint(
                id="l3",
                type=ConstraintType.LOCATION,
                description="d",
                value="Australia",
            ),
        ]
        recs = manager.recommend_additional_sources(["url1"], constraints)
        assert len(recs) <= 5

    def test_geographic_recs_use_region_domain(self, manager):
        """Geographic recommendations should use _get_region_domain for the query."""
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceProfile,
        )
        from local_deep_research.advanced_search_system.constraints.base_constraint import (
            Constraint,
            ConstraintType,
        )

        manager.source_profiles["url1"] = SourceProfile(
            url="url1",
            domain="d",
            source_type="academic",
            credibility_score=0.9,
        )
        manager.source_types["academic"].add("url1")

        constraints = [
            Constraint(
                id="l1",
                type=ConstraintType.LOCATION,
                description="d",
                value="United Kingdom",
            ),
        ]
        recs = manager.recommend_additional_sources(["url1"], constraints)
        geo_recs = [r for r in recs if r["type"] == "geographic"]
        if geo_recs:
            assert ".uk" in geo_recs[0]["query_modifier"]


# =============================================================================
# Manager initialization
# =============================================================================


class TestManagerInit:
    """Tests for SourceDiversityManager initialization."""

    def test_all_priorities_between_zero_and_one(self, manager):
        """Priority values represent credibility weights, must be valid scores."""
        for source_type, priority in manager.type_priorities.items():
            assert 0.0 <= priority <= 1.0, (
                f"{source_type} has invalid priority {priority}"
            )

    def test_priority_ordering_matches_credibility_hierarchy(self, manager):
        """Academic/government should outrank news/wiki which outranks blog/forum/social."""
        p = manager.type_priorities
        assert p["academic"] > p["news"]
        assert p["government"] > p["news"]
        assert p["news"] > p["blog"]
        assert p["blog"] > p["forum"]
        assert p["forum"] > p["social"]

    def test_source_types_is_defaultdict(self, manager):
        """source_types must be defaultdict so accessing unknown keys doesn't raise."""
        # This is critical: analyze_source does `self.source_types[source_type].add(url)`
        # without checking if the key exists first
        unknown_set = manager.source_types["never_seen_before"]
        assert isinstance(unknown_set, set)
        assert len(unknown_set) == 0

    def test_two_managers_have_independent_state(self, mock_model):
        """Each manager instance must have its own source_profiles dict."""
        from local_deep_research.advanced_search_system.source_management.diversity_manager import (
            SourceDiversityManager,
        )

        m1 = SourceDiversityManager(mock_model)
        m2 = SourceDiversityManager(mock_model)
        m1.analyze_source("https://example.com")
        assert "https://example.com" not in m2.source_profiles
