"""Integration tests for SQLAlchemy repositories."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING
from uuid import uuid4

import pytest

from noteflow.domain.entities import Annotation, Meeting, Segment, Summary, WordTiming
from noteflow.domain.entities.summary import ActionItem, KeyPoint
from noteflow.domain.value_objects import AnnotationId, AnnotationType, MeetingId, MeetingState
from noteflow.infrastructure.persistence.repositories import (
    SqlAlchemyAnnotationRepository,
    SqlAlchemyMeetingRepository,
    SqlAlchemySegmentRepository,
    SqlAlchemySummaryRepository,
)
from tests.conftest import approx_sequence

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession


@pytest.mark.integration
class TestMeetingRepository:
    """Integration tests for SqlAlchemyMeetingRepository."""

    async def test_create_and_get_meeting(self, session: AsyncSession) -> None:
        """Test creating and retrieving a meeting."""
        repo = SqlAlchemyMeetingRepository(session)
        meeting = Meeting.create(title="Test Meeting", metadata={"key": "value"})

        # Create
        await repo.create(meeting)
        await session.commit()

        # Get
        retrieved = await repo.get(meeting.id)

        assert retrieved is not None, "meeting should exist after create"
        assert retrieved.id == meeting.id, "ID should match"
        assert retrieved.title == "Test Meeting", "title should match"
        assert retrieved.state == MeetingState.CREATED, "state should be CREATED"
        assert retrieved.metadata == {"key": "value"}, "metadata should match"

    async def test_get_meeting_not_found_repository(self, session: AsyncSession) -> None:
        """Test retrieving non-existent meeting returns None."""
        repo = SqlAlchemyMeetingRepository(session)
        meeting_id = MeetingId(Meeting.create().id)

        result = await repo.get(meeting_id)

        assert result is None, f"expected None for non-existent meeting, got {result}"

    async def test_update_meeting(self, session: AsyncSession) -> None:
        """Test updating a meeting."""
        repo = SqlAlchemyMeetingRepository(session)
        meeting = Meeting.create(title="Original")
        await repo.create(meeting)
        await session.commit()

        # Update state and title
        meeting.start_recording()
        await repo.update(meeting)
        await session.commit()

        # Verify
        retrieved = await repo.get(meeting.id)
        assert retrieved is not None, "updated meeting should exist"
        assert retrieved.state == MeetingState.RECORDING, f"expected RECORDING state, got {retrieved.state}"
        assert retrieved.started_at is not None, "started_at should be set after start_recording()"

    async def test_delete_meeting(self, session: AsyncSession) -> None:
        """Test deleting a meeting."""
        repo = SqlAlchemyMeetingRepository(session)
        meeting = Meeting.create(title="To Delete")
        await repo.create(meeting)
        await session.commit()

        # Delete
        result = await repo.delete(meeting.id)
        await session.commit()

        assert result is True, "delete should return True for existing meeting"

        # Verify deleted
        retrieved = await repo.get(meeting.id)
        assert retrieved is None, "meeting should not exist after deletion"

    async def test_delete_meeting_not_found_repository(self, session: AsyncSession) -> None:
        """Test deleting non-existent meeting returns False."""
        repo = SqlAlchemyMeetingRepository(session)
        meeting_id = MeetingId(Meeting.create().id)

        result = await repo.delete(meeting_id)

        assert result is False, "delete should return False for non-existent meeting"

    async def test_list_all_meetings(self, session: AsyncSession) -> None:
        """Test listing all meetings with pagination."""
        repo = SqlAlchemyMeetingRepository(session)

        # Create multiple meetings
        meetings = [Meeting.create(title=f"Meeting {i}") for i in range(5)]
        for m in meetings:
            await repo.create(m)
        await session.commit()

        # List with pagination
        page_limit = 3
        result, total = await repo.list_all(limit=page_limit, offset=0)

        assert len(result) == page_limit, f"expected {page_limit} meetings, got {len(result)}"
        expected_total = len(meetings)
        assert total == expected_total, f"expected total {expected_total}, got {total}"

    async def test_list_meetings_filter_by_state(self, session: AsyncSession) -> None:
        """Test filtering meetings by state."""
        repo = SqlAlchemyMeetingRepository(session)

        # Create meetings in different states
        created = Meeting.create(title="Created")
        await repo.create(created)

        recording = Meeting.create(title="Recording")
        recording.start_recording()
        await repo.create(recording)
        await session.commit()

        # Filter by RECORDING state
        result, _ = await repo.list_all(states=[MeetingState.RECORDING])

        assert len(result) == 1, f"expected 1 meeting in RECORDING state, got {len(result)}"
        assert result[0].title == "Recording", f"expected 'Recording' title, got '{result[0].title}'"

    async def test_count_by_state(self, session: AsyncSession) -> None:
        """Test counting meetings by state."""
        repo = SqlAlchemyMeetingRepository(session)

        # Create meetings
        for _ in range(3):
            await repo.create(Meeting.create())
        await session.commit()

        count = await repo.count_by_state(MeetingState.CREATED)

        assert count == 3, f"expected count of 3 CREATED meetings, got {count}"


@pytest.mark.integration
class TestSegmentRepository:
    """Integration tests for SqlAlchemySegmentRepository."""

    async def test_add_and_get_segments(self, session: AsyncSession) -> None:
        """Test adding and retrieving segments."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        # Create meeting first
        meeting = Meeting.create(title="Test")
        await meeting_repo.create(meeting)
        await session.commit()

        # Add segments
        segment = Segment(
            segment_id=0,
            text="Hello world",
            start_time=0.0,
            end_time=2.5,
            meeting_id=meeting.id,
            language="en",
        )
        await segment_repo.add(meeting.id, segment)
        await session.commit()

        # Get segments
        result = await segment_repo.get_by_meeting(meeting.id)

        assert len(result) == 1, f"expected 1 segment, got {len(result)}"
        assert result[0].text == "Hello world", f"expected text 'Hello world', got '{result[0].text}'"
        assert result[0].db_id is not None, "segment db_id should be set after persistence"

    async def test_add_segment_with_words(self, session: AsyncSession) -> None:
        """Test adding segment with word-level timing."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        words = [
            WordTiming(word="Hello", start_time=0.0, end_time=0.5, probability=0.95),
            WordTiming(word="world", start_time=0.5, end_time=1.0, probability=0.98),
        ]
        segment = Segment(
            segment_id=0,
            text="Hello world",
            start_time=0.0,
            end_time=1.0,
            meeting_id=meeting.id,
            words=words,
        )
        await segment_repo.add(meeting.id, segment)
        await session.commit()

        result = await segment_repo.get_by_meeting(meeting.id, include_words=True)

        assert len(result[0].words) == 2, f"expected 2 word timings, got {len(result[0].words)}"
        assert result[0].words[0].word == "Hello", f"expected first word 'Hello', got '{result[0].words[0].word}'"

    async def test_add_batch_segments(self, session: AsyncSession) -> None:
        """Test batch adding segments."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        segments = [
            Segment(segment_id=i, text=f"Segment {i}", start_time=float(i), end_time=float(i + 1))
            for i in range(3)
        ]
        await segment_repo.add_batch(meeting.id, segments)
        await session.commit()

        result = await segment_repo.get_by_meeting(meeting.id)

        assert len(result) == 3, f"expected 3 batch-added segments, got {len(result)}"

    async def test_compute_next_segment_id(self, session: AsyncSession) -> None:
        """Test compute_next_segment_id returns max + 1 or 0 when empty."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        next_id = await segment_repo.compute_next_segment_id(meeting.id)
        assert next_id == 0, f"expected next segment_id 0 for empty meeting, got {next_id}"

        segments = [
            Segment(segment_id=0, text="Segment 0", start_time=0.0, end_time=1.0),
            Segment(segment_id=5, text="Segment 5", start_time=1.0, end_time=2.0),
        ]
        await segment_repo.add_batch(meeting.id, segments)
        await session.commit()

        next_id = await segment_repo.compute_next_segment_id(meeting.id)
        assert next_id == 6, f"expected next segment_id 6 (max 5 + 1), got {next_id}"

    async def test_update_embedding_and_retrieve(self, session: AsyncSession) -> None:
        """Test updating a segment embedding persists to the database."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        segment = Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0)
        await segment_repo.add(meeting.id, segment)
        await session.commit()

        assert segment.db_id is not None, "segment db_id should be set after add"
        embedding = [0.1] * 1536
        await segment_repo.update_embedding(segment.db_id, embedding)
        await session.commit()

        result = await segment_repo.get_by_meeting(meeting.id)
        assert result[0].embedding == approx_sequence(embedding), "retrieved embedding should match saved embedding"

    async def test_search_semantic_orders_by_similarity(self, session: AsyncSession) -> None:
        """Test semantic search returns closest matches first."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        segment_repo = SqlAlchemySegmentRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        emb1 = [1.0] + [0.0] * 1535
        emb2 = [0.0, 1.0] + [0.0] * 1534

        segment1 = Segment(
            segment_id=0,
            text="First",
            start_time=0.0,
            end_time=1.0,
            embedding=emb1,
        )
        segment2 = Segment(
            segment_id=1,
            text="Second",
            start_time=1.0,
            end_time=2.0,
            embedding=emb2,
        )
        await segment_repo.add_batch(meeting.id, [segment1, segment2])
        await session.commit()

        search_limit = 2
        results = await segment_repo.search_semantic(query_embedding=emb1, limit=search_limit)
        assert len(results) == search_limit, f"expected {search_limit} semantic search results, got {len(results)}"
        assert results[0][0].segment_id == 0, f"expected closest match segment_id 0, got {results[0][0].segment_id}"
        assert results[0][1] >= results[1][1], f"results should be ordered by similarity: {results[0][1]} >= {results[1][1]}"


@pytest.mark.integration
class TestSummaryRepository:
    """Integration tests for SqlAlchemySummaryRepository."""

    async def test_save_and_get_summary(self, session: AsyncSession) -> None:
        """Test saving and retrieving summary."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        summary_repo = SqlAlchemySummaryRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        summary = Summary(
            meeting_id=meeting.id,
            executive_summary="This was a productive meeting.",
            generated_at=datetime.now(UTC),
            provider_name="test",
            model_name="v1",
        )
        await summary_repo.save(summary)
        await session.commit()

        result = await summary_repo.get_by_meeting(meeting.id)

        assert result is not None, "summary should exist after save"
        assert result.executive_summary == "This was a productive meeting.", f"expected executive_summary to match, got '{result.executive_summary}'"
        assert result.model_version == "test/v1", f"expected model_version 'test/v1', got '{result.model_version}'"

    async def test_save_summary_with_key_points(self, session: AsyncSession) -> None:
        """Test saving summary with key points."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        summary_repo = SqlAlchemySummaryRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        key_points = [
            KeyPoint(text="Point 1", segment_ids=[0, 1]),
            KeyPoint(text="Point 2", segment_ids=[2]),
        ]
        summary = Summary(
            meeting_id=meeting.id,
            executive_summary="Summary",
            key_points=key_points,
        )
        await summary_repo.save(summary)
        await session.commit()

        result = await summary_repo.get_by_meeting(meeting.id)

        assert result is not None, "summary with key points should exist"
        assert len(result.key_points) == 2, f"expected 2 key points, got {len(result.key_points)}"
        assert result.key_points[0].text == "Point 1", f"expected first key point 'Point 1', got '{result.key_points[0].text}'"

    async def test_save_summary_with_action_items(self, session: AsyncSession) -> None:
        """Test saving summary with action items."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        summary_repo = SqlAlchemySummaryRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        action_items = [
            ActionItem(text="Review PR", assignee="Alice", priority=2),
        ]
        summary = Summary(
            meeting_id=meeting.id,
            executive_summary="Summary",
            action_items=action_items,
        )
        await summary_repo.save(summary)
        await session.commit()

        result = await summary_repo.get_by_meeting(meeting.id)

        assert result is not None, "summary should exist"
        assert len(result.action_items) == 1, "should have one action item"
        assert result.action_items[0].text == "Review PR", "action text should match"
        assert result.action_items[0].assignee == "Alice", "assignee should match"

    async def test_delete_summary(self, session: AsyncSession) -> None:
        """Test deleting summary."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        summary_repo = SqlAlchemySummaryRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        summary = Summary(meeting_id=meeting.id, executive_summary="To delete")
        await summary_repo.save(summary)
        await session.commit()

        result = await summary_repo.delete_by_meeting(meeting.id)
        await session.commit()

        assert result is True, "delete should return True for existing summary"

        retrieved = await summary_repo.get_by_meeting(meeting.id)
        assert retrieved is None, "summary should not exist after deletion"

    async def test_update_summary_replaces_items(self, session: AsyncSession) -> None:
        """Test saving a summary twice replaces key points and action items."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        summary_repo = SqlAlchemySummaryRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        summary_v1 = Summary(
            meeting_id=meeting.id,
            executive_summary="v1",
            key_points=[KeyPoint(text="Old KP")],
            action_items=[ActionItem(text="Old AI")],
        )
        await summary_repo.save(summary_v1)
        await session.commit()

        summary_v2 = Summary(
            meeting_id=meeting.id,
            executive_summary="v2",
            key_points=[KeyPoint(text="New KP")],
            action_items=[ActionItem(text="New AI")],
        )
        await summary_repo.save(summary_v2)
        await session.commit()

        result = await summary_repo.get_by_meeting(meeting.id)

        assert result is not None, "updated summary should exist"
        assert result.executive_summary == "v2", "executive summary updated"
        assert [kp.text for kp in result.key_points] == ["New KP"], "key points replaced"
        assert [ai.text for ai in result.action_items] == ["New AI"], "action items replaced"


