"""Unit tests for timmy.briefing — the morning briefing engine."""

from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, patch

from timmy.briefing import (
    ApprovalItem,
    Briefing,
    BriefingEngine,
    _gather_swarm_summary,
    _load_latest,
    _save_briefing,
    is_fresh,
)

# ---------------------------------------------------------------------------
# ApprovalItem / Briefing dataclass basics
# ---------------------------------------------------------------------------


class TestApprovalItem:
    def test_fields(self):
        now = datetime.now(UTC)
        item = ApprovalItem(
            id="a1",
            title="Deploy v2",
            description="Upgrade prod",
            proposed_action="deploy",
            impact="high",
            created_at=now,
            status="pending",
        )
        assert item.id == "a1"
        assert item.status == "pending"

    def test_briefing_defaults(self):
        b = Briefing(generated_at=datetime.now(UTC), summary="hello")
        assert b.approval_items == []
        assert b.period_start < b.period_end


# ---------------------------------------------------------------------------
# is_fresh
# ---------------------------------------------------------------------------


class TestIsFresh:
    def test_fresh_briefing(self):
        b = Briefing(generated_at=datetime.now(UTC), summary="ok")
        assert is_fresh(b) is True

    def test_stale_briefing(self):
        old = datetime.now(UTC) - timedelta(hours=2)
        b = Briefing(generated_at=old, summary="old")
        assert is_fresh(b) is False

    def test_custom_max_age(self):
        recent = datetime.now(UTC) - timedelta(minutes=10)
        b = Briefing(generated_at=recent, summary="recent")
        assert is_fresh(b, max_age_minutes=5) is False
        assert is_fresh(b, max_age_minutes=15) is True

    def test_naive_datetime_handled(self):
        # briefing.generated_at without tzinfo should still work
        naive = datetime.now(UTC).replace(tzinfo=None)
        b = Briefing(generated_at=naive, summary="naive")
        assert is_fresh(b) is True


# ---------------------------------------------------------------------------
# SQLite cache round-trip
# ---------------------------------------------------------------------------


class TestSqliteCache:
    def test_save_and_load(self, tmp_path):
        db = tmp_path / "test.db"
        now = datetime.now(UTC)
        b = Briefing(
            generated_at=now,
            summary="Good morning",
            period_start=now - timedelta(hours=6),
            period_end=now,
        )
        _save_briefing(b, db)
        loaded = _load_latest(db)
        assert loaded is not None
        assert loaded.summary == "Good morning"
        assert loaded.generated_at.isoformat()[:19] == now.isoformat()[:19]

    def test_load_latest_returns_most_recent(self, tmp_path):
        db = tmp_path / "test.db"
        old = datetime.now(UTC) - timedelta(hours=12)
        new = datetime.now(UTC)
        _save_briefing(Briefing(generated_at=old, summary="old"), db)
        _save_briefing(Briefing(generated_at=new, summary="new"), db)
        loaded = _load_latest(db)
        assert loaded.summary == "new"

    def test_load_latest_empty_db(self, tmp_path):
        db = tmp_path / "empty.db"
        assert _load_latest(db) is None


# ---------------------------------------------------------------------------
# _gather_swarm_summary
# ---------------------------------------------------------------------------


class TestGatherSwarmSummary:
    def test_missing_db_file(self):
        with patch("timmy.briefing.Path") as mock_path_cls:
            # Simulate swarm_db.exists() -> False
            mock_instance = MagicMock()
            mock_instance.exists.return_value = False

            original_path = Path

            def side_effect(arg):
                if arg == "data/swarm.db":
                    return mock_instance
                return original_path(arg)

            mock_path_cls.side_effect = side_effect
            mock_path_cls.home = original_path.home

            result = _gather_swarm_summary(datetime.now(UTC))
            assert (
                "No swarm" in result
                or "unavailable" in result.lower()
                or "No swarm activity" in result
            )


