"""Unit tests for SemanticTagger service."""

import json
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

from src.api.schemas.tags import TagStatus
from src.core.config import Settings
from src.extraction_v2.semantic_tagger import (
    BatchProcessor,
    PromptBuilder,
    SemanticTagger,
    TaxonomyLoader,
)


class TestTaxonomyLoader:
    """Tests for TaxonomyLoader class."""

    def test_load_valid_taxonomy(self, tmp_path: Path):
        """Test loading a valid taxonomy file."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Functional requirement"
  topic:
    description: "Topics"
    dynamic: true
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        loader = TaxonomyLoader(taxonomy_file)

        assert loader.taxonomy["version"] == "1.0"
        assert "req" in loader.taxonomy["namespaces"]
        assert "topic" in loader.taxonomy["namespaces"]

    def test_get_all_tags_excludes_dynamic(self, tmp_path: Path):
        """Test that get_all_tags excludes dynamic namespaces."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Functional requirement"
  topic:
    description: "Topics"
    dynamic: true
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        loader = TaxonomyLoader(taxonomy_file)
        tags = loader.get_all_tags()

        assert "req:functional" in tags
        # Dynamic namespaces should not have generated tags
        assert all(not tag.startswith("topic:") for tag in tags)

    def test_missing_config_file_raises_error(self):
        """Test that missing config file raises FileNotFoundError."""
        with pytest.raises(FileNotFoundError, match="Taxonomy config not found"):
            TaxonomyLoader("nonexistent.yaml")


class TestPromptBuilder:
    """Tests for PromptBuilder class."""

    def test_build_system_prompt_includes_taxonomy(self, tmp_path: Path):
        """Test that system prompt includes taxonomy tags."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Functional requirement"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        loader = TaxonomyLoader(taxonomy_file)
        builder = PromptBuilder(loader)

        prompt = builder.build_system_prompt()

        assert "req:functional" in prompt
        assert "Functional requirement" in prompt
        assert "ISO-29148" in prompt

    def test_build_user_prompt_formats_records(self, tmp_path: Path):
        """Test that user prompt correctly formats record batch."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Test"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        loader = TaxonomyLoader(taxonomy_file)
        builder = PromptBuilder(loader)

        records = [
            {
                "id": 1,
                "text": "The system shall validate passwords",
                "parent_header": "Security",
                "sheet_name": "Requirements",
            },
            {
                "id": 2,
                "text": "Users must authenticate",
                "parent_header": "Auth",
                "sheet_name": "Specs",
            },
        ]

        prompt = builder.build_user_prompt(records)

        assert "Block 1" in prompt
        assert "Block 2" in prompt
        assert "Security" in prompt
        assert "authenticate" in prompt


class TestBatchProcessor:
    """Tests for BatchProcessor class."""

    def test_create_batches_with_default_size(self):
        """Test batch creation with default batch size."""
        processor = BatchProcessor(batch_size=2)

        records = [{"id": i} for i in range(5)]
        batches = processor.create_batches(records)

        assert len(batches) == 3  # [2, 2, 1]
        assert len(batches[0]) == 2
        assert len(batches[1]) == 2
        assert len(batches[2]) == 1

    def test_create_batches_exact_division(self):
        """Test batch creation when records divide evenly."""
        processor = BatchProcessor(batch_size=3)

        records = [{"id": i} for i in range(6)]
        batches = processor.create_batches(records)

        assert len(batches) == 2
        assert len(batches[0]) == 3
        assert len(batches[1]) == 3

    def test_create_batches_empty_records(self):
        """Test batch creation with empty record list."""
        processor = BatchProcessor(batch_size=5)

        batches = processor.create_batches([])

        assert len(batches) == 0


