"""High-value edge case tests for the domain_classifier module.

Covers gaps not addressed by existing tests:
- DOMAIN_CATEGORIES structure validation
- DomainClassification model to_dict
- _get_domain_samples with None content_preview
- _build_classification_prompt sample truncation
- classify_all_domains URL parsing and www stripping
"""

from unittest.mock import MagicMock, patch

from local_deep_research.domain_classifier.classifier import (
    DomainClassifier,
    DOMAIN_CATEGORIES,
)
from local_deep_research.domain_classifier.models import DomainClassification


class TestDomainCategoriesStructure:
    """Validate the DOMAIN_CATEGORIES constant."""

    def test_has_ten_main_categories(self):
        """There should be exactly 10 main categories."""
        assert len(DOMAIN_CATEGORIES) == 10

    def test_no_subcategory_overlap_across_categories(self):
        """No subcategory name appears in more than one main category."""
        all_subcats = []
        for subcats in DOMAIN_CATEGORIES.values():
            all_subcats.extend(subcats)
        # Check for duplicates
        assert len(all_subcats) == len(set(all_subcats))

    def test_other_category_has_unknown(self):
        """The 'Other' category contains 'Unknown' subcategory."""
        assert "Unknown" in DOMAIN_CATEGORIES["Other"]

    def test_all_values_are_lists_of_strings(self):
        """All category values are lists of non-empty strings."""
        for cat, subcats in DOMAIN_CATEGORIES.items():
            assert isinstance(subcats, list), f"{cat} subcategories not a list"
            for s in subcats:
                assert isinstance(s, str) and len(s) > 0


class TestDomainClassificationModel:
    """Test the DomainClassification SQLAlchemy model."""

    def test_tablename(self):
        """Model uses the expected table name."""
        assert DomainClassification.__tablename__ == "domain_classifications"

    def test_to_dict_has_expected_keys(self):
        """to_dict returns all expected keys."""
        obj = DomainClassification(
            id=1,
            domain="example.com",
            category="Technology",
            subcategory="Software Development",
            confidence=0.9,
            reasoning="test",
            sample_titles='["Title"]',
            sample_count=1,
        )
        d = obj.to_dict()
        expected_keys = {
            "id",
            "domain",
            "category",
            "subcategory",
            "confidence",
            "reasoning",
            "sample_titles",
            "sample_count",
            "created_at",
            "updated_at",
        }
        assert set(d.keys()) == expected_keys

    def test_to_dict_none_timestamps(self):
        """to_dict handles None timestamps gracefully."""
        obj = DomainClassification(
            id=1,
            domain="test.com",
            category="Other",
        )
        obj.created_at = None
        obj.updated_at = None
        d = obj.to_dict()
        assert d["created_at"] is None
        assert d["updated_at"] is None


class TestGetDomainSamples:
    """Test _get_domain_samples method."""

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_none_content_preview_returns_none_in_sample(
        self, mock_session_ctx
    ):
        """When content_preview is None, sample preview is None."""
        classifier = DomainClassifier(username="test")

        mock_session = MagicMock()
        mock_resource = MagicMock()
        mock_resource.title = "Test Title"
        mock_resource.url = "http://example.com/page"
        mock_resource.content_preview = None

        mock_session.query.return_value.filter.return_value.limit.return_value.all.return_value = [
            mock_resource
        ]

        samples = classifier._get_domain_samples("example.com", mock_session)
        assert len(samples) == 1
        assert samples[0]["title"] == "Test Title"
        assert samples[0]["preview"] is None

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_empty_resources_returns_empty_list(self, mock_session_ctx):
        """When no resources match, returns empty list."""
        classifier = DomainClassifier(username="test")
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.limit.return_value.all.return_value = []

        samples = classifier._get_domain_samples("no-match.com", mock_session)
        assert samples == []

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_none_title_becomes_untitled(self, mock_session_ctx):
        """When resource.title is None, it becomes 'Untitled'."""
        classifier = DomainClassifier(username="test")
        mock_session = MagicMock()
        mock_resource = MagicMock()
        mock_resource.title = None
        mock_resource.url = "http://example.com"
        mock_resource.content_preview = "some preview"
        mock_session.query.return_value.filter.return_value.limit.return_value.all.return_value = [
            mock_resource
        ]

        samples = classifier._get_domain_samples("example.com", mock_session)
        assert samples[0]["title"] == "Untitled"

    @patch(
        "local_deep_research.domain_classifier.classifier.get_user_db_session"
    )
    def test_preview_truncated_to_200_chars(self, mock_session_ctx):
        """content_preview is truncated to 200 characters."""
        classifier = DomainClassifier(username="test")
        mock_session = MagicMock()
        mock_resource = MagicMock()
        mock_resource.title = "Title"
        mock_resource.url = "http://example.com"
        mock_resource.content_preview = "x" * 500
        mock_session.query.return_value.filter.return_value.limit.return_value.all.return_value = [
            mock_resource
        ]

        samples = classifier._get_domain_samples("example.com", mock_session)
        assert len(samples[0]["preview"]) == 200


class TestBuildClassificationPromptEdgeCases:
    """Test _build_classification_prompt edge cases."""

    def test_samples_without_preview_omit_preview_line(self):
        """Samples with no preview don't include a Preview line."""
        classifier = DomainClassifier(username="test")
        samples = [
            {"title": "Test", "url": "http://example.com", "preview": None}
        ]
        prompt = classifier._build_classification_prompt("example.com", samples)
        assert "Preview:" not in prompt

    def test_only_first_five_samples_included(self):
        """Only the first 5 samples are included in the prompt."""
        classifier = DomainClassifier(username="test")
        samples = [
            {
                "title": f"Title {i}",
                "url": f"http://example.com/{i}",
                "preview": f"preview {i}",
            }
            for i in range(10)
        ]
        prompt = classifier._build_classification_prompt("example.com", samples)
        assert "Title 4" in prompt
        assert (
            "Title 5" not in prompt
        )  # 0-indexed: 5th sample (index 4) is last

    def test_preview_truncated_at_100_chars(self):
        """Sample previews in prompt are truncated to 100 chars."""
        classifier = DomainClassifier(username="test")
        long_preview = "a" * 200
        samples = [
            {"title": "T", "url": "http://ex.com", "preview": long_preview}
        ]
        prompt = classifier._build_classification_prompt("example.com", samples)
        # The prompt should contain the truncated preview followed by "..."
        assert "a" * 100 + "..." in prompt

    def test_all_main_categories_in_prompt(self):
        """All main category names appear in the prompt."""
        classifier = DomainClassifier(username="test")
        prompt = classifier._build_classification_prompt("test.com", [])
        for category in DOMAIN_CATEGORIES.keys():
            assert category in prompt