@pytest.mark.integration
class TestAnnotationRepository:
    """Integration tests for SqlAlchemyAnnotationRepository."""

    async def test_add_and_get_annotation(self, session: AsyncSession) -> None:
        """Test adding and retrieving annotation."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        annotation_repo = SqlAlchemyAnnotationRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        annotation = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting.id,
            annotation_type=AnnotationType.NOTE,
            text="Decision made",
            start_time=1.0,
            end_time=2.0,
            segment_ids=[0],
        )
        await annotation_repo.add(annotation)
        await session.commit()

        retrieved = await annotation_repo.get(annotation.id)

        assert retrieved is not None, "annotation should exist after add"
        assert retrieved.text == "Decision made", f"expected text 'Decision made', got '{retrieved.text}'"
        assert retrieved.segment_ids == [0], f"expected segment_ids [0], got {retrieved.segment_ids}"

    async def test_get_by_meeting_ordered(self, session: AsyncSession) -> None:
        """Test annotations returned in start_time order."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        annotation_repo = SqlAlchemyAnnotationRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        a1 = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting.id,
            annotation_type=AnnotationType.NOTE,
            text="Second",
            start_time=2.0,
            end_time=3.0,
        )
        a2 = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting.id,
            annotation_type=AnnotationType.NOTE,
            text="First",
            start_time=1.0,
            end_time=2.0,
        )
        await annotation_repo.add(a1)
        await annotation_repo.add(a2)
        await session.commit()

        result = await annotation_repo.get_by_meeting(meeting.id)

        texts = [a.text for a in result]
        assert texts == ["First", "Second"], f"expected annotations ordered by start_time, got {texts}"

    async def test_get_by_time_range_inclusive(self, session: AsyncSession) -> None:
        """Test time range query includes boundary overlaps."""
        meeting_repo = SqlAlchemyMeetingRepository(session)
        annotation_repo = SqlAlchemyAnnotationRepository(session)

        meeting = Meeting.create()
        await meeting_repo.create(meeting)
        await session.commit()

        a1 = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting.id,
            annotation_type=AnnotationType.NOTE,
            text="Ends at boundary",
            start_time=0.0,
            end_time=1.0,
        )
        a2 = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting.id,
            annotation_type=AnnotationType.NOTE,
            text="Starts at boundary",
            start_time=1.0,
            end_time=2.0,
        )
        await annotation_repo.add(a1)
        await annotation_repo.add(a2)
        await session.commit()

        result = await annotation_repo.get_by_time_range(meeting.id, start_time=1.0, end_time=1.0)

        texts = {a.text for a in result}
        expected = {"Ends at boundary", "Starts at boundary"}
        assert texts == expected, f"expected both boundary annotations, got {texts}"

    async def test_update_annotation_not_found_raises_repository(self, session: AsyncSession) -> None:
        """Test update raises when annotation does not exist."""
        annotation_repo = SqlAlchemyAnnotationRepository(session)

        annotation = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=MeetingId(uuid4()),
            annotation_type=AnnotationType.NOTE,
            text="Missing",
            start_time=0.0,
            end_time=1.0,
        )

        with pytest.raises(ValueError, match=r"Annotation .* not found"):
            await annotation_repo.update(annotation)

    async def test_delete_annotation_not_found_repository(self, session: AsyncSession) -> None:
        """Test deleting unknown annotation returns False."""
        annotation_repo = SqlAlchemyAnnotationRepository(session)

        result = await annotation_repo.delete(AnnotationId(uuid4()))

        assert result is False, "delete should return False for non-existent annotation"
