# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License - see LICENSE file for details

"""Tests for the email-to-calendar bridge (event detection, triage flags, tool, RSVP hint)."""

import threading
from unittest.mock import MagicMock, patch

# ---------------------------------------------------------------------------
# Event Detection Engine
# ---------------------------------------------------------------------------

class TestDetectCalendarEvent:
    """Tests for _detect_calendar_event()."""

    def _detect(self, subject, body=""):
        from familiar.skills.triage.skill import _detect_calendar_event
        return _detect_calendar_event(subject, body)

    def test_day_of_week_with_time(self):
        result = self._detect("Board meeting Thursday 2pm")
        assert result is not None
        assert result["has_event"] is True
        assert "thursday 2pm" in result["date_hint"].lower()
        assert result["event_hint"] == "meeting"

    def test_month_day_with_time(self):
        result = self._detect("Workshop on March 15 at 10am")
        assert result is not None
        assert result["has_event"] is True
        assert "march 15" in result["date_hint"].lower()

    def test_iso_date(self):
        result = self._detect("Team call 2026-03-15T14:00")
        assert result is not None
        assert "2026-03-15" in result["date_hint"]

    def test_us_date(self):
        result = self._detect("Deadline 03/15/2026", "Submit report by this deadline")
        assert result is not None
        assert result["event_hint"] == "deadline"

    def test_relative_date(self):
        result = self._detect("Standup tomorrow")
        assert result is not None
        assert "tomorrow" in result["date_hint"].lower()
        assert result["event_hint"] == "standup"

    def test_next_weekday(self):
        result = self._detect("All-hands next Monday at 3pm")
        assert result is not None
        assert "monday" in result["date_hint"].lower()

    def test_no_date_returns_none(self):
        result = self._detect("Meeting notes from last week")
        assert result is None

    def test_no_event_keyword_returns_none(self):
        result = self._detect("Report for Thursday 2pm")
        # "report" is not an event keyword — no match
        assert result is None

    def test_date_only_no_keyword_returns_none(self):
        result = self._detect("Invoice dated 2026-03-15")
        assert result is None

    def test_rsvp_detected(self):
        result = self._detect(
            "Gala RSVP Thursday 7pm",
            "Please confirm your attendance by March 10",
        )
        assert result is not None
        assert result["has_rsvp"] is True

    def test_no_rsvp(self):
        result = self._detect("Team call Thursday 2pm")
        assert result is not None
        assert result["has_rsvp"] is False

    def test_body_only_date(self):
        result = self._detect(
            "Important meeting",
            "Let's meet on Wednesday 3pm in the conference room",
        )
        assert result is not None
        assert result["has_event"] is True

    def test_case_insensitive(self):
        result = self._detect("WORKSHOP TUESDAY 10AM")
        assert result is not None
        assert result["event_hint"] == "workshop"

    def test_appointment_keyword(self):
        result = self._detect("Doctor appointment Monday 9am")
        assert result is not None
        assert result["event_hint"] == "appointment"

    def test_check_in_keyword(self):
        result = self._detect("Weekly check-in Friday 4pm")
        assert result is not None
        assert result["event_hint"] == "check-in"


