"""Integration tests for SqlAlchemyEntityRepository."""

from __future__ import annotations

from typing import TYPE_CHECKING
from uuid import uuid4

import pytest

from noteflow.domain.entities import Meeting
from noteflow.domain.entities.named_entity import EntityCategory, NamedEntity
from noteflow.domain.value_objects import MeetingId
from noteflow.infrastructure.persistence.repositories.entity_repo import (
    SqlAlchemyEntityRepository,
)
from noteflow.infrastructure.persistence.repositories.meeting_repo import (
    SqlAlchemyMeetingRepository,
)

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def entity_repo(session: AsyncSession) -> SqlAlchemyEntityRepository:
    """Create entity repository instance."""
    return SqlAlchemyEntityRepository(session)


@pytest.fixture
def meeting_repo(session: AsyncSession) -> SqlAlchemyMeetingRepository:
    """Create meeting repository for foreign key constraints."""
    return SqlAlchemyMeetingRepository(session)


@pytest.fixture
async def persisted_meeting(
    session: AsyncSession,
    meeting_repo: SqlAlchemyMeetingRepository,
) -> MeetingId:
    """Create and persist a meeting for foreign key constraints."""
    meeting = Meeting.create(title="Test Meeting")
    await meeting_repo.create(meeting)
    await session.flush()
    return meeting.id


# ============================================================================
# TestSave
# ============================================================================


