"""Tests for the observation categorizer pipeline."""
import pytest
from unittest.mock import MagicMock

from greymatter_core.categorizer import (
    ObservationCategorizer,
    VALID_CATEGORIES,
    DOMAIN_TAXONOMY,
    _parse_result,
    _empty_result,
)


@pytest.fixture
def mock_llm():
    llm = MagicMock()
    llm.generate_json.return_value = {
        "domains": ["python", "testing"],
        "suggested_category": "pattern",
        "category_changed": False,
        "confidence": 0.9,
    }
    return llm


@pytest.fixture
def categorizer(mock_llm):
    return ObservationCategorizer(mock_llm)


@pytest.fixture
def mock_db():
    db = MagicMock()
    db.execute = MagicMock()
    db.execute_one = MagicMock(return_value=None)
    return db


class TestParseResult:
    def test_valid_result(self):
        raw = {"domains": ["database", "performance"], "suggested_category": "insight",
               "category_changed": False, "confidence": 0.85}
        result = _parse_result(raw, "insight")
        assert result["domains"] == ["database", "performance"]
        assert result["confidence"] == 0.85

    def test_normalizes_domains_to_lowercase(self):
        raw = {"domains": ["DATABASE", "Python"], "confidence": 0.8}
        result = _parse_result(raw, "insight")
        assert result["domains"] == ["database", "python"]

    def test_replaces_spaces_with_hyphens(self):
        raw = {"domains": ["data pipeline", "error handling"], "confidence": 0.8}
        result = _parse_result(raw, "insight")
        assert result["domains"] == ["data-pipeline", "error-handling"]

    def test_caps_at_three_domains(self):
        raw = {"domains": ["a", "b", "c", "d", "e"], "confidence": 0.8}
        assert len(_parse_result(raw, "insight")["domains"]) == 3

    def test_rejects_invalid_category(self):
        raw = {"domains": ["python"], "suggested_category": "INVALID",
               "category_changed": True, "confidence": 0.9}
        result = _parse_result(raw, "insight")
        assert result["suggested_category"] == "insight"
        assert result["category_changed"] is False

    def test_category_changed_requires_different(self):
        raw = {"domains": ["python"], "suggested_category": "insight",
               "category_changed": True, "confidence": 0.9}
        assert _parse_result(raw, "insight")["category_changed"] is False

    def test_category_changed_true_when_different(self):
        raw = {"domains": ["python"], "suggested_category": "decision",
               "category_changed": True, "confidence": 0.9}
        result = _parse_result(raw, "insight")
        assert result["category_changed"] is True

    def test_non_dict_returns_empty(self):
        result = _parse_result("not a dict", "insight")
        assert result["domains"] == []

    def test_non_list_domains_returns_empty(self):
        raw = {"domains": "python,testing", "confidence": 0.8}
        assert _parse_result(raw, "insight")["domains"] == []

    def test_clamps_confidence(self):
        assert _parse_result({"domains": ["python"], "confidence": 1.5}, "insight")["confidence"] == 1.0
        assert _parse_result({"domains": ["python"], "confidence": -0.5}, "insight")["confidence"] == 0.0

    def test_filters_empty_tags(self):
        raw = {"domains": ["python", "", "  ", "testing"], "confidence": 0.8}
        assert _parse_result(raw, "insight")["domains"] == ["python", "testing"]

    def test_filters_long_tags(self):
        raw = {"domains": ["a" * 50, "python"], "confidence": 0.8}
        assert _parse_result(raw, "insight")["domains"] == ["python"]


class TestEmptyResult:
    def test_returns_correct_structure(self):
        result = _empty_result("decision")
        assert result["domains"] == []
        assert result["confidence"] == 0.0


class TestCategorize:
    def test_categorize_success(self, categorizer, mock_llm):
        obs = {"title": "Run pytest", "content": "Convention.", "category": "pattern"}
        result = categorizer.categorize(obs)
        assert result["domains"] == ["python", "testing"]
        mock_llm.generate_json.assert_called_once()

    def test_empty_content_returns_empty(self, categorizer, mock_llm):
        result = categorizer.categorize({"title": "", "content": "", "category": "insight"})
        assert result["domains"] == []
        mock_llm.generate_json.assert_not_called()

    def test_llm_failure_returns_empty(self, categorizer, mock_llm):
        mock_llm.generate_json.side_effect = ConnectionError("down")
        result = categorizer.categorize({"title": "X", "content": "Y", "category": "insight"})
        assert result["domains"] == []