class TestExtractEventDetails:
    """Tests for _extract_event_details()."""

    def _extract(self, subject, body, cal_hint=None):
        from familiar.skills.triage.skill import _extract_event_details
        return _extract_event_details(subject, body, cal_hint)

    def test_basic_extraction(self):
        result = self._extract(
            "Board Meeting",
            "Join us Thursday 2pm in Room 301\nLocation: Conference Room B",
        )
        assert result is not None
        assert result["title"] == "Board Meeting"
        assert "thursday 2pm" in result["start"].lower()
        assert result["duration"] == 60  # default
        assert "Conference Room B" in result["location"]

    def test_strips_re_fwd_prefix(self):
        result = self._extract(
            "Re: Fwd: Team Sync",
            "Meeting tomorrow at 10am",
        )
        assert result is not None
        assert result["title"] == "Team Sync"

    def test_duration_hours(self):
        result = self._extract(
            "Workshop",
            "Join us Monday 9am for a 2 hour session",
        )
        assert result is not None
        assert result["duration"] == 120

    def test_duration_minutes(self):
        result = self._extract(
            "Quick sync",
            "Tuesday 3pm, should take about 30 minutes",
        )
        assert result is not None
        assert result["duration"] == 30

    def test_zoom_url_as_location(self):
        result = self._extract(
            "Team Call",
            "Wednesday 2pm\nhttps://zoom.us/j/123456789",
        )
        assert result is not None
        assert "zoom.us" in result["location"]

    def test_teams_url_as_location(self):
        result = self._extract(
            "Sprint Review",
            "Friday 11am\nhttps://teams.microsoft.com/l/meetup-join/abc",
        )
        assert result is not None
        assert "teams.microsoft.com" in result["location"]

    def test_where_line_as_location(self):
        result = self._extract(
            "Lunch Meeting",
            "Thursday 12pm\nWhere: The Bistro on Main Street",
        )
        assert result is not None
        assert "The Bistro" in result["location"]

    def test_no_date_returns_none(self):
        result = self._extract("Notes from meeting", "Here are the notes...")
        assert result is None

    def test_fallback_to_cal_hint(self):
        hint = {"date_hint": "thursday 2pm", "has_rsvp": True}
        result = self._extract(
            "Board Meeting",
            "See you there!",  # no date in body
            cal_hint=hint,
        )
        assert result is not None
        assert result["start"] == "thursday 2pm"
        assert result["has_rsvp"] is True

    def test_rsvp_from_body(self):
        result = self._extract(
            "Annual Gala",
            "Please RSVP by March 10\nEvent: Friday 7pm",
        )
        assert result is not None
        assert result["has_rsvp"] is True

    def test_no_rsvp(self):
        result = self._extract(
            "Team Standup",
            "Daily standup Monday 9am",
        )
        assert result is not None
        assert result["has_rsvp"] is False


# ---------------------------------------------------------------------------
# Triage Flag Integration
# ---------------------------------------------------------------------------

class TestTriageFlagIntegration:
    """Tests that calendar detection is wired into triage email dicts."""

    def test_gmail_categorized_has_calendar_fields(self):
        """Verify the categorized dict includes calendar fields."""
        # We test the detection function directly since the Gmail path
        # calls it and adds the fields — unit testing the wiring.
        from familiar.skills.triage.skill import _detect_calendar_event

        info = _detect_calendar_event("Team sync Thursday 3pm", "")
        assert info is not None

        # Simulate what the triage path does
        email_dict = {
            "id": "abc123",
            "subject": "Team sync Thursday 3pm",
            "priority": "action",
            "has_calendar_event": bool(info),
            "calendar_hint": info,
        }
        assert email_dict["has_calendar_event"] is True
        assert email_dict["calendar_hint"]["event_hint"] == "sync"

    def test_no_calendar_event_fields_false(self):
        from familiar.skills.triage.skill import _detect_calendar_event

        info = _detect_calendar_event("Budget report attached", "")
        email_dict = {
            "id": "def456",
            "subject": "Budget report attached",
            "priority": "fyi",
            "has_calendar_event": bool(info),
            "calendar_hint": info,
        }
        assert email_dict["has_calendar_event"] is False
        assert email_dict["calendar_hint"] is None


class TestCalendarDisplayFlag:
    """Test that the 📅 flag appears in display output."""

    def test_cal_flag_in_display_line(self):
        """Simulate the display formatting logic."""
        em = {"index": 1, "priority": "action", "from_name": "Sarah", "has_calendar_event": True}
        priority_emoji = {"urgent": "🔴", "action": "🟡", "fyi": "🟢"}
        p_emoji = priority_emoji.get(em["priority"], "⚪")
        cal_flag = "📅" if em.get("has_calendar_event") else ""
        line = f"  #{em['index']} {p_emoji}{cal_flag} {em['from_name']}"
        assert "📅" in line
        assert "🟡📅" in line

    def test_no_cal_flag_when_absent(self):
        em = {"index": 2, "priority": "fyi", "from_name": "Bob", "has_calendar_event": False}
        priority_emoji = {"urgent": "🔴", "action": "🟡", "fyi": "🟢"}
        p_emoji = priority_emoji.get(em["priority"], "⚪")
        cal_flag = "📅" if em.get("has_calendar_event") else ""
        line = f"  #{em['index']} {p_emoji}{cal_flag} {em['from_name']}"
        assert "📅" not in line

    def test_cal_count_in_summary(self):
        emails = [
            {"priority": "action", "has_calendar_event": True},
            {"priority": "action", "has_calendar_event": True},
            {"priority": "fyi", "has_calendar_event": False},
        ]
        cal_count = sum(1 for e in emails if e.get("has_calendar_event"))
        assert cal_count == 2

        summary_parts = []
        if cal_count:
            summary_parts.append(f"{cal_count} with calendar events 📅")
        assert "2 with calendar events 📅" in summary_parts[0]