class TestSemanticTagger:
    """Tests for SemanticTagger class."""

    def test_tag_records_with_mock_llm(self, tmp_path: Path):
        """Test tag_records with mocked LLM response."""
        # Create minimal taxonomy
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Functional requirement"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        # Mock chat service
        mock_chat_service = Mock()
        mock_response = json.dumps([
            {"id": 0, "tags": ["req:functional", "topic:security"]},
            {"id": 1, "tags": ["req:functional"]},
        ])
        mock_chat_service.invoke.return_value = mock_response

        # Create tagger
        tagger = SemanticTagger(
            taxonomy_path=taxonomy_file,
            batch_size=10,
            chat_service=mock_chat_service,
        )

        # Test records
        records = [
            {"text": "System shall validate", "parent_header": "Security", "sheet_name": "Sheet1"},
            {"text": "User can login", "parent_header": "Auth", "sheet_name": "Sheet1"},
        ]

        # Tag records
        result = tagger.tag_records(records)

        # Assertions
        assert len(result) == 2
        assert result[0]["tags"] == ["req:functional", "topic:security"]
        assert result[1]["tags"] == ["req:functional"]
        assert mock_chat_service.invoke.called

    def test_tag_records_handles_llm_error(self, tmp_path: Path):
        """Test that tag_records handles LLM errors gracefully."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Test"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        # Mock chat service that raises exception
        mock_chat_service = Mock()
        mock_chat_service.invoke.side_effect = Exception("LLM API error")

        tagger = SemanticTagger(
            taxonomy_path=taxonomy_file,
            batch_size=5,
            chat_service=mock_chat_service,
        )

        records = [
            {"text": "Test", "parent_header": "H1", "sheet_name": "S1"},
        ]

        # Should not raise, should return empty tags
        result = tagger.tag_records(records)

        assert len(result) == 1
        assert result[0]["tags"] == []

    def test_tag_records_handles_invalid_json(self, tmp_path: Path):
        """Test that tag_records handles invalid JSON from LLM."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Test"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        # Mock chat service returning invalid JSON
        mock_chat_service = Mock()
        mock_chat_service.invoke.return_value = "This is not JSON"

        tagger = SemanticTagger(
            taxonomy_path=taxonomy_file,
            batch_size=5,
            chat_service=mock_chat_service,
        )

        records = [
            {"text": "Test", "parent_header": "H1", "sheet_name": "S1"},
        ]

        result = tagger.tag_records(records)

        # Should handle gracefully with empty tags
        assert len(result) == 1
        assert result[0]["tags"] == []

    def test_tag_records_empty_list(self, tmp_path: Path):
        """Test tag_records with empty record list."""
        taxonomy_content = """
version: "1.0"
namespaces:
  req:
    description: "Requirements"
    tags:
      - name: functional
        description: "Test"
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)

        tagger = SemanticTagger(taxonomy_path=taxonomy_file)

        result = tagger.tag_records([])

        assert result == []


class TestAutoApproveThreshold:
    """Verifies the auto-approve env threshold drives _determine_tag_status."""

    def _make_tagger(self, tmp_path: Path, threshold: float) -> SemanticTagger:
        taxonomy_content = """
