"""Tests for NER application service."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest

from noteflow.application.services.ner_service import ExtractionResult, NerService
from noteflow.domain.entities import Meeting, Segment
from noteflow.domain.entities.named_entity import EntityCategory, NamedEntity
from noteflow.domain.value_objects import MeetingId


class MockNerEngine:
    """Mock NER engine for testing."""

    def __init__(self, entities: list[NamedEntity] | None = None) -> None:
        self._entities = entities or []
        self._ready = False
        self.extract_call_count = 0

    def extract(self, text: str) -> list[NamedEntity]:
        """Extract entities from text (mock)."""
        self._ready = True
        self.extract_call_count += 1
        return self._entities

    def extract_from_segments(
        self, segments: list[tuple[int, str]]
    ) -> list[NamedEntity]:
        """Extract entities from segments (mock)."""
        self._ready = True
        self.extract_call_count += 1
        return self._entities

    def is_ready(self) -> bool:
        """Check if engine is ready."""
        return self._ready

    def set_ready(self, ready: bool) -> None:
        """Set the ready state for testing."""
        self._ready = ready


def _create_entity(
    text: str,
    category: EntityCategory = EntityCategory.PERSON,
    segment_ids: list[int] | None = None,
) -> NamedEntity:
    """Create a test entity."""
    return NamedEntity(
        text=text,
        normalized_text=text.lower(),
        category=category,
        segment_ids=segment_ids or [0],
        confidence=0.9,
    )


def _create_segment(segment_id: int, text: str) -> Segment:
    """Create a test segment."""
    return Segment(
        segment_id=segment_id,
        text=text,
        start_time=segment_id * 5.0,
        end_time=(segment_id + 1) * 5.0,
    )


class TestNerServiceExtraction:
    """Tests for entity extraction."""

    @pytest.fixture
    def mock_uow_factory(self, mock_uow: MagicMock) -> MagicMock:
        """Create mock UoW factory."""
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=[])
        mock_uow.entities.save_batch = AsyncMock(return_value=[])
        mock_uow.entities.delete_by_meeting = AsyncMock(return_value=0)
        mock_uow.segments.get_by_meeting = AsyncMock(return_value=[])
        return MagicMock(return_value=mock_uow)

    @pytest.fixture
    def sample_meeting(self) -> Meeting:
        """Create a sample meeting with segments."""
        meeting = Meeting.create(title="Test Meeting")
        meeting.segments.extend([
            _create_segment(0, "John talked to Mary."),
            _create_segment(1, "They discussed the project."),
        ])
        return meeting

    @pytest.fixture
    def mock_ner_engine(self) -> MockNerEngine:
        """Create mock NER engine with sample entities."""
        return MockNerEngine(
            entities=[
                _create_entity("John", EntityCategory.PERSON, [0]),
                _create_entity("Mary", EntityCategory.PERSON, [0]),
            ]
        )

    async def test_extract_entities_returns_result(
        self,
        mock_uow: MagicMock,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        sample_meeting: Meeting,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Extract entities returns ExtractionResult."""
        mock_uow.meetings.get = AsyncMock(return_value=sample_meeting)
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=[])
        mock_uow.entities.save_batch = AsyncMock(return_value=[])
        mock_uow.segments.get_by_meeting = AsyncMock(return_value=sample_meeting.segments)

        # Mock feature flag
        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )

        service = NerService(mock_ner_engine, mock_uow_factory)
        result = await service.extract_entities(sample_meeting.id)

        assert isinstance(result, ExtractionResult), "Should return ExtractionResult"
        assert result.total_count == 2, "Should have 2 entities"
        assert not result.cached, "Should not be cached on first extraction"
        assert len(result.entities) == 2, "Entities list should match total_count"

    async def test_extract_entities_uses_cache(
        self,
        mock_uow: MagicMock,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Returns cached entities without re-extraction."""
        cached_entities = [_create_entity("Cached", EntityCategory.PERSON)]
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=cached_entities)

        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )

        service = NerService(mock_ner_engine, mock_uow_factory)
        result = await service.extract_entities(MeetingId(uuid4()))

        assert result.cached
        assert result.total_count == 1
        assert mock_ner_engine.extract_call_count == 0

    async def test_extract_entities_force_refresh_bypasses_cache(
        self,
        mock_uow: MagicMock,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        sample_meeting: Meeting,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Force refresh re-extracts even with cached data."""
        cached_entities = [_create_entity("Cached", EntityCategory.PERSON)]
        mock_uow.meetings.get = AsyncMock(return_value=sample_meeting)
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=cached_entities)
        mock_uow.entities.save_batch = AsyncMock(return_value=[])
        mock_uow.entities.delete_by_meeting = AsyncMock(return_value=1)
        mock_uow.segments.get_by_meeting = AsyncMock(return_value=sample_meeting.segments)

        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )

        # Mark engine as ready to skip warmup extraction
        mock_ner_engine.set_ready(True)
        initial_count = mock_ner_engine.extract_call_count

        service = NerService(mock_ner_engine, mock_uow_factory)
        result = await service.extract_entities(sample_meeting.id, force_refresh=True)

        assert not result.cached
        assert mock_ner_engine.extract_call_count == initial_count + 1
        mock_uow.entities.delete_by_meeting.assert_called_once()

    async def test_extract_entities_meeting_not_found(
        self,
        mock_uow: MagicMock,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Raises ValueError when meeting not found."""
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=[])
        mock_uow.meetings.get = AsyncMock(return_value=None)

        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )

        service = NerService(mock_ner_engine, mock_uow_factory)
        meeting_id = MeetingId(uuid4())

        with pytest.raises(ValueError, match=str(meeting_id)):
            await service.extract_entities(meeting_id)

    async def test_extract_entities_feature_disabled(
        self,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Raises RuntimeError when feature flag is disabled."""
        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=False),
        )

        service = NerService(mock_ner_engine, mock_uow_factory)

        with pytest.raises(RuntimeError, match="disabled"):
            await service.extract_entities(MeetingId(uuid4()))

    async def test_extract_entities_no_segments_returns_empty(
        self,
        mock_uow: MagicMock,
        mock_uow_factory: MagicMock,
        mock_ner_engine: MockNerEngine,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Returns empty result for meeting with no segments."""
        meeting = Meeting.create(title="Empty Meeting")
        mock_uow.meetings.get = AsyncMock(return_value=meeting)
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=[])

        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )

        service = NerService(mock_ner_engine, mock_uow_factory)
        result = await service.extract_entities(meeting.id)

        assert result.total_count == 0
        assert result.entities == []
        assert not result.cached


class TestNerServicePinning:
    """Tests for entity pinning."""

    async def test_pin_entity_success(self, mock_uow: MagicMock) -> None:
        """Pin entity updates database and returns True."""
        mock_uow.entities.update_pinned = AsyncMock(return_value=True)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        entity_id = uuid4()

        result = await service.pin_entity(entity_id, is_pinned=True)

        assert result is True
        mock_uow.entities.update_pinned.assert_called_once_with(entity_id, True)
        mock_uow.commit.assert_called_once()

    async def test_pin_entity_not_found(self, mock_uow: MagicMock) -> None:
        """Pin entity returns False when entity not found."""
        mock_uow.entities.update_pinned = AsyncMock(return_value=False)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        entity_id = uuid4()

        result = await service.pin_entity(entity_id)

        assert result is False
        mock_uow.commit.assert_not_called()


class TestNerServiceClear:
    """Tests for clearing entities."""

    async def test_clear_entities(self, mock_uow: MagicMock) -> None:
        """Clear entities deletes all for meeting."""
        mock_uow.entities.delete_by_meeting = AsyncMock(return_value=5)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        meeting_id = MeetingId(uuid4())

        count = await service.clear_entities(meeting_id)

        assert count == 5
        mock_uow.entities.delete_by_meeting.assert_called_once_with(meeting_id)
        mock_uow.commit.assert_called_once()


class TestNerServiceHelpers:
    """Tests for helper methods."""

    async def test_get_entities(self, mock_uow: MagicMock) -> None:
        """Get entities returns cached entities without extraction."""
        entities = [_create_entity("Test")]
        mock_uow.entities.get_by_meeting = AsyncMock(return_value=entities)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        meeting_id = MeetingId(uuid4())

        result = await service.get_entities(meeting_id)

        assert result == entities
        mock_uow.entities.get_by_meeting.assert_called_once_with(meeting_id)

    async def test_has_entities_true(self, mock_uow: MagicMock) -> None:
        """Has entities returns True when entities exist."""
        mock_uow.entities.exists_for_meeting = AsyncMock(return_value=True)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        meeting_id = MeetingId(uuid4())

        result = await service.has_entities(meeting_id)

        assert result is True

    async def test_has_entities_false(self, mock_uow: MagicMock) -> None:
        """Has entities returns False when no entities exist."""
        mock_uow.entities.exists_for_meeting = AsyncMock(return_value=False)
        mock_uow_factory = MagicMock(return_value=mock_uow)

        service = NerService(MockNerEngine(), mock_uow_factory)
        meeting_id = MeetingId(uuid4())

        result = await service.has_entities(meeting_id)

        assert result is False

    def test_is_engine_ready_checks_both_flag_and_engine(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Is engine ready checks both feature flag and engine state."""
        engine = MockNerEngine()
        service = NerService(engine, MagicMock())

        # NER disabled - should be False regardless of engine state
        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=False),
        )
        engine.set_ready(True)
        assert not service.is_engine_ready(), "Should be False when NER disabled"

        # NER enabled but engine not ready
        monkeypatch.setattr(
            "noteflow.application.services.ner_service.get_feature_flags",
            lambda: MagicMock(ner_enabled=True),
        )
        engine.set_ready(False)
        assert not service.is_engine_ready(), "Should be False when engine not ready"

        # NER enabled and engine ready
        engine.set_ready(True)
        assert service.is_engine_ready(), "Should be True when both conditions met"