# ---------------------------------------------------------------------------
# BriefingEngine
# ---------------------------------------------------------------------------


class TestBriefingEngine:
    def test_get_cached_empty(self, tmp_path):
        db = tmp_path / "test.db"
        engine = BriefingEngine(db_path=db)
        assert engine.get_cached() is None

    def test_needs_refresh_empty(self, tmp_path):
        db = tmp_path / "test.db"
        engine = BriefingEngine(db_path=db)
        assert engine.needs_refresh() is True

    def test_needs_refresh_fresh(self, tmp_path):
        db = tmp_path / "test.db"
        _save_briefing(Briefing(generated_at=datetime.now(UTC), summary="fresh"), db)
        engine = BriefingEngine(db_path=db)
        assert engine.needs_refresh() is False

    def test_needs_refresh_stale(self, tmp_path):
        db = tmp_path / "test.db"
        old = datetime.now(UTC) - timedelta(hours=2)
        _save_briefing(Briefing(generated_at=old, summary="stale"), db)
        engine = BriefingEngine(db_path=db)
        assert engine.needs_refresh() is True

    @patch("timmy.briefing.BriefingEngine._call_agent")
    @patch("timmy.briefing.BriefingEngine._load_pending_items")
    @patch("timmy.briefing._gather_swarm_summary")
    @patch("timmy.briefing._gather_chat_summary")
    @patch("timmy.briefing._gather_task_queue_summary")
    def test_generate(self, mock_task, mock_chat, mock_swarm, mock_pending, mock_agent, tmp_path):
        mock_swarm.return_value = "2 tasks completed"
        mock_chat.return_value = "No conversations"
        mock_task.return_value = "No tasks"
        mock_agent.return_value = "Good morning, Alexander."
        mock_pending.return_value = []

        db = tmp_path / "test.db"
        engine = BriefingEngine(db_path=db)
        briefing = engine.generate()

        assert briefing.summary == "Good morning, Alexander."
        mock_agent.assert_called_once()
        # Verify it was cached
        assert _load_latest(db) is not None

    @patch("timmy.briefing.BriefingEngine._call_agent")
    @patch("timmy.briefing.BriefingEngine._load_pending_items")
    @patch("timmy.briefing._gather_swarm_summary")
    @patch("timmy.briefing._gather_chat_summary")
    @patch("timmy.briefing._gather_task_queue_summary")
    def test_generate_agent_failure(
        self, mock_task, mock_chat, mock_swarm, mock_pending, mock_agent, tmp_path
    ):
        mock_swarm.return_value = ""
        mock_chat.return_value = ""
        mock_task.return_value = ""
        mock_agent.side_effect = Exception("LLM offline")
        mock_pending.return_value = []

        db = tmp_path / "test.db"
        engine = BriefingEngine(db_path=db)
        briefing = engine.generate()
        # Should gracefully degrade
        assert "offline" in briefing.summary.lower()

    @patch("timmy.briefing.BriefingEngine._load_pending_items")
    def test_get_or_generate_returns_cached(self, mock_pending, tmp_path):
        db = tmp_path / "test.db"
        _save_briefing(Briefing(generated_at=datetime.now(UTC), summary="cached"), db)
        mock_pending.return_value = []

        engine = BriefingEngine(db_path=db)
        result = engine.get_or_generate()
        assert result.summary == "cached"

    @patch("timmy.briefing.BriefingEngine.generate")
    def test_get_or_generate_regenerates_when_stale(self, mock_gen, tmp_path):
        db = tmp_path / "test.db"
        old = datetime.now(UTC) - timedelta(hours=2)
        _save_briefing(Briefing(generated_at=old, summary="stale"), db)

        fresh = Briefing(generated_at=datetime.now(UTC), summary="fresh")
        mock_gen.return_value = fresh

        engine = BriefingEngine(db_path=db)
        result = engine.get_or_generate()
        assert result.summary == "fresh"
        mock_gen.assert_called_once()