# ---------------------------------------------------------------------------
# suggest_calendar_from_email Tool
# ---------------------------------------------------------------------------

class TestSuggestCalendarFromEmail:
    """Tests for the suggest_calendar_from_email tool handler."""

    def test_no_email_ref_returns_error(self):
        from familiar.skills.triage.skill import suggest_calendar_from_email

        result = suggest_calendar_from_email({"index": "#99"})
        assert "Could not find" in result

    def test_stale_triage_returns_warning(self):
        import datetime as _dt

        from familiar.skills.triage.skill import suggest_calendar_from_email

        stale_time = (_dt.datetime.now(_dt.timezone.utc) - _dt.timedelta(hours=5)).isoformat()
        session = MagicMock()
        session.session_context = {
            "triage_emails": [
                {"index": 1, "id": "abc", "id_source": "gmail", "subject": "Test"},
            ],
            "triage_at": stale_time,
        }
        result = suggest_calendar_from_email({
            "index": "#1",
            "_context": {"session": session},
        })
        assert "stale" in result.lower()

    @patch("familiar.skills.email.skill._gmail_read_email_by_id")
    def test_successful_extraction(self, mock_gmail_read):
        import datetime as _dt

        from familiar.core.confirmations import ConfirmationRequired
        from familiar.skills.triage.skill import suggest_calendar_from_email

        mock_gmail_read.return_value = (
            "📧 Email Details:\n\nFrom: Sarah Chen\nDate: 2026-03-10\n"
            "Subject: Board Meeting\n\n---\n"
            "Hi,\n\nPlease join us Thursday 2pm in Room 301.\n"
            "Location: Conference Room B\n\nPlease RSVP by Wednesday.\n"
        )

        session = MagicMock()
        session.session_context = {
            "triage_emails": [
                {
                    "index": 1,
                    "id": "msg123",
                    "id_source": "gmail",
                    "subject": "Board Meeting",
                    "from_name": "Sarah Chen",
                    "from_email": "sarah@example.com",
                    "calendar_hint": {
                        "has_event": True,
                        "has_rsvp": True,
                        "date_hint": "thursday 2pm",
                        "event_hint": "meeting",
                    },
                },
            ],
            "triage_at": _dt.datetime.now(_dt.timezone.utc).isoformat(),
        }

        result = suggest_calendar_from_email({
            "index": "#1",
            "_context": {"session": session},
        })

        assert isinstance(result, ConfirmationRequired)
        assert result.tool_name == "create_event"
        assert result.tool_input["title"] == "Board Meeting"
        assert "RSVP" in result.preview
        assert "Conference Room B" in result.preview

    @patch("familiar.skills.email.skill._gmail_read_email_by_id")
    def test_no_event_in_email(self, mock_gmail_read):
        import datetime as _dt

        from familiar.skills.triage.skill import suggest_calendar_from_email

        mock_gmail_read.return_value = (
            "📧 Email Details:\n\nFrom: Bob\nDate: 2026-03-10\n"
            "Subject: Budget Report\n\n---\n"
            "Here is the budget report for Q1.\n"
        )

        session = MagicMock()
        session.session_context = {
            "triage_emails": [
                {
                    "index": 1,
                    "id": "msg456",
                    "id_source": "gmail",
                    "subject": "Budget Report",
                    "from_name": "Bob",
                    "from_email": "bob@example.com",
                    "calendar_hint": None,
                },
            ],
            "triage_at": _dt.datetime.now(_dt.timezone.utc).isoformat(),
        }

        result = suggest_calendar_from_email({
            "index": "#1",
            "_context": {"session": session},
        })

        assert isinstance(result, str)
        assert "Could not extract" in result

    @patch("familiar.skills.email.skill._imap_read_email_by_id")
    def test_imap_path(self, mock_imap_read):
        import datetime as _dt

        from familiar.core.confirmations import ConfirmationRequired
        from familiar.skills.triage.skill import suggest_calendar_from_email

        mock_imap_read.return_value = (
            "📧 Email Details:\n\nFrom: HR\nDate: 2026-03-10\n"
            "Subject: All-hands meeting\n\n---\n"
            "All-hands next Friday at 10am.\n"
            "https://meet.google.com/abc-def-ghi\n"
        )

        session = MagicMock()
        session.session_context = {
            "triage_emails": [
                {
                    "index": 2,
                    "id": "42",
                    "id_source": "imap",
                    "subject": "All-hands meeting",
                    "from_name": "HR Department",
                    "from_email": "hr@company.com",
                    "calendar_hint": None,
                },
            ],
            "triage_at": _dt.datetime.now(_dt.timezone.utc).isoformat(),
        }

        result = suggest_calendar_from_email({
            "index": "#2",
            "_context": {"session": session},
        })

        assert isinstance(result, ConfirmationRequired)
        assert result.tool_name == "create_event"

    @patch("familiar.skills.email.skill._gmail_read_email_by_id")
    def test_email_read_failure(self, mock_gmail_read):
        import datetime as _dt

        from familiar.skills.triage.skill import suggest_calendar_from_email

        mock_gmail_read.return_value = "❌ Could not fetch that email."

        session = MagicMock()
        session.session_context = {
            "triage_emails": [
                {
                    "index": 1,
                    "id": "gone",
                    "id_source": "gmail",
                    "subject": "Meeting",
                    "from_name": "Test",
                    "from_email": "test@test.com",
                    "calendar_hint": None,
                },
            ],
            "triage_at": _dt.datetime.now(_dt.timezone.utc).isoformat(),
        }

        result = suggest_calendar_from_email({
            "index": "#1",
            "_context": {"session": session},
        })
        assert "Could not fetch" in result