class TestCategorizeAndApply:
    def test_updates_domains_in_db(self, categorizer, mock_db):
        obs = {"id": "obs-123", "title": "WAL", "content": "Perf.", "category": "pattern"}
        categorizer.categorize_and_apply(obs, mock_db)
        mock_db.execute.assert_called_once()
        assert mock_db.execute.call_args[0][1][0] == "python,testing"

    def test_skips_empty_domains(self, categorizer, mock_llm, mock_db):
        mock_llm.generate_json.return_value = {"domains": [], "confidence": 0.3}
        categorizer.categorize_and_apply({"id": "obs-1", "title": "X", "content": "Y", "category": "p"}, mock_db)
        mock_db.execute.assert_not_called()

    def test_skips_no_id(self, categorizer, mock_db):
        categorizer.categorize_and_apply({"title": "X", "content": "Y", "category": "p"}, mock_db)
        mock_db.execute.assert_not_called()

    def test_recategorize_when_enabled(self, mock_llm, mock_db):
        mock_llm.generate_json.return_value = {"domains": ["security"], "suggested_category": "gotcha",
                                                 "category_changed": True, "confidence": 0.95}
        cat = ObservationCategorizer(mock_llm, auto_recategorize=True)
        cat.categorize_and_apply({"id": "obs-456", "title": "SQL", "content": "Inject", "category": "insight"}, mock_db)
        assert "category" in mock_db.execute.call_args[0][0]

    def test_no_recategorize_when_disabled(self, categorizer, mock_llm, mock_db):
        mock_llm.generate_json.return_value = {"domains": ["security"], "suggested_category": "gotcha",
                                                 "category_changed": True, "confidence": 0.95}
        categorizer.categorize_and_apply({"id": "obs-456", "title": "SQL", "content": "X", "category": "insight"}, mock_db)
        assert "category = ?" not in mock_db.execute.call_args[0][0]


class TestCategorizeBatch:
    def test_batch_tags_untagged(self, categorizer, mock_db):
        mock_db.execute.return_value = [
            {"id": "obs-1", "title": "A", "content": "B", "category": "pattern", "domains": ""},
            {"id": "obs-2", "title": "C", "content": "D", "category": "decision", "domains": ""},
        ]
        stats = categorizer.categorize_batch(mock_db, batch_size=10)
        assert stats["processed"] == 2
        assert stats["tagged"] == 2

    def test_batch_skips_low_confidence(self, mock_llm, mock_db):
        mock_llm.generate_json.return_value = {"domains": ["python"], "confidence": 0.2}
        cat = ObservationCategorizer(mock_llm, min_confidence=0.5)
        mock_db.execute.return_value = [{"id": "obs-1", "title": "X", "content": "Y", "category": "p", "domains": ""}]
        assert cat.categorize_batch(mock_db, batch_size=10)["skipped"] == 1

    def test_batch_empty_table(self, categorizer, mock_db):
        mock_db.execute.return_value = []
        assert categorizer.categorize_batch(mock_db)["processed"] == 0

    def test_batch_handles_db_error(self, categorizer, mock_db):
        mock_db.execute.side_effect = [
            [{"id": "obs-1", "title": "X", "content": "Y", "category": "p", "domains": ""}],
            Exception("DB error"),
        ]
        assert categorizer.categorize_batch(mock_db, batch_size=10)["failed"] == 1


class TestCategorizeChunk:
    def test_single_uses_categorize(self, categorizer):
        results = categorizer._categorize_chunk([{"title": "X", "content": "Y", "category": "pattern"}])
        assert len(results) == 1

    def test_batch_for_multiple(self, categorizer, mock_llm):
        mock_llm.generate_json.return_value = [
            {"domains": ["python"], "suggested_category": "pattern", "category_changed": False, "confidence": 0.9},
            {"domains": ["database"], "suggested_category": "decision", "category_changed": False, "confidence": 0.85},
        ]
        results = categorizer._categorize_chunk([
            {"id": "obs-1", "title": "A", "content": "B", "category": "pattern"},
            {"id": "obs-2", "title": "C", "content": "D", "category": "decision"},
        ])
        assert len(results) == 2
        assert results[0]["domains"] == ["python"]
        assert results[1]["domains"] == ["database"]

    def test_batch_mismatch_falls_back(self, categorizer, mock_llm):
        mock_llm.generate_json.side_effect = [
            [{"domains": ["python"], "confidence": 0.9}],
            {"domains": ["python"], "suggested_category": "pattern", "category_changed": False, "confidence": 0.9},
            {"domains": ["db"], "suggested_category": "decision", "category_changed": False, "confidence": 0.85},
        ]
        results = categorizer._categorize_chunk([
            {"id": "obs-1", "title": "A", "content": "B", "category": "pattern"},
            {"id": "obs-2", "title": "C", "content": "D", "category": "decision"},
        ])
        assert len(results) == 2


class TestConstants:
    def test_valid_categories_matches_extraction(self):
        from greymatter_core.extraction import ExtractionPipeline
        assert VALID_CATEGORIES == ExtractionPipeline._VALID_CATEGORIES

    def test_domain_taxonomy_has_common_tags(self):
        for tag in ["python", "security", "database", "testing"]:
            assert tag in DOMAIN_TAXONOMY