version: "1.0"
namespaces:
  topic:
    description: "Topics"
    dynamic: true
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)
        return SemanticTagger(
            taxonomy_path=taxonomy_file,
            chat_service=Mock(),
            auto_approve_threshold=threshold,
        )

    def test_killswitch_threshold_keeps_high_confidence_pending(
        self, tmp_path: Path
    ):
        tagger = self._make_tagger(tmp_path, threshold=1.01)
        assignments = tagger._process_llm_tags(
            [{"name": "topic:security", "confidence": 0.99}]
        )
        assert assignments[0].status == TagStatus.PENDING
        assert assignments[0].history[0].action.value == "suggested"

    def test_confidence_at_or_above_threshold_auto_verifies(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path, threshold=0.95)
        assignments = tagger._process_llm_tags(
            [{"name": "topic:security", "confidence": 0.95}]
        )
        assert assignments[0].status == TagStatus.VERIFIED
        assert assignments[0].history[0].action.value == "confirmed"

    def test_confidence_below_threshold_stays_pending(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path, threshold=0.95)
        assignments = tagger._process_llm_tags(
            [{"name": "topic:security", "confidence": 0.94}]
        )
        assert assignments[0].status == TagStatus.PENDING

    def test_settings_default_is_0_95(self):
        assert Settings().tag_auto_approve_threshold == 0.95

    def test_env_var_sets_threshold(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setenv("TAG_AUTO_APPROVE_THRESHOLD", "0.9")
        assert Settings().tag_auto_approve_threshold == 0.9


class TestVerifiedTagNamesProjection:
    """TEXTIQ-569: SemanticTagger emits a flat `verified_tag_names` per record.

    The Milvus JSON-path INVERTED index on this dynamic-envelope key powers
    the Chatbot tag-cascade filter. Verified-only; cap at 64; warn on overflow.
    """

    def _make_tagger(self, tmp_path: Path) -> SemanticTagger:
        taxonomy_content = """
version: "1.0"
namespaces:
  topic:
    description: "Topics"
    dynamic: true
"""
        taxonomy_file = tmp_path / "taxonomy.yaml"
        taxonomy_file.write_text(taxonomy_content)
        return SemanticTagger(taxonomy_path=taxonomy_file, chat_service=Mock())

    def test_project_filters_verified_only_from_tag_assignments(self, tmp_path: Path):
        from src.api.schemas.tags import TagAssignment, TagSource

        tagger = self._make_tagger(tmp_path)
        tags = [
            TagAssignment(tag_id="1", name="topic:a", source=TagSource.AI,
                          confidence=0.99, status=TagStatus.VERIFIED, history=[]),
            TagAssignment(tag_id="2", name="topic:b", source=TagSource.AI,
                          confidence=0.5, status=TagStatus.PENDING, history=[]),
            TagAssignment(tag_id="3", name="topic:c", source=TagSource.AI,
                          confidence=0.99, status=TagStatus.VERIFIED, history=[]),
        ]
        assert tagger._project_verified_tag_names(tags) == ["topic:a", "topic:c"]

    def test_project_filters_verified_only_from_dicts(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path)
        tags = [
            {"name": "topic:a", "status": "verified"},
            {"name": "topic:b", "status": "pending"},
            {"name": "topic:c", "status": "rejected"},
        ]
        assert tagger._project_verified_tag_names(tags) == ["topic:a"]

    def test_project_empty_inputs_return_empty(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path)
        assert tagger._project_verified_tag_names([]) == []
        assert tagger._project_verified_tag_names(None) == []

    def test_project_legacy_list_of_strings_returns_empty(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path)
        # Bare strings have no status → cannot claim "verified" → safe default [].
        assert tagger._project_verified_tag_names(["topic:foo", "domain:bar"]) == []

    def test_project_preserves_input_order(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path)
        tags = [
            {"name": "z", "status": "verified"},
            {"name": "a", "status": "verified"},
            {"name": "m", "status": "verified"},
        ]
        assert tagger._project_verified_tag_names(tags) == ["z", "a", "m"]

    def test_project_overflow_truncates_and_warns(self, tmp_path: Path):
        from src.extraction_v2.semantic_tagger import VERIFIED_TAG_NAMES_MAX_CAPACITY

        tagger = self._make_tagger(tmp_path)
        tags = [
            {"name": f"t{i:03d}", "status": "verified"}
            for i in range(VERIFIED_TAG_NAMES_MAX_CAPACITY + 5)
        ]
        with patch("src.extraction_v2.semantic_tagger.logger") as mock_logger:
            result = tagger._project_verified_tag_names(tags)
        assert len(result) == VERIFIED_TAG_NAMES_MAX_CAPACITY
        assert result[0] == "t000"
        mock_logger.warning.assert_called_once()
        args, kwargs = mock_logger.warning.call_args
        assert args[0] == "verified_tag_names_truncated"
        assert kwargs["dropped"] == 5

    def test_stamp_writes_field_on_every_record(self, tmp_path: Path):
        tagger = self._make_tagger(tmp_path)
        records = [
            {"id": 0, "tags": [{"name": "a", "status": "verified"},
                                {"name": "b", "status": "pending"}]},
            {"id": 1, "tags": []},
            {"id": 2},  # no tags key at all
        ]
        tagger._stamp_verified_tag_names(records)
        assert records[0]["verified_tag_names"] == ["a"]
        assert records[1]["verified_tag_names"] == []
        assert records[2]["verified_tag_names"] == []
