"""
Comprehensive tests for domain_classifier/classifier.py

Tests cover:
- DomainClassifier initialization
- Domain categories
- Classification logic
- LLM integration
"""

import pytest
from unittest.mock import Mock, patch, MagicMock


class TestDomainCategories:
    """Tests for domain category definitions."""

    def test_domain_categories_exist(self):
        """Test that domain categories are defined."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        assert isinstance(DOMAIN_CATEGORIES, dict)
        assert len(DOMAIN_CATEGORIES) > 0

    def test_expected_categories_present(self):
        """Test that expected top-level categories exist."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        expected_categories = [
            "Academic & Research",
            "News & Media",
            "Reference & Documentation",
            "Social & Community",
            "Business & Commerce",
            "Technology",
            "Government & Organization",
            "Other",
        ]

        for category in expected_categories:
            assert category in DOMAIN_CATEGORIES, (
                f"Missing category: {category}"
            )

    def test_academic_subcategories(self):
        """Test academic category subcategories."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        academic = DOMAIN_CATEGORIES.get("Academic & Research", [])
        assert "University/Education" in academic
        assert "Scientific Journal" in academic
        assert "Research Institution" in academic

    def test_news_subcategories(self):
        """Test news category subcategories."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        news = DOMAIN_CATEGORIES.get("News & Media", [])
        assert "General News" in news
        assert "Tech News" in news
        assert "Business News" in news

    def test_technology_subcategories(self):
        """Test technology category subcategories."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        tech = DOMAIN_CATEGORIES.get("Technology", [])
        assert "Software Development" in tech
        assert "Open Source Project" in tech
        assert "Cloud Service" in tech

    def test_other_includes_unknown(self):
        """Test that Other category includes Unknown."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        other = DOMAIN_CATEGORIES.get("Other", [])
        assert "Unknown" in other

    def test_all_categories_have_subcategories(self):
        """Test that all categories have at least one subcategory."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        for category, subcategories in DOMAIN_CATEGORIES.items():
            assert len(subcategories) > 0, (
                f"Category {category} has no subcategories"
            )


class TestDomainClassifierInit:
    """Tests for DomainClassifier initialization."""

    def test_init_with_username(self):
        """Test initialization with username."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        classifier = DomainClassifier(username="testuser")

        assert classifier.username == "testuser"
        assert classifier.settings_snapshot is None
        assert classifier.llm is None

    def test_init_with_settings_snapshot(self):
        """Test initialization with settings snapshot."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        snapshot = {"llm.model": "test-model"}
        classifier = DomainClassifier(
            username="testuser", settings_snapshot=snapshot
        )

        assert classifier.settings_snapshot == snapshot

    @patch("local_deep_research.domain_classifier.classifier.get_llm")
    def test_get_llm_creates_instance(self, mock_get_llm):
        """Test that _get_llm creates LLM instance."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        mock_llm = Mock()
        mock_get_llm.return_value = mock_llm

        classifier = DomainClassifier(username="testuser")
        result = classifier._get_llm()

        assert result == mock_llm
        mock_get_llm.assert_called_once()

    @patch("local_deep_research.domain_classifier.classifier.get_llm")
    def test_get_llm_caches_instance(self, mock_get_llm):
        """Test that _get_llm caches LLM instance."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        mock_llm = Mock()
        mock_get_llm.return_value = mock_llm

        classifier = DomainClassifier(username="testuser")
        result1 = classifier._get_llm()
        result2 = classifier._get_llm()

        assert result1 == result2
        # Should only be called once due to caching
        assert mock_get_llm.call_count == 1


class TestDomainClassifierClassify:
    """Tests for classification functionality."""

    @pytest.fixture
    def classifier(self):
        """Create a DomainClassifier instance for testing."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        return DomainClassifier(username="testuser")

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    @patch("local_deep_research.domain_classifier.classifier.get_llm")
    def test_classify_domain_success(
        self, mock_get_llm, mock_get_db_session, classifier
    ):
        """Test successful domain classification."""
        mock_llm = MagicMock()
        mock_response = Mock()
        mock_response.content = """
{
    "category": "Technology",
    "subcategory": "Software Development",
    "confidence": 0.9,
    "reasoning": "Domain is a software project"
}
"""
        mock_llm.invoke.return_value = mock_response
        mock_get_llm.return_value = mock_llm

        # Mock the database session context manager
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        # No existing classification
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_get_db_session.return_value = mock_session

        result = classifier.classify_domain("github.com")

        assert result is not None

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    @patch("local_deep_research.domain_classifier.classifier.get_llm")
    def test_classify_domain_handles_invalid_json(
        self, mock_get_llm, mock_get_db_session, classifier
    ):
        """Test handling of invalid JSON response from LLM."""
        mock_llm = MagicMock()
        mock_response = Mock()
        mock_response.content = "Not valid JSON"
        mock_llm.invoke.return_value = mock_response
        mock_get_llm.return_value = mock_llm

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_get_db_session.return_value = mock_session

        # Invalid JSON from the LLM raises ValueError inside the session
        # context, which the SUT's broad `except Exception` catches and
        # converts to a None return. (See classifier.py:225-265.)
        result = classifier.classify_domain("example.com")

        assert result is None

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    @patch("local_deep_research.domain_classifier.classifier.get_llm")
    def test_classify_domain_handles_llm_error(
        self, mock_get_llm, mock_get_db_session, classifier
    ):
        """Test handling of LLM errors during classification."""
        mock_llm = MagicMock()
        mock_llm.invoke.side_effect = Exception("LLM error")
        mock_get_llm.return_value = mock_llm

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_get_db_session.return_value = mock_session

        # Should handle error gracefully
        result = classifier.classify_domain("example.com")

        # Should return Unknown or None on error
        assert result is None or (
            hasattr(result, "category") and "Unknown" in str(result.category)
        )

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_classify_domain_empty_domain(
        self, mock_get_db_session, classifier
    ):
        """Empty-string domain must not crash; the SUT routes through the
        LLM path (no cached row), and with no LLM mock configured the
        broad except in classify_domain converts the error to a None
        return."""
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_get_db_session.return_value = mock_session

        result = classifier.classify_domain("")

        assert result is None

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_classify_domain_none_domain(self, mock_get_db_session, classifier):
        """Same path as the empty-string case: no cached row, no LLM mock,
        broad except → None return."""
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_get_db_session.return_value = mock_session

        result = classifier.classify_domain(None)

        assert result is None


