"""High-value pure logic tests for diversity_manager.py.

Covers dataclass fields/defaults, domain extraction edge cases,
source type classification, credibility scoring, temporal/geographic
extraction, diversity metrics formulas, temporal diversity scoring,
gap identification, diverse source selection, and helper methods.
"""

from datetime import datetime
from unittest.mock import MagicMock, patch


from local_deep_research.advanced_search_system.source_management.diversity_manager import (
    DiversityMetrics,
    SourceDiversityManager,
    SourceProfile,
)


# ---------------------------------------------------------------------------
# Dataclass field / default tests
# ---------------------------------------------------------------------------


class TestSourceProfileDataclass:
    """Verify SourceProfile dataclass fields and defaults."""

    def test_required_fields_assigned(self):
        profile = SourceProfile(
            url="http://x.com",
            domain="x.com",
            source_type="news",
            credibility_score=0.7,
        )
        assert profile.url == "http://x.com"
        assert profile.domain == "x.com"
        assert profile.source_type == "news"
        assert profile.credibility_score == 0.7

    def test_specialties_default_is_independent_list(self):
        """Each instance gets its own specialties list (no shared mutable default)."""
        p1 = SourceProfile(
            url="a", domain="a", source_type="x", credibility_score=0.5
        )
        p2 = SourceProfile(
            url="b", domain="b", source_type="x", credibility_score=0.5
        )
        p1.specialties.append("topic")
        assert p2.specialties == []

    def test_evidence_count_default_zero(self):
        profile = SourceProfile(
            url="u", domain="d", source_type="t", credibility_score=0.1
        )
        assert profile.evidence_count == 0


class TestDiversityMetricsDataclass:
    """Verify DiversityMetrics dataclass fields."""

    def test_all_fields_stored(self):
        metrics = DiversityMetrics(
            type_diversity=0.5,
            temporal_diversity=0.3,
            geographic_diversity=0.2,
            credibility_distribution={"high": 2, "low": 1},
            specialty_coverage={"ai": 3},
            overall_score=0.4,
        )
        assert metrics.type_diversity == 0.5
        assert metrics.credibility_distribution == {"high": 2, "low": 1}
        assert metrics.specialty_coverage == {"ai": 3}


# ---------------------------------------------------------------------------
# Helper to build a manager without a real model
# ---------------------------------------------------------------------------


def _make_manager(**overrides):
    """Create a SourceDiversityManager with a MagicMock model."""
    mgr = SourceDiversityManager(model=MagicMock())
    for key, val in overrides.items():
        setattr(mgr, key, val)
    return mgr


# ---------------------------------------------------------------------------
# _extract_domain
# ---------------------------------------------------------------------------


class TestExtractDomain:
    """Edge cases for URL domain extraction."""

    def test_subdomain_preserved(self):
        mgr = _make_manager()
        assert (
            mgr._extract_domain("https://api.example.com/v1")
            == "api.example.com"
        )

    def test_port_included_in_domain(self):
        mgr = _make_manager()
        result = mgr._extract_domain("http://localhost:8080/page")
        assert "localhost" in result

    def test_deeply_nested_path(self):
        mgr = _make_manager()
        assert mgr._extract_domain("https://a.b.c.d.com/x/y/z") == "a.b.c.d.com"

    def test_empty_string_returns_as_is(self):
        mgr = _make_manager()
        assert mgr._extract_domain("") == ""

    def test_ftp_scheme_not_matched(self):
        """ftp:// does not match the http(s) regex; raw string returned."""
        mgr = _make_manager()
        assert (
            mgr._extract_domain("ftp://files.example.com/f")
            == "ftp://files.example.com/f"
        )


# ---------------------------------------------------------------------------
# _determine_source_type
# ---------------------------------------------------------------------------


