"""Tests for NER engine (spaCy wrapper)."""

from __future__ import annotations

import pytest

from noteflow.domain.entities.named_entity import EntityCategory, NamedEntity
from noteflow.infrastructure.ner import NerEngine


@pytest.fixture(scope="module")
def ner_engine() -> NerEngine:
    """Create NER engine (module-scoped to avoid repeated model loads)."""
    return NerEngine(model_name="en_core_web_sm")


class TestNerEngineBasics:
    """Basic NER engine functionality tests."""

    def test_is_ready_before_load(self) -> None:
        """Engine is not ready before first use."""
        engine = NerEngine()
        assert not engine.is_ready(), "Engine should not be ready before model is loaded"

    def test_is_ready_after_extract(self, ner_engine: NerEngine) -> None:
        """Engine is ready after extraction triggers lazy load."""
        ner_engine.extract("Hello, John.")
        assert ner_engine.is_ready(), "Engine should be ready after extraction triggers lazy load"


class TestEntityExtraction:
    """Tests for entity extraction from text."""

    @pytest.mark.parametrize(
        ("text", "expected_category", "expected_text"),
        [
            ("I met John Smith yesterday.", EntityCategory.PERSON, "John Smith"),
            ("Apple Inc. announced new products.", EntityCategory.COMPANY, "Apple Inc."),
            ("We visited New York City.", EntityCategory.LOCATION, "New York City"),
            ("The meeting is scheduled for Monday.", EntityCategory.DATE, "Monday"),
        ],
    )
    def test_extract_entity_types(
        self,
        ner_engine: NerEngine,
        text: str,
        expected_category: EntityCategory,
        expected_text: str,
    ) -> None:
        """Extract entities of various types."""
        entities = ner_engine.extract(text)
        matching = [e for e in entities if e.category == expected_category]
        assert matching, f"No {expected_category.value} entities found in: {text}"
        texts = [e.text for e in matching]
        assert expected_text in texts, f"Expected '{expected_text}' not found in {texts}"

    def test_extract_returns_list(self, ner_engine: NerEngine) -> None:
        """Extract returns a list of NamedEntity objects."""
        entities = ner_engine.extract("John works at Google.")
        assert isinstance(entities, list), "extract() should return a list"
        assert all(isinstance(e, NamedEntity) for e in entities), "All items should be NamedEntity instances"

    def test_extract_empty_text_returns_empty(self, ner_engine: NerEngine) -> None:
        """Empty text returns empty list."""
        entities = ner_engine.extract("")
        assert entities == [], "Empty text should return an empty list"

    def test_extract_no_entities_returns_empty(self, ner_engine: NerEngine) -> None:
        """Text with no entities returns empty list."""
        entities = ner_engine.extract("The quick brown fox.")
        # May still find entities depending on model, but should not crash
        assert isinstance(entities, list), "Result should be a list even with no entities"


class TestSegmentExtraction:
    """Tests for extraction from multiple segments."""

    def test_extract_from_segments_tracks_segment_ids(self, ner_engine: NerEngine) -> None:
        """Segment extraction tracks which segments contain each entity."""
        segments = [
            (0, "John talked to Mary."),
            (1, "Mary went to the store."),
            (2, "Then John called Mary again."),
        ]
        entities = ner_engine.extract_from_segments(segments)

        # Mary should appear in multiple segments
        mary_entities = [e for e in entities if "mary" in e.normalized_text]
        assert mary_entities, "Mary entity not found"
        mary = mary_entities[0]
        assert len(mary.segment_ids) >= 2, "Mary should appear in multiple segments"

    def test_extract_from_segments_deduplicates(self, ner_engine: NerEngine) -> None:
        """Same entity in multiple segments is deduplicated."""
        segments = [
            (0, "John Smith is here."),
            (1, "John Smith left."),
        ]
        entities = ner_engine.extract_from_segments(segments)

        # Should have one John Smith entity (deduplicated by normalized text)
        john_entities = [e for e in entities if "john" in e.normalized_text]
        assert len(john_entities) == 1, "John Smith should be deduplicated"
        assert len(john_entities[0].segment_ids) == 2, "Should track both segments"

    def test_extract_from_segments_empty_returns_empty(self, ner_engine: NerEngine) -> None:
        """Empty segments list returns empty entities."""
        entities = ner_engine.extract_from_segments([])
        assert entities == [], "Empty segments list should return an empty entities list"


class TestEntityNormalization:
    """Tests for entity text normalization."""

    def test_normalized_text_is_lowercase(self, ner_engine: NerEngine) -> None:
        """Normalized text should be lowercase."""
        entities = ner_engine.extract("John SMITH went to NYC.")
        non_lowercase = [e for e in entities if e.normalized_text != e.normalized_text.lower()]
        assert not non_lowercase, f"All normalized text should be lowercase, but found: {[e.normalized_text for e in non_lowercase]}"

    def test_confidence_is_set(self, ner_engine: NerEngine) -> None:
        """Entities should have confidence score."""
        entities = ner_engine.extract("Microsoft Corporation is based in Seattle.")
        assert entities, "Should find entities"
        invalid_confidence = [e for e in entities if not (0.0 <= e.confidence <= 1.0)]
        assert not invalid_confidence, f"All entities should have confidence between 0 and 1, but found: {[(e.text, e.confidence) for e in invalid_confidence]}"
