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

"""Tests for the email→calendar→proactive pipeline.

Covers: PendingEventStore, EmailActionStore, nonprofit signal detection,
monitor queue wiring, and proactive tool handlers.
"""

import tempfile
from pathlib import Path
from unittest import mock

# ── PendingEventStore tests ───────────────────────────────────────────────────


class TestPendingEventStore:
    """Tests for the pending calendar event store."""

    def setup_method(self):
        self.tmpdir = tempfile.mkdtemp()
        self._patch = mock.patch(
            "familiar.skills.email.pending_events._store_path",
            return_value=Path(self.tmpdir) / "pending_events.json",
        )
        self._patch.start()

    def teardown_method(self):
        self._patch.stop()

    def test_add_and_get_pending(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        eid = store.add_event(
            email_id="msg-1",
            email_subject="Board Meeting March 25",
            email_from="chair@nonprofit.org",
            title="Board Meeting",
            start="2026-03-25T18:00:00",
            duration=90,
            location="Zoom",
            has_rsvp=True,
            source_category="Board",
        )
        assert eid is not None
        assert eid.startswith("pe_")

        pending = store.get_pending()
        assert len(pending) == 1
        assert pending[0]["title"] == "Board Meeting"
        assert pending[0]["status"] == "pending"
        assert pending[0]["has_rsvp"] is True

    def test_deduplicate_by_email_id(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        eid1 = store.add_event(
            email_id="msg-1",
            email_subject="Meeting",
            email_from="a@b.com",
            title="Meeting",
            start="2026-03-25",
        )
        eid2 = store.add_event(
            email_id="msg-1",
            email_subject="Meeting",
            email_from="a@b.com",
            title="Meeting",
            start="2026-03-25",
        )
        assert eid1 is not None
        assert eid2 is None  # Duplicate rejected
        assert len(store.get_pending()) == 1

    def test_confirm_and_dismiss(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        eid1 = store.add_event(
            email_id="msg-1",
            email_subject="A",
            email_from="a@b.com",
            title="A",
            start="2026-03-25",
        )
        eid2 = store.add_event(
            email_id="msg-2",
            email_subject="B",
            email_from="b@b.com",
            title="B",
            start="2026-03-26",
        )

        store.mark_confirmed(eid1)
        store.mark_dismissed(eid2)

        assert len(store.get_pending()) == 0

        # Verify statuses persisted
        store2 = PendingEventStore()
        assert store2.get_event(eid1)["status"] == "confirmed"
        assert store2.get_event(eid2)["status"] == "dismissed"

    def test_get_by_index(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        store.add_event(
            email_id="msg-1",
            email_subject="First",
            email_from="a@b.com",
            title="First",
            start="2026-03-20",
        )
        store.add_event(
            email_id="msg-2",
            email_subject="Second",
            email_from="b@b.com",
            title="Second",
            start="2026-03-25",
        )

        ev1 = store.get_by_index(1)
        ev2 = store.get_by_index(2)
        assert ev1["title"] == "First"
        assert ev2["title"] == "Second"
        assert store.get_by_index(3) is None

    def test_cleanup_stale(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        # Add and confirm an event
        eid = store.add_event(
            email_id="msg-old",
            email_subject="Old",
            email_from="a@b.com",
            title="Old",
            start="2026-01-01",
        )
        store.mark_confirmed(eid)

        # Backdate the detected_at to make it stale
        for ev in store._data["events"]:
            if ev["id"] == eid:
                ev["detected_at"] = "2020-01-01T00:00:00+00:00"
        store.save()

        store.cleanup_stale(max_age_days=7)
        assert len(store._data["events"]) == 0

    def test_count_pending(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store = PendingEventStore()
        assert store.count_pending() == 0
        store.add_event(
            email_id="msg-1",
            email_subject="A",
            email_from="a@b.com",
            title="A",
            start="2026-03-25",
        )
        assert store.count_pending() == 1

    def test_persistence_across_instances(self):
        from familiar.skills.email.pending_events import PendingEventStore

        store1 = PendingEventStore()
        store1.add_event(
            email_id="msg-1",
            email_subject="Persisted",
            email_from="a@b.com",
            title="Persisted",
            start="2026-03-25",
        )

        store2 = PendingEventStore()
        assert len(store2.get_pending()) == 1
        assert store2.get_pending()[0]["title"] == "Persisted"


# ── EmailActionStore tests ────────────────────────────────────────────────────


class TestEmailActionStore:
    """Tests for the email action queue store."""

    def setup_method(self):
        self.tmpdir = tempfile.mkdtemp()
        self._patch = mock.patch(
            "familiar.skills.email.action_queue._store_path",
            return_value=Path(self.tmpdir) / "email_actions.json",
        )
        self._patch.start()

    def teardown_method(self):
        self._patch.stop()

    def test_add_and_get_pending(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        aid = store.add_action(
            email_id="msg-1",
            email_subject="Thank you for your $500 gift",
            email_from="donor@example.com",
            action_type="thank_you",
            suggested_response="Send thank-you for $500 gift",
            category="Donors",
            priority="action",
        )
        assert aid is not None
        assert aid.startswith("ea_")

        pending = store.get_pending()
        assert len(pending) == 1
        assert pending[0]["action_type"] == "thank_you"

    def test_deduplicate_by_email_and_type(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        aid1 = store.add_action(
            email_id="msg-1",
            email_subject="Gift",
            email_from="a@b.com",
            action_type="thank_you",
        )
        aid2 = store.add_action(
            email_id="msg-1",
            email_subject="Gift",
            email_from="a@b.com",
            action_type="thank_you",
        )
        assert aid1 is not None
        assert aid2 is None

        # Same email, different action type — should work
        aid3 = store.add_action(
            email_id="msg-1",
            email_subject="Gift",
            email_from="a@b.com",
            action_type="rsvp",
        )
        assert aid3 is not None
        assert len(store.get_pending()) == 2

    def test_complete_and_delegate(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        aid1 = store.add_action(
            email_id="msg-1",
            email_subject="A",
            email_from="a@b.com",
            action_type="thank_you",
        )
        aid2 = store.add_action(
            email_id="msg-2",
            email_subject="B",
            email_from="b@b.com",
            action_type="board_prep",
        )

        store.mark_complete(aid1)
        store.mark_delegated(aid2, "ganesha@gjl.org")

        assert len(store.get_pending()) == 0

    def test_get_by_type(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="Gift",
            email_from="a@b.com",
            action_type="thank_you",
        )
        store.add_action(
            email_id="msg-2",
            email_subject="RSVP",
            email_from="b@b.com",
            action_type="rsvp",
        )
        store.add_action(
            email_id="msg-3",
            email_subject="Gift 2",
            email_from="c@b.com",
            action_type="thank_you",
        )

        thanks = store.get_by_type("thank_you")
        assert len(thanks) == 2
        rsvps = store.get_by_type("rsvp")
        assert len(rsvps) == 1

    def test_summary_by_type(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="A",
            email_from="a@b.com",
            action_type="thank_you",
        )
        store.add_action(
            email_id="msg-2",
            email_subject="B",
            email_from="b@b.com",
            action_type="thank_you",
        )
        store.add_action(
            email_id="msg-3",
            email_subject="C",
            email_from="c@b.com",
            action_type="grant_report",
        )

        summary = store.summary_by_type()
        assert summary == {"thank_you": 2, "grant_report": 1}

    def test_priority_sorting(self):
        from familiar.skills.email.action_queue import EmailActionStore

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="Low",
            email_from="a@b.com",
            action_type="info_request",
            priority="fyi",
        )
        store.add_action(
            email_id="msg-2",
            email_subject="Urgent",
            email_from="b@b.com",
            action_type="legislative_action",
            priority="urgent",
        )
        store.add_action(
            email_id="msg-3",
            email_subject="Action",
            email_from="c@b.com",
            action_type="thank_you",
            priority="action",
        )

        pending = store.get_pending()
        assert pending[0]["priority"] == "urgent"
        assert pending[1]["priority"] == "action"
        assert pending[2]["priority"] == "fyi"


# ── Nonprofit signal detection tests ──────────────────────────────────────────


class TestNonprofitSignalDetection:
    """Tests for _detect_nonprofit_signals in triage skill."""

    def test_donor_gift_detection(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Thank you for your generous donation",
            "We received your $500.00 contribution to Gender Justice League",
            "Donors",
        )
        assert signals.get("donor_gift") is True
        assert signals.get("gift_amount") == "$500.00"

    def test_donor_gift_no_amount(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "New donation received",
            "A supporter has made a charitable gift to your organization",
            "Donors",
        )
        assert signals.get("donor_gift") is True
        assert "gift_amount" not in signals

    def test_grant_deadline_detection(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Grant Report Due",
            "Your interim report for the Pride Foundation grant is due by March 30, 2026",
            "Grants",
        )
        assert "grant_deadline" in signals
        assert (
            "march 30" in signals["grant_deadline"].lower()
            or "2026-03-30" in signals["grant_deadline"]
        )

    def test_board_reference_detection(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Board Meeting Agenda - March 2026",
            "Please review the attached board packet before our meeting",
            "Board",
        )
        assert signals.get("board_reference") is True

    def test_event_planning_detection(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Trans Pride 2026 Logistics",
            "We need to confirm the venue and catering for the community fair",
            "Trans Pride",
        )
        assert signals.get("event_planning") is True

    def test_legislative_action_detection(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "URGENT: Action Alert - HB 1234",
            "Please call your representatives to vote no on this harmful bill",
            "Campaign",
        )
        assert signals.get("legislative_action") is True

    def test_no_signals_for_routine_email(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Newsletter: March Updates",
            "Here are the latest updates from our organization",
            "Notifications",
        )
        assert signals == {}

    def test_multiple_signals(self):
        from familiar.skills.triage.skill import _detect_nonprofit_signals

        signals = _detect_nonprofit_signals(
            "Board Meeting & Fundraiser Gala",
            "Board meeting to discuss the venue for the annual gala fundraiser",
            "Board",
        )
        assert signals.get("board_reference") is True
        assert signals.get("event_planning") is True


# ── Proactive tool handler tests ──────────────────────────────────────────────


class TestPendingEventTools:
    """Tests for review/confirm/dismiss pending event tools."""

    def setup_method(self):
        self.tmpdir = tempfile.mkdtemp()
        self._patch = mock.patch(
            "familiar.skills.email.pending_events._store_path",
            return_value=Path(self.tmpdir) / "pending_events.json",
        )
        self._patch.start()

    def teardown_method(self):
        self._patch.stop()

    def test_review_empty(self):
        from familiar.skills.proactive.skill import review_pending_events

        result = review_pending_events({})
        assert "No pending" in result

    def test_review_with_events(self):
        from familiar.skills.email.pending_events import PendingEventStore
        from familiar.skills.proactive.skill import review_pending_events

        store = PendingEventStore()
        store.add_event(
            email_id="msg-1",
            email_subject="Board Meeting",
            email_from="chair@gjl.org",
            title="Board Meeting",
            start="2026-03-25T18:00",
            has_rsvp=True,
        )
        store.add_event(
            email_id="msg-2",
            email_subject="Coalition Call",
            email_from="partner@org.com",
            title="Coalition Call",
            start="2026-03-27T10:00",
            location="Zoom",
        )

        result = review_pending_events({})
        assert "2 meetings" in result
        assert "Board Meeting" in result
        assert "Coalition Call" in result
        assert "RSVP requested" in result
        assert "Zoom" in result

    def test_dismiss_by_index(self):
        from familiar.skills.email.pending_events import PendingEventStore
        from familiar.skills.proactive.skill import dismiss_pending_event

        store = PendingEventStore()
        store.add_event(
            email_id="msg-1",
            email_subject="Meeting",
            email_from="a@b.com",
            title="Meeting",
            start="2026-03-25",
        )

        result = dismiss_pending_event({"index": 1})
        assert "Dismissed" in result
        assert store.count_pending() == 0


class TestActionQueueTools:
    """Tests for review/complete/delegate action queue tools."""

    def setup_method(self):
        self.tmpdir = tempfile.mkdtemp()
        self._patch = mock.patch(
            "familiar.skills.email.action_queue._store_path",
            return_value=Path(self.tmpdir) / "email_actions.json",
        )
        self._patch.start()

    def teardown_method(self):
        self._patch.stop()

    def test_review_empty(self):
        from familiar.skills.proactive.skill import review_action_queue

        result = review_action_queue({})
        assert "No pending" in result

    def test_review_with_actions(self):
        from familiar.skills.email.action_queue import EmailActionStore
        from familiar.skills.proactive.skill import review_action_queue

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="$500 gift",
            email_from="donor@example.com",
            action_type="thank_you",
            suggested_response="Send thank-you for $500 gift",
        )
        store.add_action(
            email_id="msg-2",
            email_subject="HB 1234 Alert",
            email_from="coalition@org.com",
            action_type="legislative_action",
            suggested_response="Review advocacy action item",
            priority="urgent",
        )

        result = review_action_queue({})
        assert "2 email actions" in result
        assert (
            "thank-you" in result.lower() or "thank_you" in result.lower() or "Send thank" in result
        )
        assert "advocacy" in result.lower() or "legislative" in result.lower()

    def test_complete_by_index(self):
        from familiar.skills.email.action_queue import EmailActionStore
        from familiar.skills.proactive.skill import complete_action

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="Gift",
            email_from="a@b.com",
            action_type="thank_you",
        )

        result = complete_action({"index": 1})
        assert "Completed" in result
        assert store.count_pending() == 0

    def test_delegate_by_index(self):
        from familiar.skills.email.action_queue import EmailActionStore
        from familiar.skills.proactive.skill import delegate_action

        store = EmailActionStore()
        store.add_action(
            email_id="msg-1",
            email_subject="Board packet",
            email_from="a@b.com",
            action_type="board_prep",
        )

        result = delegate_action({"index": 1, "delegate_to": "ganesha@gjl.org"})
        assert "Delegated" in result
        assert "ganesha" in result
        assert store.count_pending() == 0

    def test_delegate_requires_target(self):
        from familiar.skills.proactive.skill import delegate_action

        result = delegate_action({"index": 1})
        assert "specify" in result.lower() or "❌" in result


# ── Monitor queue wiring tests ────────────────────────────────────────────────


class TestMonitorQueueWiring:
    """Tests for _queue_pending_events and _queue_email_actions."""

    def setup_method(self):
        self.tmpdir = tempfile.mkdtemp()
        self._pe_patch = mock.patch(
            "familiar.skills.email.pending_events._store_path",
            return_value=Path(self.tmpdir) / "pending_events.json",
        )
        self._aq_patch = mock.patch(
            "familiar.skills.email.action_queue._store_path",
            return_value=Path(self.tmpdir) / "email_actions.json",
        )
        self._pe_patch.start()
        self._aq_patch.start()

    def teardown_method(self):
        self._pe_patch.stop()
        self._aq_patch.stop()

    def test_queue_pending_events(self):
        from familiar.skills.email.monitor import _queue_pending_events
        from familiar.skills.email.pending_events import PendingEventStore

        cal_emails = [
            {
                "id": "msg-1",
                "subject": "Board Meeting March 25 at 6pm",
                "snippet": "Please join us for the quarterly board meeting on March 25 at 6pm",
                "from_name": "Chair",
                "from_email": "chair@gjl.org",
                "category": "Board",
                "calendar_hint": {
                    "has_event": True,
                    "has_rsvp": False,
                    "date_hint": "March 25",
                    "event_hint": "meeting",
                },
            }
        ]

        _queue_pending_events(cal_emails)

        store = PendingEventStore()
        pending = store.get_pending()
        assert len(pending) == 1
        assert "Board Meeting" in pending[0]["title"] or "meeting" in pending[0]["title"].lower()

    def test_queue_email_actions_donor_gift(self):
        from familiar.skills.email.action_queue import EmailActionStore
        from familiar.skills.email.monitor import _queue_email_actions

        np_emails = [
            {
                "id": "msg-1",
                "subject": "New donation received",
                "from_name": "Stripe",
                "from_email": "notifications@stripe.com",
                "category": "Donors",
                "nonprofit_signals": {
                    "donor_gift": True,
                    "gift_amount": "$500.00",
                },
            }
        ]

        _queue_email_actions(np_emails)

        store = EmailActionStore()
        pending = store.get_pending()
        assert len(pending) == 1
        assert pending[0]["action_type"] == "thank_you"
        assert "$500" in pending[0]["suggested_response"]

    def test_queue_email_actions_legislative(self):
        from familiar.skills.email.action_queue import EmailActionStore
        from familiar.skills.email.monitor import _queue_email_actions

        np_emails = [
            {
                "id": "msg-2",
                "subject": "ACTION ALERT: HB 1234",
                "from_name": "Coalition",
                "from_email": "alerts@coalition.org",
                "category": "Campaign",
                "nonprofit_signals": {"legislative_action": True},
            }
        ]

        _queue_email_actions(np_emails)

        store = EmailActionStore()
        pending = store.get_pending()
        assert len(pending) == 1
        assert pending[0]["action_type"] == "legislative_action"
        assert pending[0]["priority"] == "urgent"

    def test_queue_empty_lists(self):
        from familiar.skills.email.monitor import _queue_email_actions, _queue_pending_events

        # Should not raise
        _queue_pending_events([])
        _queue_email_actions([])