class TestDetermineSourceType:
    """Domain pattern matching for source type classification."""

    def test_scholar_in_url_is_academic(self):
        mgr = _make_manager()
        result = mgr._determine_source_type(
            "https://scholar.google.com/citations", "scholar.google.com", None
        )
        assert result == "academic"

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

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

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

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

    def test_ac_uk_is_academic(self):
        mgr = _make_manager()
        assert (
            mgr._determine_source_type(
                "https://ox.ac.uk/paper", "ox.ac.uk", None
            )
            == "academic"
        )

    def test_academic_in_url_but_not_domain(self):
        """'scholar' in the URL path still triggers academic."""
        mgr = _make_manager()
        result = mgr._determine_source_type(
            "https://google.com/scholar?q=test", "google.com", None
        )
        assert result == "academic"

    def test_priority_academic_over_news(self):
        """Academic patterns are checked first and win over news."""
        mgr = _make_manager()
        # domain has both 'edu' and 'news' -- academic checked first
        result = mgr._determine_source_type(
            "https://news.stanford.edu/article", "news.stanford.edu", None
        )
        assert result == "academic"

    def test_content_fallback_calls_analyze(self):
        """When no pattern matches but content is provided, _analyze_content_type is called."""
        mgr = _make_manager()
        with patch.object(
            mgr, "_analyze_content_type", return_value="blog"
        ) as mock_act:
            result = mgr._determine_source_type(
                "https://random.io/page", "random.io", "Some blog content"
            )
            assert result == "blog"
            mock_act.assert_called_once_with("Some blog content")


# ---------------------------------------------------------------------------
# _calculate_credibility
# ---------------------------------------------------------------------------


class TestCalculateCredibility:
    """Scoring logic for credibility."""

    def test_http_no_https_bonus(self):
        mgr = _make_manager()
        score = mgr._calculate_credibility(
            "http://example.com", "example.com", "general", None
        )
        assert score == 0.5  # base only, no https bonus

    def test_gov_domain_bonus(self):
        mgr = _make_manager()
        score = mgr._calculate_credibility(
            "https://data.gov/set", "data.gov", "government", None
        )
        # government base=0.85, +0.1(gov)=0.95, +0.05(https)=1.0
        assert score == 1.0

    def test_author_pattern_bonus(self):
        mgr = _make_manager()
        score = mgr._calculate_credibility(
            "http://example.com",
            "example.com",
            "general",
            "By John Smith, this paper explores...",
        )
        # general=0.5 + author=0.05 = 0.55
        assert score >= 0.55

    def test_citation_and_author_stack(self):
        mgr = _make_manager()
        content = "Author: Jane Doe. References [1] show trends from (2020)."
        score = mgr._calculate_credibility(
            "http://example.com", "example.com", "general", content
        )
        # general=0.5 + citation=0.1 + author=0.05 = 0.65
        assert abs(score - 0.65) < 0.01

    def test_unknown_source_type_base_score(self):
        mgr = _make_manager()
        score = mgr._calculate_credibility(
            "http://example.com", "example.com", "unknown_type", None
        )
        assert score == 0.5  # default from type_priorities.get fallback

    def test_score_never_exceeds_one(self):
        """Even with all bonuses, score is capped at 1.0."""
        mgr = _make_manager()
        content = "Author: A B. [1] (2020) references."
        score = mgr._calculate_credibility(
            "https://mit.edu/paper", "mit.edu", "academic", content
        )
        assert score <= 1.0


# ---------------------------------------------------------------------------
# _extract_temporal_coverage
# ---------------------------------------------------------------------------


class TestExtractTemporalCoverage:
    """Year extraction from text content."""

    def test_content_with_1900s_and_2000s(self):
        mgr = _make_manager()
        result = mgr._extract_temporal_coverage("Events from 1945 and 2005.")
        assert result is not None
        assert result[0].year == 1945
        assert result[1].year == 2005

    def test_only_19xx_years(self):
        mgr = _make_manager()
        result = mgr._extract_temporal_coverage(
            "Built in 1920 and renovated in 1985."
        )
        assert result[0].year == 1920
        assert result[1].year == 1985

    def test_empty_string_returns_none(self):
        mgr = _make_manager()
        assert mgr._extract_temporal_coverage("") is None

    def test_non_year_numbers_ignored(self):
        """Numbers that don't look like years (e.g., 1234, 2100) are not matched."""
        mgr = _make_manager()
        assert (
            mgr._extract_temporal_coverage("Order 1234 shipped on day 5")
            is None
        )