class TestClassifyAllDomains:
    """Tests for batch/all-domain classification functionality."""

    def test_classify_all_domains_method_exists(self):
        """Test that classify_all_domains method exists."""
        from local_deep_research.domain_classifier.classifier import (
            DomainClassifier,
        )

        classifier = DomainClassifier(username="testuser")
        assert hasattr(classifier, "classify_all_domains")
        assert callable(classifier.classify_all_domains)


class TestCategoryValidation:
    """Tests for category validation."""

    def test_all_subcategories_are_strings(self):
        """Test that all subcategories are strings."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        for category, subcategories in DOMAIN_CATEGORIES.items():
            for subcat in subcategories:
                assert isinstance(subcat, str), (
                    f"Subcategory in {category} is not a string: {subcat}"
                )

    def test_no_duplicate_subcategories_within_category(self):
        """Test that there are no duplicate subcategories within a category."""
        from local_deep_research.domain_classifier.classifier import (
            DOMAIN_CATEGORIES,
        )

        for category, subcategories in DOMAIN_CATEGORIES.items():
            unique_subcats = set(subcategories)
            assert len(unique_subcats) == len(subcategories), (
                f"Duplicate subcategories in {category}"
            )


class TestUrlparseExceptionInClassifyAll:
    """Tests for urlparse exception handling in classify_all_domains (PR #2027).

    PR #2027 changed bare `except:` to `except Exception:` when
    parsing URLs to extract domains.
    """

    def test_valid_url_extracts_domain(self):
        """Valid URL extracts domain correctly."""
        from urllib.parse import urlparse

        url = "https://www.example.com/path"
        parsed = urlparse(url)
        domain = parsed.netloc.lower()
        if domain.startswith("www."):
            domain = domain[4:]

        assert domain == "example.com"

    def test_url_without_scheme_no_netloc(self):
        """URL without scheme has no netloc."""
        from urllib.parse import urlparse

        url = "just-some-text"
        parsed = urlparse(url)
        domain = parsed.netloc.lower()

        assert domain == ""

    def test_empty_url_handled(self):
        """Empty URL string produces empty domain."""
        from urllib.parse import urlparse

        url = ""
        parsed = urlparse(url)
        domain = parsed.netloc.lower()

        assert domain == ""
