"""Unit tests for metrics_routes pure logic: _extract_domain, get_rating_analytics, get_link_analytics.

These test the aggregation logic directly (mocking DB), not through HTTP endpoints.
"""

from unittest.mock import MagicMock, Mock, patch

from local_deep_research.database.models import (
    ResearchHistory,
)
from local_deep_research.domain_classifier import DomainClassification
from local_deep_research.web.routes.metrics_routes import (
    _extract_domain,
    get_link_analytics,
    get_rating_analytics,
)


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


class TestExtractDomain:
    """Tests for _extract_domain URL normalization."""

    def test_normal_url(self):
        assert _extract_domain("https://example.com/path") == "example.com"

    def test_www_prefix_stripped(self):
        assert _extract_domain("https://www.example.com") == "example.com"

    def test_no_scheme_empty_netloc(self):
        # urlparse("example.com") → netloc="", path="example.com"
        assert _extract_domain("example.com") is None

    def test_url_with_port(self):
        assert (
            _extract_domain("https://example.com:8080/path")
            == "example.com:8080"
        )

    def test_none_returns_none(self):
        """None input should be treated like other invalid URLs."""
        assert _extract_domain(None) is None

    def test_empty_string_returns_none(self):
        assert _extract_domain("") is None

    def test_uppercase_normalized(self):
        assert _extract_domain("https://EXAMPLE.COM/path") == "example.com"

    def test_www_with_subdomain(self):
        # Only leading "www." is stripped, not "www2."
        assert (
            _extract_domain("https://www.sub.example.com") == "sub.example.com"
        )

    def test_http_scheme(self):
        assert _extract_domain("http://example.com/page?q=1") == "example.com"


# ---------------------------------------------------------------------------
# get_rating_analytics — pure logic
# ---------------------------------------------------------------------------


def _make_rating(value):
    """Create a mock ResearchRating with a .rating attribute."""
    r = Mock()
    r.rating = value
    return r


def _mock_session_ctx(mock_ctx, session):
    """Wire up get_user_db_session mock as context manager."""
    mock_ctx.return_value.__enter__ = Mock(return_value=session)
    mock_ctx.return_value.__exit__ = Mock(return_value=False)