# ---------------------------------------------------------------------------
# _extract_geographic_focus
# ---------------------------------------------------------------------------


class TestExtractGeographicFocus:
    """Geographic pattern matching from URL and content."""

    def test_ca_tld_is_canada(self):
        mgr = _make_manager()
        result = mgr._extract_geographic_focus("https://example.ca/page", None)
        assert result == "Canada"

    def test_eu_in_url_is_europe(self):
        mgr = _make_manager()
        result = mgr._extract_geographic_focus("https://example.eu/data", None)
        assert result == "Europe"

    def test_content_with_multiple_mentions_picks_most_frequent(self):
        mgr = _make_manager()
        content = "UK policy affects UK citizens. Canada also participates."
        result = mgr._extract_geographic_focus("https://example.com", content)
        assert result == "UK"

    def test_content_case_insensitive_match(self):
        mgr = _make_manager()
        content = "The UNITED STATES announced new policy."
        result = mgr._extract_geographic_focus("https://example.com", content)
        assert result is not None


# ---------------------------------------------------------------------------
# _calculate_temporal_diversity
# ---------------------------------------------------------------------------


class TestCalculateTemporalDiversity:
    """Year range scoring for temporal diversity."""

    def test_empty_ranges(self):
        mgr = _make_manager()
        assert mgr._calculate_temporal_diversity([]) == 0.0

    def test_single_year_range_zero_diversity(self):
        """A single range covering one year gives 0.0 diversity."""
        mgr = _make_manager()
        result = mgr._calculate_temporal_diversity(
            [(datetime(2020, 1, 1), datetime(2020, 12, 31))]
        )
        assert result == 0.0

    def test_twenty_year_span_is_max(self):
        mgr = _make_manager()
        result = mgr._calculate_temporal_diversity(
            [(datetime(2000, 1, 1), datetime(2020, 12, 31))]
        )
        assert result == 1.0

    def test_span_over_twenty_capped(self):
        mgr = _make_manager()
        result = mgr._calculate_temporal_diversity(
            [(datetime(1950, 1, 1), datetime(2020, 12, 31))]
        )
        assert result == 1.0

    def test_ten_year_span_half_diversity(self):
        mgr = _make_manager()
        result = mgr._calculate_temporal_diversity(
            [(datetime(2010, 1, 1), datetime(2020, 12, 31))]
        )
        assert result == 0.5

    def test_multiple_overlapping_ranges(self):
        mgr = _make_manager()
        result = mgr._calculate_temporal_diversity(
            [
                (datetime(2000, 1, 1), datetime(2005, 12, 31)),
                (datetime(2003, 1, 1), datetime(2010, 12, 31)),
            ]
        )
        # span 2000-2010 = 10 years -> 0.5
        assert result == 0.5


# ---------------------------------------------------------------------------
# calculate_diversity_metrics
# ---------------------------------------------------------------------------