# ---------------------------------------------------------------------------
# RSVP Reply Hint (tool_executor)
# ---------------------------------------------------------------------------

class TestRsvpReplyHint:
    """Tests for RSVP context capture in _capture_context_from_result."""

    def test_rsvp_hint_captured(self):
        from familiar.core.tool_executor import _capture_context_from_result

        session = MagicMock()
        session.session_context = {}

        _capture_context_from_result(
            tool_name="create_event",
            tool_input={
                "title": "Board Meeting",
                "_email_index": 1,
                "_email_has_rsvp": True,
            },
            result="✅ Created: Board Meeting",
            session=session,
            sessions_manager=None,
        )

        assert "pending_rsvp_reply" in session.session_context
        rsvp = session.session_context["pending_rsvp_reply"]
        assert rsvp["email_index"] == 1
        assert rsvp["event_title"] == "Board Meeting"

    def test_no_rsvp_hint_without_flag(self):
        from familiar.core.tool_executor import _capture_context_from_result

        session = MagicMock()
        session.session_context = {}

        _capture_context_from_result(
            tool_name="create_event",
            tool_input={"title": "Team Standup"},
            result="✅ Created: Team Standup",
            session=session,
            sessions_manager=None,
        )

        assert "pending_rsvp_reply" not in session.session_context

    def test_no_rsvp_hint_when_rsvp_false(self):
        from familiar.core.tool_executor import _capture_context_from_result

        session = MagicMock()
        session.session_context = {}

        _capture_context_from_result(
            tool_name="create_event",
            tool_input={
                "title": "Meeting",
                "_email_index": 3,
                "_email_has_rsvp": False,
            },
            result="✅ Created: Meeting",
            session=session,
            sessions_manager=None,
        )

        assert "pending_rsvp_reply" not in session.session_context

    def test_no_rsvp_on_failed_event(self):
        from familiar.core.tool_executor import _capture_context_from_result

        session = MagicMock()
        session.session_context = {}

        _capture_context_from_result(
            tool_name="create_event",
            tool_input={
                "title": "Meeting",
                "_email_index": 1,
                "_email_has_rsvp": True,
            },
            result="❌ Failed to create event",
            session=session,
            sessions_manager=None,
        )

        assert "pending_rsvp_reply" not in session.session_context

    def test_last_event_title_still_captured(self):
        from familiar.core.tool_executor import _capture_context_from_result

        session = MagicMock()
        session.session_context = {}

        _capture_context_from_result(
            tool_name="create_event",
            tool_input={
                "title": "Board Meeting",
                "_email_index": 1,
                "_email_has_rsvp": True,
            },
            result="✅ Created: Board Meeting",
            session=session,
            sessions_manager=None,
        )

        assert session.session_context["last_event_title"] == "Board Meeting"