class TestGetRatingAnalyticsPureLogic:
    """Tests for get_rating_analytics aggregation, mocking DB layer."""

    @patch("local_deep_research.web.routes.metrics_routes.flask_session", {})
    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_no_username_returns_error(self, mock_ctx):
        result = get_rating_analytics(username=None)
        assert result["rating_analytics"]["error"] == "No user session"
        assert result["rating_analytics"]["total_ratings"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_empty_ratings(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = []
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="30d", username="alice")
        analytics = result["rating_analytics"]
        assert analytics["avg_rating"] is None
        assert analytics["total_ratings"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_rating_distribution(self, mock_ctx):
        ratings = [_make_rating(v) for v in [5, 5, 4, 3]]
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = (
            ratings
        )
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="30d", username="alice")
        dist = result["rating_analytics"]["rating_distribution"]
        assert dist["5"] == 2
        assert dist["4"] == 1
        assert dist["3"] == 1
        assert dist["2"] == 0
        assert dist["1"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_average_rating(self, mock_ctx):
        ratings = [_make_rating(v) for v in [5, 4, 3]]
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = (
            ratings
        )
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="30d", username="alice")
        assert result["rating_analytics"]["avg_rating"] == 4.0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_satisfaction_mapping(self, mock_ctx):
        ratings = [_make_rating(v) for v in [5, 4, 3, 2, 1]]
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = (
            ratings
        )
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="30d", username="alice")
        sat = result["rating_analytics"]["satisfaction_stats"]
        assert sat["very_satisfied"] == 1
        assert sat["satisfied"] == 1
        assert sat["neutral"] == 1
        assert sat["dissatisfied"] == 1
        assert sat["very_dissatisfied"] == 1

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_period_all_no_time_filter(self, mock_ctx):
        """period='all' → days=None → no time filter applied."""
        ratings = [_make_rating(5)]
        session = MagicMock()
        query_mock = session.query.return_value
        # When days is None, .all() is called directly (no .filter())
        query_mock.all.return_value = ratings
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="all", username="alice")
        assert result["rating_analytics"]["total_ratings"] == 1
        # filter should NOT be called when period is "all"
        query_mock.filter.assert_not_called()

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_unknown_period_defaults_to_30(self, mock_ctx):
        """Unknown period string falls back to 30 days."""
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = []
        _mock_session_ctx(mock_ctx, session)

        # Should not raise — just uses default 30
        result = get_rating_analytics(period="999d", username="alice")
        assert result["rating_analytics"]["total_ratings"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_exception_returns_fallback(self, mock_ctx):
        """Any exception → returns zero-count fallback dict."""
        mock_ctx.return_value.__enter__ = Mock(
            side_effect=RuntimeError("db error")
        )
        mock_ctx.return_value.__exit__ = Mock(return_value=False)

        result = get_rating_analytics(period="30d", username="alice")
        analytics = result["rating_analytics"]
        assert analytics["avg_rating"] is None
        assert analytics["total_ratings"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_total_ratings_count(self, mock_ctx):
        ratings = [_make_rating(v) for v in [5, 5, 4, 4, 3]]
        session = MagicMock()
        session.query.return_value.filter.return_value.all.return_value = (
            ratings
        )
        _mock_session_ctx(mock_ctx, session)

        result = get_rating_analytics(period="7d", username="bob")
        assert result["rating_analytics"]["total_ratings"] == 5


# ---------------------------------------------------------------------------
# get_link_analytics — pure logic
# ---------------------------------------------------------------------------


def _build_link_session(resources, classifications=None, researches=None):
    """Build a mock session that handles multiple session.query(Model) calls.

    get_link_analytics calls session.query with different models:
    1. ResearchResource → filter → all → resources
    2. DomainClassification → filter (in_) → all → classifications
    3. ResearchHistory → filter (in_) → all → researches
    """
    if classifications is None:
        classifications = []
    if researches is None:
        researches = []

    # Create separate query chain mocks for each model
    resource_query = MagicMock()
    resource_query.filter.return_value.all.return_value = resources
    resource_query.all.return_value = resources

    classification_query = MagicMock()
    classification_query.filter.return_value.all.return_value = classifications

    research_query = MagicMock()
    research_query.filter.return_value.all.return_value = researches

    session = MagicMock()

    def query_dispatch(*entities):
        # ResearchResource is now queried by projected columns (id/url/... +
        # a has_preview label) rather than the full entity (#4560), so match
        # the other two models by identity and default to the resource query.
        first = entities[0]
        if first is DomainClassification:
            return classification_query
        if first is ResearchHistory:
            return research_query
        return resource_query

    session.query.side_effect = query_dispatch
    return session


def _make_resource(
    url,
    research_id=1,
    title=None,
    preview=None,
    created_at="2025-01-15T12:00:00",
    source_type=None,
):
    r = Mock()
    r.url = url
    r.research_id = research_id
    r.title = title
    r.content_preview = preview
    # get_link_analytics now reads the SQL-projected ``has_preview`` boolean
    # (content_preview IS NOT NULL AND != '') rather than the body itself (#4560).
    r.has_preview = bool(preview)
    r.created_at = created_at
    r.source_type = source_type
    return r


class TestGetLinkAnalyticsPureLogic:
    """Tests for get_link_analytics aggregation, mocking DB layer."""

    @patch("local_deep_research.web.routes.metrics_routes.flask_session", {})
    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_no_username_returns_error(self, mock_ctx):
        result = get_link_analytics(username=None)
        assert result["link_analytics"]["error"] == "No user session"
        assert result["link_analytics"]["total_links"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_empty_resources(self, mock_ctx):
        session = _build_link_session(resources=[])
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        la = result["link_analytics"]
        assert la["top_domains"] == []
        assert la["total_unique_domains"] == 0
        assert la["total_links"] == 0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_domain_counting(self, mock_ctx):
        resources = [
            _make_resource("https://example.com/a", research_id=1),
            _make_resource("https://example.com/b", research_id=1),
            _make_resource("https://example.com/c", research_id=2),
            _make_resource("https://other.com/d", research_id=2),
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        la = result["link_analytics"]
        assert la["total_links"] == 4
        # example.com should be the top domain with count=3
        top = la["top_domains"]
        assert len(top) == 2
        assert top[0]["domain"] == "example.com"
        assert top[0]["count"] == 3

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_avg_links_per_research(self, mock_ctx):
        # 6 resources across 3 researches → avg = 2.0
        resources = [
            _make_resource(f"https://d{i}.com/p", research_id=(i % 3) + 1)
            for i in range(6)
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        assert result["link_analytics"]["avg_links_per_research"] == 2.0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_temporal_date_slicing(self, mock_ctx):
        resources = [
            _make_resource("https://a.com/1", created_at="2025-01-15T12:00:00"),
            _make_resource("https://a.com/2", created_at="2025-01-15T14:00:00"),
            _make_resource("https://a.com/3", created_at="2025-01-16T10:00:00"),
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        trend = result["link_analytics"]["temporal_trend"]
        # Two dates: 2025-01-15 (count=2) and 2025-01-16 (count=1)
        assert len(trend) == 2
        assert trend[0]["date"] == "2025-01-15"
        assert trend[0]["count"] == 2
        assert trend[1]["date"] == "2025-01-16"
        assert trend[1]["count"] == 1

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_top_10_domain_limit(self, mock_ctx):
        # Create 12 unique domains
        resources = [
            _make_resource(f"https://domain{i}.com/p") for i in range(12)
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        # Should only return top 10
        assert len(result["link_analytics"]["top_domains"]) == 10
        assert result["link_analytics"]["total_unique_domains"] == 12

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_percentage_calculation(self, mock_ctx):
        resources = [
            _make_resource("https://a.com/1"),
            _make_resource("https://a.com/2"),
            _make_resource("https://b.com/1"),
            _make_resource("https://b.com/2"),
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        top = result["link_analytics"]["top_domains"]
        # Each domain has 2/4 = 50%
        for d in top:
            assert d["percentage"] == 50.0

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_source_type_tracking(self, mock_ctx):
        resources = [
            _make_resource("https://a.com/1", source_type="academic"),
            _make_resource("https://b.com/1", source_type="academic"),
            _make_resource("https://c.com/1", source_type="news"),
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        src = result["link_analytics"]["source_type_analysis"]
        assert src["academic"] == 2
        assert src["news"] == 1

    @patch("local_deep_research.web.routes.metrics_routes.get_user_db_session")
    def test_unique_domains_count(self, mock_ctx):
        resources = [
            _make_resource("https://a.com/1"),
            _make_resource("https://a.com/2"),
            _make_resource("https://b.com/1"),
            _make_resource("https://c.com/1"),
        ]
        session = _build_link_session(resources=resources)
        _mock_session_ctx(mock_ctx, session)

        result = get_link_analytics(period="30d", username="alice")
        assert result["link_analytics"]["total_unique_domains"] == 3