class TestCalculateDiversityMetrics:
    """Formula tests for the diversity metrics computation."""

    def _setup_profiles(self, mgr, profiles_data):
        """Load pre-built profiles into manager."""
        for data in profiles_data:
            url = data["url"]
            mgr.source_profiles[url] = SourceProfile(**data)

    def test_single_source_type_diversity(self):
        mgr = _make_manager()
        url = "https://only.com"
        mgr.source_profiles[url] = SourceProfile(
            url=url,
            domain="only.com",
            source_type="news",
            credibility_score=0.7,
        )
        metrics = mgr.calculate_diversity_metrics([url])
        # 1 unique type / 3 minimum = 0.333...
        assert abs(metrics.type_diversity - 1.0 / 3) < 0.01

    def test_three_types_reaches_full_type_diversity(self):
        mgr = _make_manager()
        types = ["academic", "news", "wiki"]
        urls = []
        for idx, stype in enumerate(types):
            url = f"https://site{idx}.com"
            urls.append(url)
            mgr.source_profiles[url] = SourceProfile(
                url=url,
                domain=f"site{idx}.com",
                source_type=stype,
                credibility_score=0.5,
            )
        metrics = mgr.calculate_diversity_metrics(urls)
        assert metrics.type_diversity == 1.0

    def test_credibility_boundary_0_6_is_medium(self):
        mgr = _make_manager()
        url = "https://med.com"
        mgr.source_profiles[url] = SourceProfile(
            url=url,
            domain="med.com",
            source_type="general",
            credibility_score=0.6,
        )
        metrics = mgr.calculate_diversity_metrics([url])
        assert "medium" in metrics.credibility_distribution
        assert metrics.credibility_distribution["medium"] == 1

    def test_credibility_below_0_6_is_low(self):
        mgr = _make_manager()
        url = "https://low.com"
        mgr.source_profiles[url] = SourceProfile(
            url=url,
            domain="low.com",
            source_type="general",
            credibility_score=0.59,
        )
        metrics = mgr.calculate_diversity_metrics([url])
        assert "low" in metrics.credibility_distribution

    def test_overall_score_formula(self):
        """Verify the weighted formula for overall_score."""
        mgr = _make_manager()
        urls = []
        for idx, stype in enumerate(["academic", "news", "wiki"]):
            url = f"https://s{idx}.com"
            urls.append(url)
            mgr.source_profiles[url] = SourceProfile(
                url=url,
                domain=f"s{idx}.com",
                source_type=stype,
                credibility_score=0.9,
                geographic_focus=["US", "UK", "CA"][idx],
                temporal_coverage=(
                    datetime(2000, 1, 1),
                    datetime(2020, 12, 31),
                ),
            )
        metrics = mgr.calculate_diversity_metrics(urls)
        # type_diversity=1.0, temporal=1.0, geo=1.0, high_ratio=3/3=1.0
        expected = 1.0 * 0.3 + 1.0 * 0.2 + 1.0 * 0.2 + 1.0 * 0.3
        assert abs(metrics.overall_score - expected) < 0.01

    def test_specialty_coverage_aggregated(self):
        mgr = _make_manager()
        url1 = "https://a.com"
        url2 = "https://b.com"
        mgr.source_profiles[url1] = SourceProfile(
            url=url1,
            domain="a.com",
            source_type="news",
            credibility_score=0.5,
            specialties=["ai", "ml"],
        )
        mgr.source_profiles[url2] = SourceProfile(
            url=url2,
            domain="b.com",
            source_type="wiki",
            credibility_score=0.5,
            specialties=["ai"],
        )
        metrics = mgr.calculate_diversity_metrics([url1, url2])
        assert metrics.specialty_coverage["ai"] == 2
        assert metrics.specialty_coverage["ml"] == 1


# ---------------------------------------------------------------------------
# _identify_diversity_gaps
# ---------------------------------------------------------------------------


class TestIdentifyDiversityGaps:
    """Gap identification logic."""

    def test_low_type_diversity_flags_missing_types(self):
        mgr = _make_manager()
        url = "https://blog.com"
        mgr.source_profiles[url] = SourceProfile(
            url=url,
            domain="blog.com",
            source_type="blog",
            credibility_score=0.5,
        )
        metrics = DiversityMetrics(
            type_diversity=0.3,
            temporal_diversity=0.8,
            geographic_diversity=0.8,
            credibility_distribution={"low": 1},
            specialty_coverage={},
            overall_score=0.3,
        )
        gaps = mgr._identify_diversity_gaps(metrics, [])
        assert "source_type" in gaps
        # All of academic, government, news, wiki should be missing
        assert set(gaps["source_type"]) == {
            "academic",
            "government",
            "news",
            "wiki",
        }

    def test_high_type_diversity_no_type_gap(self):
        mgr = _make_manager()
        for stype in ["academic", "government", "news", "wiki"]:
            url = f"https://{stype}.com"
            mgr.source_profiles[url] = SourceProfile(
                url=url,
                domain=f"{stype}.com",
                source_type=stype,
                credibility_score=0.5,
            )
        metrics = DiversityMetrics(
            type_diversity=0.9,
            temporal_diversity=0.8,
            geographic_diversity=0.8,
            credibility_distribution={"medium": 4},
            specialty_coverage={},
            overall_score=0.7,
        )
        gaps = mgr._identify_diversity_gaps(metrics, [])
        assert "source_type" not in gaps

    def test_low_credibility_ratio_flags_gap(self):
        mgr = _make_manager()
        metrics = DiversityMetrics(
            type_diversity=0.9,
            temporal_diversity=0.8,
            geographic_diversity=0.8,
            credibility_distribution={"low": 10},
            specialty_coverage={},
            overall_score=0.3,
        )
        gaps = mgr._identify_diversity_gaps(metrics, [])
        assert "credibility" in gaps

    def test_high_credibility_ratio_no_gap(self):
        mgr = _make_manager()
        metrics = DiversityMetrics(
            type_diversity=0.9,
            temporal_diversity=0.8,
            geographic_diversity=0.8,
            credibility_distribution={"high": 5, "low": 1},
            specialty_coverage={},
            overall_score=0.8,
        )
        gaps = mgr._identify_diversity_gaps(metrics, [])
        assert "credibility" not in gaps