# ---------------------------------------------------------------------------
# Proactive Briefing Integration
# ---------------------------------------------------------------------------

class TestProactiveBriefingCalendarEmails:
    """Tests for calendar email hints in briefing and heartbeat."""

    def test_briefing_picks_up_calendar_emails(self):
        """When triage context has calendar emails, briefing includes them."""

        t = threading.current_thread()
        t._familiar_triage_ctx = {
            "emails": [
                {"index": 1, "has_calendar_event": True, "subject": "Meeting Thu 2pm"},
                {"index": 2, "has_calendar_event": False, "subject": "Budget report"},
                {"index": 3, "has_calendar_event": True, "subject": "Call Fri 10am"},
            ],
        }

        try:
            from familiar.skills.proactive.skill import generate_briefing_data
            data = generate_briefing_data()
            cal_section = data["sections"].get("calendar_emails", "")
            assert "2 email" in cal_section
            assert "#1" in cal_section
            assert "#3" in cal_section
        finally:
            t._familiar_triage_ctx = None

    def test_briefing_no_calendar_emails(self):
        """When no calendar emails, section is absent."""

        t = threading.current_thread()
        t._familiar_triage_ctx = {
            "emails": [
                {"index": 1, "has_calendar_event": False},
            ],
        }

        try:
            from familiar.skills.proactive.skill import generate_briefing_data
            data = generate_briefing_data()
            assert "calendar_emails" not in data["sections"]
        finally:
            t._familiar_triage_ctx = None

    def test_heartbeat_with_calendar_emails(self):
        """Heartbeat mentions unscheduled calendar emails."""

        t = threading.current_thread()
        t._familiar_triage_ctx = {
            "emails": [
                {"index": 1, "has_calendar_event": True},
                {"index": 2, "has_calendar_event": True},
            ],
        }

        try:
            from familiar.skills.proactive.skill import heartbeat_check
            result = heartbeat_check({})
            assert "📅" in result
            assert "2 emails" in result
            assert "calendar events" in result
        finally:
            t._familiar_triage_ctx = None

    def test_heartbeat_silent_without_triage_data(self):
        """Heartbeat stays silent when no triage context exists."""

        t = threading.current_thread()
        # Ensure no triage context
        if hasattr(t, "_familiar_triage_ctx"):
            delattr(t, "_familiar_triage_ctx")

        from familiar.skills.proactive.skill import heartbeat_check
        result = heartbeat_check({})
        assert "📅" not in result


# ---------------------------------------------------------------------------
# Constants Sanity Checks
# ---------------------------------------------------------------------------

class TestConstants:
    """Verify the calendar detection constants are well-formed."""

    def test_date_patterns_are_compiled(self):
        import re

        from familiar.skills.triage.skill import _CAL_DATE_PATTERNS

        assert len(_CAL_DATE_PATTERNS) >= 4
        for pat in _CAL_DATE_PATTERNS:
            assert isinstance(pat, re.Pattern)

    def test_event_keywords_is_set(self):
        from familiar.skills.triage.skill import _CAL_EVENT_KEYWORDS

        assert isinstance(_CAL_EVENT_KEYWORDS, set)
        assert "meeting" in _CAL_EVENT_KEYWORDS
        assert "deadline" in _CAL_EVENT_KEYWORDS

    def test_rsvp_keywords_is_set(self):
        from familiar.skills.triage.skill import _CAL_RSVP_KEYWORDS

        assert isinstance(_CAL_RSVP_KEYWORDS, set)
        assert "rsvp" in _CAL_RSVP_KEYWORDS

    def test_tool_registered(self):
        from familiar.skills.triage.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "suggest_calendar_from_email" in names

        tool = next(t for t in TOOLS if t["name"] == "suggest_calendar_from_email")
        assert tool["category"] == "triage"
        assert "index" in tool["input_schema"]["properties"]
