"""Tests for RetentionService."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock

import pytest

from noteflow.application.services.retention_service import RetentionReport, RetentionService
from noteflow.domain.entities import Meeting


def _create_meeting(ended_at: datetime | None = None) -> Meeting:
    """Create a test meeting with optional ended_at."""
    meeting = Meeting.create(title="Test Meeting")
    if ended_at:
        meeting.ended_at = ended_at
    return meeting


class TestRetentionServiceProperties:
    """Tests for RetentionService properties."""

    def test_is_enabled_reflects_init(self) -> None:
        """is_enabled should reflect constructor parameter."""
        uow = MagicMock()

        def factory() -> MagicMock:
            return uow

        enabled_service = RetentionService(factory, retention_days=30, enabled=True)
        disabled_service = RetentionService(factory, retention_days=30, enabled=False)

        assert enabled_service.is_enabled is True
        assert disabled_service.is_enabled is False

    def test_retention_days_property(self) -> None:
        """retention_days should return configured value."""
        uow = MagicMock()
        expected_days = 45
        service = RetentionService(lambda: uow, retention_days=expected_days)

        assert service.retention_days == expected_days

    def test_cutoff_date_calculation(self) -> None:
        """cutoff_date should be retention_days in the past."""
        uow = MagicMock()
        service = RetentionService(lambda: uow, retention_days=30)

        cutoff = service.cutoff_date
        expected = datetime.now(UTC) - timedelta(days=30)

        # Allow 1 second tolerance
        assert abs((cutoff - expected).total_seconds()) < 1


class TestRetentionServiceFindExpired:
    """Tests for find_expired_meetings method."""

    @pytest.mark.asyncio
    async def test_find_expired_returns_meetings(self, mock_uow: MagicMock) -> None:
        """find_expired_meetings should return meetings from repository."""
        old_meeting = _create_meeting(ended_at=datetime.now(UTC) - timedelta(days=100))
        mock_uow.meetings.find_older_than = AsyncMock(return_value=[old_meeting])

        service = RetentionService(lambda: mock_uow, retention_days=30)
        result = await service.find_expired_meetings()

        assert len(result) == 1
        mock_uow.meetings.find_older_than.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_find_expired_returns_empty_list(self, mock_uow: MagicMock) -> None:
        """find_expired_meetings should return empty list when none found."""
        mock_uow.meetings.find_older_than = AsyncMock(return_value=[])

        service = RetentionService(lambda: mock_uow, retention_days=30)
        result = await service.find_expired_meetings()

        assert result == []


class TestRetentionServiceRunCleanup:
    """Tests for run_cleanup method."""

    @pytest.mark.asyncio
    async def test_run_cleanup_disabled_returns_empty_report(self, mock_uow: MagicMock) -> None:
        """run_cleanup should return empty report when disabled."""
        service = RetentionService(lambda: mock_uow, retention_days=30, enabled=False)

        report = await service.run_cleanup()

        assert report.meetings_checked == 0
        assert report.meetings_deleted == 0
        assert report.errors == ()

    @pytest.mark.asyncio
    async def test_run_cleanup_dry_run_does_not_delete(self, mock_uow: MagicMock) -> None:
        """run_cleanup with dry_run should not delete meetings."""
        old_meeting = _create_meeting(ended_at=datetime.now(UTC) - timedelta(days=100))
        mock_uow.meetings.find_older_than = AsyncMock(return_value=[old_meeting])

        service = RetentionService(lambda: mock_uow, retention_days=30, enabled=False)
        report = await service.run_cleanup(dry_run=True)

        # Should report meeting was checked but not deleted
        assert report.meetings_checked == 1
        assert report.meetings_deleted == 0
        assert report.errors == ()

    @pytest.mark.asyncio
    async def test_run_cleanup_deletes_expired_meetings(
        self, mock_uow: MagicMock
    ) -> None:
        """run_cleanup should delete expired meetings when enabled."""
        old_meeting = _create_meeting(ended_at=datetime.now(UTC) - timedelta(days=100))
        mock_uow.meetings.find_older_than = AsyncMock(return_value=[old_meeting])
        mock_uow.meetings.get = AsyncMock(return_value=old_meeting)
        mock_uow.meetings.delete = AsyncMock(return_value=True)

        service = RetentionService(
            lambda: mock_uow,
            retention_days=30,
            enabled=True,
        )
        report = await service.run_cleanup()

        assert report.meetings_checked == 1
        assert report.meetings_deleted == 1
        assert report.errors == ()

    @pytest.mark.asyncio
    async def test_run_cleanup_handles_errors_gracefully(self, mock_uow: MagicMock) -> None:
        """run_cleanup should capture errors without failing."""
        old_meeting = _create_meeting(ended_at=datetime.now(UTC) - timedelta(days=100))
        mock_uow.meetings.find_older_than = AsyncMock(return_value=[old_meeting])
        mock_uow.meetings.get = AsyncMock(side_effect=RuntimeError("DB error"))

        service = RetentionService(lambda: mock_uow, retention_days=30, enabled=True)
        report = await service.run_cleanup()

        assert report.meetings_checked == 1, "Should check one meeting"
        assert report.meetings_deleted == 0, "Should not delete when error occurs"
        assert len(report.errors) == 1, "Should capture error"
        assert "DB error" in report.errors[0], "Error message should contain DB error"


class TestRetentionReport:
    """Tests for RetentionReport dataclass."""

    def test_retention_report_is_immutable(self) -> None:
        """RetentionReport should be frozen."""
        from dataclasses import FrozenInstanceError

        report = RetentionReport(
            meetings_checked=5,
            meetings_deleted=3,
            errors=("error1",),
        )

        with pytest.raises(FrozenInstanceError, match="cannot assign"):
            report.meetings_checked = 10

    def test_retention_report_stores_values(self) -> None:
        """RetentionReport should store all values correctly."""
        report = RetentionReport(
            meetings_checked=10,
            meetings_deleted=8,
            errors=("err1", "err2"),
        )

        assert report.meetings_checked == 10
        assert report.meetings_deleted == 8
        assert report.errors == ("err1", "err2")