# ---------------------------------------------------------------------------
# select_diverse_sources
# ---------------------------------------------------------------------------


class TestSelectDiverseSources:
    """Scoring and selection of diverse sources."""

    def test_selects_up_to_target_count(self):
        """With diverse source types, selection reaches the target count."""
        mgr = _make_manager()
        urls = []
        types = [
            "academic",
            "news",
            "wiki",
            "government",
            "blog",
            "general",
            "forum",
            "social",
            "academic",
            "news",
        ]
        for idx, stype in enumerate(types):
            url = f"https://site{idx}.com"
            urls.append(url)
            mgr.source_profiles[url] = SourceProfile(
                url=url,
                domain=f"site{idx}.com",
                source_type=stype,
                credibility_score=0.5,
            )
        result = mgr.select_diverse_sources(urls, target_count=5)
        assert len(result) == 5

    def test_diverse_types_preferred(self):
        """Sources with unique types are preferred over duplicates."""
        mgr = _make_manager()
        urls = []
        types = ["academic", "news", "general", "general", "general"]
        for idx, stype in enumerate(types):
            url = f"https://s{idx}.com"
            urls.append(url)
            mgr.source_profiles[url] = SourceProfile(
                url=url,
                domain=f"s{idx}.com",
                source_type=stype,
                credibility_score=0.5,
            )
        result = mgr.select_diverse_sources(urls, target_count=2)
        selected_types = {mgr.source_profiles[u].source_type for u in result}
        # Should pick at least 2 different types
        assert len(selected_types) >= 2

    def test_equal_to_target_returns_all(self):
        mgr = _make_manager()
        urls = ["https://a.com", "https://b.com"]
        result = mgr.select_diverse_sources(urls, target_count=2)
        assert result == urls


# ---------------------------------------------------------------------------
# _get_source_type_modifier and _get_region_domain
# ---------------------------------------------------------------------------


class TestGetSourceTypeModifier:
    """Search modifier mapping tests."""

    def test_news_modifier_contains_keywords(self):
        mgr = _make_manager()
        result = mgr._get_source_type_modifier("news")
        assert "news" in result

    def test_wiki_modifier(self):
        mgr = _make_manager()
        result = mgr._get_source_type_modifier("wiki")
        assert "wikipedia" in result

    def test_blog_modifier(self):
        mgr = _make_manager()
        result = mgr._get_source_type_modifier("blog")
        assert "blog" in result


class TestGetRegionDomain:
    """Region-to-domain suffix mapping tests."""

    def test_united_kingdom(self):
        mgr = _make_manager()
        assert mgr._get_region_domain("United Kingdom") == ".uk"

    def test_canada(self):
        mgr = _make_manager()
        assert mgr._get_region_domain("Canada") == ".ca"

    def test_australia(self):
        mgr = _make_manager()
        assert mgr._get_region_domain("Australia") == ".au"

    def test_europe_includes_multiple_tlds(self):
        mgr = _make_manager()
        result = mgr._get_region_domain("Europe")
        assert ".eu" in result
        assert ".de" in result
        assert ".fr" in result