@pytest.mark.integration
class TestEntityRepositorySave:
    """Tests for save() method."""

    async def test_saves_new_entity(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Save creates new entity and populates db_id."""
        entity = NamedEntity.create(
            text="Acme Corp",
            category=EntityCategory.COMPANY,
            segment_ids=[0, 1],
            confidence=0.95,
            meeting_id=persisted_meeting,
        )

        saved = await entity_repo.save(entity)
        await session.commit()

        assert saved.db_id is not None, "db_id should be populated after save"
        assert saved.text == "Acme Corp", "Text should be preserved"
        assert saved.category == EntityCategory.COMPANY, "Category should be preserved"

    @pytest.mark.parametrize(
        ("category", "text"),
        [
            pytest.param(EntityCategory.PERSON, "John Doe", id="person"),
            pytest.param(EntityCategory.COMPANY, "Google Inc", id="company"),
            pytest.param(EntityCategory.PRODUCT, "iPhone", id="product"),
            pytest.param(EntityCategory.LOCATION, "New York", id="location"),
            pytest.param(EntityCategory.DATE, "January 15", id="date"),
            pytest.param(EntityCategory.OTHER, "Some Term", id="other"),
        ],
    )
    async def test_saves_entity_with_category(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
        category: EntityCategory,
        text: str,
    ) -> None:
        """Save preserves entity category correctly."""
        entity = NamedEntity.create(
            text=text,
            category=category,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        saved = await entity_repo.save(entity)
        await session.commit()

        assert saved.category == category, f"Category {category} should be preserved"

    async def test_save_preserves_segment_ids(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Save preserves segment_ids list."""
        entity = NamedEntity.create(
            text="Important Person",
            category=EntityCategory.PERSON,
            segment_ids=[0, 2, 5, 7, 10],
            confidence=0.9,
            meeting_id=persisted_meeting,
        )

        await entity_repo.save(entity)
        await session.commit()

        retrieved = await entity_repo.get(entity.id)
        assert retrieved is not None, f"Entity {entity.id} should exist after save"
        assert retrieved.segment_ids == [0, 2, 5, 7, 10], "Segment IDs should be preserved"

    async def test_save_normalizes_text_to_lowercase(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Save normalizes text to lowercase for normalized_text field."""
        entity = NamedEntity.create(
            text="ACME CORP",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.9,
            meeting_id=persisted_meeting,
        )

        await entity_repo.save(entity)
        await session.commit()

        retrieved = await entity_repo.get(entity.id)
        assert retrieved is not None, f"Entity {entity.id} should exist after save"
        assert retrieved.text == "ACME CORP", "Original text preserved"
        assert retrieved.normalized_text == "acme corp", "Normalized text is lowercase"


# ============================================================================
# TestSaveBatch
# ============================================================================


@pytest.mark.integration
class TestEntityRepositorySaveBatch:
    """Tests for save_batch() method."""

    async def test_saves_multiple_entities(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Save batch creates all entities with db_ids."""
        entities = [
            NamedEntity.create(
                text=f"Entity {i}",
                category=EntityCategory.COMPANY,
                segment_ids=[i],
                confidence=0.8,
                meeting_id=persisted_meeting,
            )
            for i in range(5)
        ]

        saved = await entity_repo.save_batch(entities)
        await session.commit()

        assert len(saved) == 5, "Should return all entities"
        missing_db_id = [e for e in saved if e.db_id is None]
        assert not missing_db_id, f"All entities should have db_id, but {len(missing_db_id)} are missing"

    async def test_saves_empty_batch(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
    ) -> None:
        """Save batch handles empty list gracefully."""
        saved = await entity_repo.save_batch([])
        await session.commit()

        assert saved == [], "Should return empty list"


# ============================================================================
# TestGet
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryGet:
    """Tests for get() method."""

    async def test_retrieves_existing_entity(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Get retrieves entity by ID."""
        entity = NamedEntity.create(
            text="Test Company",
            category=EntityCategory.COMPANY,
            segment_ids=[0, 1, 2],
            confidence=0.95,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        retrieved = await entity_repo.get(entity.id)

        assert retrieved is not None, "Should find entity"
        assert retrieved.text == "Test Company", "Text should match"
        assert retrieved.category == EntityCategory.COMPANY, "Category should match"
        assert retrieved.confidence == 0.95, "Confidence should match"

    async def test_get_returns_none_for_nonexistent_entity(
        self,
        entity_repo: SqlAlchemyEntityRepository,
    ) -> None:
        """Get returns None for nonexistent entity."""
        nonexistent_id = uuid4()

        retrieved = await entity_repo.get(nonexistent_id)

        assert retrieved is None, "Should return None"

    async def test_get_converts_category_string_to_enum(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Get converts category string to EntityCategory enum."""
        entity = NamedEntity.create(
            text="Jane Doe",
            category=EntityCategory.PERSON,
            segment_ids=[0],
            confidence=0.9,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        retrieved = await entity_repo.get(entity.id)

        assert retrieved is not None, f"Entity {entity.id} should exist after save"
        assert isinstance(retrieved.category, EntityCategory), "Category should be enum"
        assert retrieved.category == EntityCategory.PERSON, f"Expected PERSON category, got {retrieved.category}"


# ============================================================================
# TestGetByMeeting
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryGetByMeeting:
    """Tests for get_by_meeting() method."""

    async def test_returns_all_entities_for_meeting(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Get by meeting returns all entities for the meeting."""
        entities = [
            NamedEntity.create(
                text=f"Entity {i}",
                category=EntityCategory.COMPANY,
                segment_ids=[i],
                confidence=0.8,
                meeting_id=persisted_meeting,
            )
            for i in range(3)
        ]
        await entity_repo.save_batch(entities)
        await session.commit()

        result = await entity_repo.get_by_meeting(persisted_meeting)

        assert len(result) == 3, "Should return all 3 entities"

    async def test_returns_empty_when_no_entities(
        self,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Get by meeting returns empty list when no entities exist."""
        result = await entity_repo.get_by_meeting(persisted_meeting)

        assert result == [], "Should return empty list"

    async def test_orders_by_category_then_text(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Get by meeting orders by category and text alphabetically."""
        # Create entities with different categories and texts (insert in random order)
        entities = [
            NamedEntity.create(
                text="Zebra Corp",
                category=EntityCategory.COMPANY,
                segment_ids=[0],
                confidence=0.8,
                meeting_id=persisted_meeting,
            ),
            NamedEntity.create(
                text="Alice",
                category=EntityCategory.PERSON,
                segment_ids=[1],
                confidence=0.8,
                meeting_id=persisted_meeting,
            ),
            NamedEntity.create(
                text="Acme Inc",
                category=EntityCategory.COMPANY,
                segment_ids=[2],
                confidence=0.8,
                meeting_id=persisted_meeting,
            ),
            NamedEntity.create(
                text="Bob",
                category=EntityCategory.PERSON,
                segment_ids=[3],
                confidence=0.8,
                meeting_id=persisted_meeting,
            ),
        ]
        await entity_repo.save_batch(entities)
        await session.commit()

        result = await entity_repo.get_by_meeting(persisted_meeting)

        # Check ordering (category first, then text within category)
        texts = [e.text for e in result]
        assert texts == sorted(texts, key=lambda t: (result[texts.index(t)].category.value, t)), f"Entities should be ordered by category then text, got {texts}"

    async def test_isolates_meetings(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        meeting_repo: SqlAlchemyMeetingRepository,
    ) -> None:
        """Get by meeting only returns entities for specified meeting."""
        meeting1 = Meeting.create(title="Meeting 1")
        meeting2 = Meeting.create(title="Meeting 2")
        await meeting_repo.create(meeting1)
        await meeting_repo.create(meeting2)
        await session.flush()

        entity1 = NamedEntity.create(
            text="Meeting 1 Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=meeting1.id,
        )
        entity2 = NamedEntity.create(
            text="Meeting 2 Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=meeting2.id,
        )
        await entity_repo.save(entity1)
        await entity_repo.save(entity2)
        await session.commit()

        result = await entity_repo.get_by_meeting(meeting1.id)

        assert len(result) == 1, "Should return only meeting1 entities"
        assert result[0].text == "Meeting 1 Entity", f"Expected 'Meeting 1 Entity', got '{result[0].text}'"


# ============================================================================
# TestExistsForMeeting
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryExistsForMeeting:
    """Tests for exists_for_meeting() method."""

    async def test_returns_true_when_entities_exist(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Exists for meeting returns True when entities exist."""
        entity = NamedEntity.create(
            text="Test Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.exists_for_meeting(persisted_meeting)

        assert result is True, "Should return True"

    async def test_returns_false_when_no_entities(
        self,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Exists for meeting returns False when no entities exist."""
        result = await entity_repo.exists_for_meeting(persisted_meeting)

        assert result is False, "Should return False"


# ============================================================================
# TestUpdatePinned
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryUpdatePinned:
    """Tests for update_pinned() method."""

    async def test_pins_entity(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Update pinned sets is_pinned to True."""
        entity = NamedEntity.create(
            text="Pin Me",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        assert entity.is_pinned is False, "Initially unpinned"

        result = await entity_repo.update_pinned(entity.id, is_pinned=True)
        await session.commit()

        assert result is True, "Should return True on success"

        retrieved = await entity_repo.get(entity.id)
        assert retrieved is not None, f"Entity {entity.id} should exist after update"
        assert retrieved.is_pinned is True, "Should be pinned"

    async def test_unpins_entity(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Update pinned sets is_pinned to False."""
        entity = NamedEntity(
            meeting_id=persisted_meeting,
            text="Unpin Me",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            is_pinned=True,  # Start pinned
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.update_pinned(entity.id, is_pinned=False)
        await session.commit()

        assert result is True, "Should return True"

        retrieved = await entity_repo.get(entity.id)
        assert retrieved is not None, f"Entity {entity.id} should exist after update"
        assert retrieved.is_pinned is False, "Should be unpinned"

    async def test_update_pinned_returns_false_for_nonexistent(
        self,
        entity_repo: SqlAlchemyEntityRepository,
    ) -> None:
        """Update pinned returns False for nonexistent entity."""
        nonexistent_id = uuid4()

        result = await entity_repo.update_pinned(nonexistent_id, is_pinned=True)

        assert result is False, "Should return False"


# ============================================================================
# TestUpdate
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryUpdate:
    """Tests for update() method."""

    async def test_updates_text(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Update modifies text and normalized_text."""
        entity = NamedEntity.create(
            text="Original Text",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.update(entity.id, text="New Text")
        await session.commit()

        assert result is not None, "Should return updated entity"
        assert result.text == "New Text", "Text should be updated"
        assert result.normalized_text == "new text", "Normalized text should update"

    async def test_updates_category(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Update modifies category."""
        entity = NamedEntity.create(
            text="Some Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.update(entity.id, category="person")
        await session.commit()

        assert result is not None, "Should return updated entity"
        assert result.category == EntityCategory.PERSON, "Category should be updated"

    async def test_updates_both_fields(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Update modifies both text and category."""
        entity = NamedEntity.create(
            text="Original",
            category=EntityCategory.OTHER,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.update(entity.id, text="John Smith", category="person")
        await session.commit()

        assert result is not None, f"Entity {entity.id} should exist after update"
        assert result.text == "John Smith", "Text updated"
        assert result.category == EntityCategory.PERSON, "Category updated"

    async def test_returns_none_for_nonexistent(
        self,
        entity_repo: SqlAlchemyEntityRepository,
    ) -> None:
        """Update returns None for nonexistent entity."""
        nonexistent_id = uuid4()

        result = await entity_repo.update(nonexistent_id, text="New Text")

        assert result is None, "Should return None"


# ============================================================================
# TestDelete
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryDelete:
    """Tests for delete() method."""

    async def test_deletes_existing_entity(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Delete removes entity and returns True."""
        entity = NamedEntity.create(
            text="Delete Me",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=persisted_meeting,
        )
        await entity_repo.save(entity)
        await session.commit()

        result = await entity_repo.delete(entity.id)
        await session.commit()

        assert result is True, "Should return True"

        retrieved = await entity_repo.get(entity.id)
        assert retrieved is None, "Entity should be deleted"

    async def test_entity_delete_returns_false_for_nonexistent(
        self,
        entity_repo: SqlAlchemyEntityRepository,
    ) -> None:
        """Delete returns False for nonexistent entity."""
        nonexistent_id = uuid4()

        result = await entity_repo.delete(nonexistent_id)

        assert result is False, "Should return False"


# ============================================================================
# TestDeleteByMeeting
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryDeleteByMeeting:
    """Tests for delete_by_meeting() method."""

    async def test_deletes_all_meeting_entities(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Delete by meeting removes all entities for the meeting."""
        entities = [
            NamedEntity.create(
                text=f"Entity {i}",
                category=EntityCategory.COMPANY,
                segment_ids=[i],
                confidence=0.8,
                meeting_id=persisted_meeting,
            )
            for i in range(5)
        ]
        await entity_repo.save_batch(entities)
        await session.commit()

        count = await entity_repo.delete_by_meeting(persisted_meeting)
        await session.commit()

        assert count == 5, "Should return count of deleted entities"

        remaining = await entity_repo.get_by_meeting(persisted_meeting)
        assert remaining == [], "All entities should be deleted"

    async def test_returns_zero_when_no_entities(
        self,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Delete by meeting returns 0 when no entities exist."""
        count = await entity_repo.delete_by_meeting(persisted_meeting)

        assert count == 0, "Should return 0"

    async def test_isolates_deletion_to_meeting(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        meeting_repo: SqlAlchemyMeetingRepository,
    ) -> None:
        """Delete by meeting only removes entities for specified meeting."""
        meeting1 = Meeting.create(title="Meeting 1")
        meeting2 = Meeting.create(title="Meeting 2")
        await meeting_repo.create(meeting1)
        await meeting_repo.create(meeting2)
        await session.flush()

        entity1 = NamedEntity.create(
            text="Meeting 1 Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=meeting1.id,
        )
        entity2 = NamedEntity.create(
            text="Meeting 2 Entity",
            category=EntityCategory.COMPANY,
            segment_ids=[0],
            confidence=0.8,
            meeting_id=meeting2.id,
        )
        await entity_repo.save(entity1)
        await entity_repo.save(entity2)
        await session.commit()

        await entity_repo.delete_by_meeting(meeting1.id)
        await session.commit()

        meeting1_entities = await entity_repo.get_by_meeting(meeting1.id)
        meeting2_entities = await entity_repo.get_by_meeting(meeting2.id)

        assert meeting1_entities == [], "Meeting 1 entities should be deleted"
        assert len(meeting2_entities) == 1, "Meeting 2 entities should remain"


# ============================================================================
# TestRoundTrip
# ============================================================================


@pytest.mark.integration
class TestEntityRepositoryRoundTrip:
    """Tests for round-trip conversion fidelity."""

    async def test_round_trip_preserves_all_fields(
        self,
        session: AsyncSession,
        entity_repo: SqlAlchemyEntityRepository,
        persisted_meeting: MeetingId,
    ) -> None:
        """Save and retrieve preserves all NamedEntity fields."""
        original = NamedEntity.create(
            text="Microsoft Corporation",
            category=EntityCategory.COMPANY,
            segment_ids=[0, 2, 5, 8, 10],
            confidence=0.92,
            meeting_id=persisted_meeting,
        )

        await entity_repo.save(original)
        await session.commit()

        retrieved = await entity_repo.get(original.id)

        assert retrieved is not None, "Should retrieve entity"
        assert retrieved.id == original.id, "ID preserved"
        assert retrieved.meeting_id == original.meeting_id, "Meeting ID preserved"
        assert retrieved.text == original.text, "Text preserved"
        assert retrieved.normalized_text == original.normalized_text, "Normalized text preserved"
        assert retrieved.category == original.category, "Category preserved"
        assert retrieved.segment_ids == original.segment_ids, "Segment IDs preserved"
        assert retrieved.confidence == original.confidence, "Confidence preserved"
        assert retrieved.is_pinned == original.is_pinned, "is_pinned preserved"

    async def test_confidence_validation_on_domain(
        self,
        persisted_meeting: MeetingId,
    ) -> None:
        """Confidence validation enforced at domain level."""
        with pytest.raises(ValueError, match="confidence must be between 0 and 1"):
            NamedEntity.create(
                text="Invalid",
                category=EntityCategory.COMPANY,
                segment_ids=[0],
                confidence=1.5,  # Invalid: > 1.0
                meeting_id=persisted_meeting,
            )

        with pytest.raises(ValueError, match="confidence must be between 0 and 1"):
            NamedEntity.create(
                text="Invalid",
                category=EntityCategory.COMPANY,
                segment_ids=[0],
                confidence=-0.1,  # Invalid: < 0.0
                meeting_id=persisted_meeting,
            )
