# 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

"""Unit tests for the background inbox monitor (familiar/skills/email/monitor.py)."""

import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from unittest import mock

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))

from familiar.skills.email.monitor import (
    _DEFAULT_CONFIG,
    _in_quiet_hours,
    _should_alert,
    check_new_priority_emails,
    format_alert_message,
    get_monitor_config,
    save_monitor_config,
)


@pytest.fixture(autouse=True)
def isolated_monitor(tmp_path, monkeypatch):
    """Redirect monitor + sorted cache files to tmp_path so tests never touch ~/.familiar."""
    monitor_path = tmp_path / "email_monitor.json"
    cache_path = tmp_path / "email_sorted_cache.json"
    monkeypatch.setattr(
        "familiar.skills.email.monitor._monitor_file",
        lambda: monitor_path,
    )
    monkeypatch.setattr(
        "familiar.skills.email.monitor._sorted_cache_path",
        lambda: cache_path,
    )
    return monitor_path


# ── Config load / save ─────────────────────────────────────────────────────────


class TestConfig:
    def test_defaults_when_missing(self):
        cfg = get_monitor_config()
        assert cfg["enabled"] is False
        assert cfg["interval_minutes"] == 10
        assert cfg["alert_on"] == ["urgent"]

    def test_save_and_reload(self, isolated_monitor):
        cfg = get_monitor_config()
        cfg["interval_minutes"] = 5
        cfg["watch_senders"] = ["boss@co.com"]
        save_monitor_config(cfg)

        loaded = get_monitor_config()
        assert loaded["interval_minutes"] == 5
        assert loaded["watch_senders"] == ["boss@co.com"]

    def test_file_permissions(self, isolated_monitor):
        save_monitor_config(get_monitor_config())
        assert oct(isolated_monitor.stat().st_mode)[-3:] == "600"

    def test_corrupted_file_returns_defaults(self, isolated_monitor):
        isolated_monitor.write_text("not json")
        cfg = get_monitor_config()
        assert cfg == dict(_DEFAULT_CONFIG)

    def test_partial_file_fills_defaults(self, isolated_monitor):
        isolated_monitor.write_text(json.dumps({"interval_minutes": 30}))
        cfg = get_monitor_config()
        assert cfg["interval_minutes"] == 30
        assert cfg["enabled"] is False  # default filled in


# ── Quiet hours ────────────────────────────────────────────────────────────────


class TestQuietHours:
    def _cfg(self, start, end):
        return {"quiet_hours_start": start, "quiet_hours_end": end}

    def test_inside_overnight_window(self):
        cfg = self._cfg("22:00", "07:00")
        with mock.patch("familiar.skills.email.monitor.datetime") as mock_dt:
            mock_dt.now.return_value = datetime(2026, 3, 11, 23, 0)
            mock_dt.strptime.side_effect = datetime.strptime
            assert _in_quiet_hours(cfg) is True

    def test_outside_overnight_window(self):
        cfg = self._cfg("22:00", "07:00")
        with mock.patch("familiar.skills.email.monitor.datetime") as mock_dt:
            mock_dt.now.return_value = datetime(2026, 3, 11, 10, 0)
            mock_dt.strptime.side_effect = datetime.strptime
            assert _in_quiet_hours(cfg) is False

    def test_inside_daytime_window(self):
        cfg = self._cfg("12:00", "13:00")
        with mock.patch("familiar.skills.email.monitor.datetime") as mock_dt:
            mock_dt.now.return_value = datetime(2026, 3, 11, 12, 30)
            mock_dt.strptime.side_effect = datetime.strptime
            assert _in_quiet_hours(cfg) is True

    def test_invalid_format_returns_false(self):
        cfg = self._cfg("not-a-time", "07:00")
        assert _in_quiet_hours(cfg) is False


# ── Alert filtering ────────────────────────────────────────────────────────────


class TestShouldAlert:
    def _email(self, priority="fyi", from_email="foo@example.com", subject="Hello"):
        return {
            "priority": priority,
            "from_email": from_email,
            "subject": subject,
            "snippet": "",
        }

    def test_urgent_triggers_alert(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": ["urgent"]}
        assert _should_alert(self._email(priority="urgent"), cfg) is True

    def test_fyi_does_not_trigger(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": ["urgent"]}
        assert _should_alert(self._email(priority="fyi"), cfg) is False

    def test_action_in_alert_on(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": ["urgent", "action"]}
        assert _should_alert(self._email(priority="action"), cfg) is True

    def test_watch_sender_exact_match(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_senders": ["editor@press.com"]}
        assert _should_alert(self._email(from_email="editor@press.com"), cfg) is True

    def test_watch_sender_domain_match(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_senders": ["press.com"]}
        assert _should_alert(self._email(from_email="editor@press.com"), cfg) is True

    def test_watch_sender_no_partial_match(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_senders": ["press.com"]}
        # "notpress.com" must NOT match "press.com"
        assert _should_alert(self._email(from_email="user@notpress.com"), cfg) is False

    def test_watch_topic_in_subject(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_topics": ["manuscript"]}
        assert _should_alert(self._email(subject="Re: manuscript revisions"), cfg) is True

    def test_watch_topic_no_match(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_topics": ["manuscript"]}
        assert _should_alert(self._email(subject="Hello there"), cfg) is False

    def test_empty_config_never_alerts(self):
        cfg = {**_DEFAULT_CONFIG, "alert_on": [], "watch_senders": [], "watch_topics": []}
        assert _should_alert(self._email(priority="urgent"), cfg) is False


# ── Alert message formatting ───────────────────────────────────────────────────


class TestFormatAlertMessage:
    def _hit(self, priority="urgent", from_name="Alice", subject="Test"):
        return {
            "priority": priority,
            "from_email": "alice@example.com",
            "from_name": from_name,
            "subject": subject,
            "snippet": "",
            "category": "Other",
        }

    def test_empty_returns_empty(self):
        assert format_alert_message([]) == ""

    def test_single_urgent(self):
        msg = format_alert_message([self._hit(priority="urgent")])
        assert "🔴" in msg
        assert "Alice" in msg
        assert "triage_inbox" in msg

    def test_plural_noun(self):
        hits = [self._hit(), self._hit(from_name="Bob")]
        msg = format_alert_message(hits)
        assert "emails" in msg

    def test_singular_noun(self):
        msg = format_alert_message([self._hit()])
        assert "email" in msg
        assert "emails" not in msg

    def test_overflow_shows_remaining(self):
        hits = [self._hit(priority="urgent")] * 10
        msg = format_alert_message(hits)
        assert "more" in msg

    def test_long_subject_truncated(self):
        long_subject = "A" * 100
        msg = format_alert_message([self._hit(subject=long_subject)])
        assert "…" in msg


# ── check_new_priority_emails integration ──────────────────────────────────────


class TestCheckNewPriorityEmails:
    def test_quiet_hours_returns_empty(self, monkeypatch):
        monkeypatch.setattr("familiar.skills.email.monitor._in_quiet_hours", lambda cfg: True)
        result = check_new_priority_emails()
        assert result == []

    def test_no_email_config_returns_empty(self, monkeypatch):
        monkeypatch.setattr("familiar.skills.email.monitor._in_quiet_hours", lambda cfg: False)
        monkeypatch.setattr(
            "familiar.skills.email.monitor._fetch_gmail_since",
            lambda svc, since: [],
        )
        monkeypatch.setattr(
            "familiar.skills.email.monitor._fetch_imap_since",
            lambda cfg, since: [],
        )
        result = check_new_priority_emails()
        assert result == []

    def test_last_check_time_updated(self, isolated_monitor, monkeypatch):
        monkeypatch.setattr("familiar.skills.email.monitor._in_quiet_hours", lambda cfg: False)
        monkeypatch.setattr(
            "familiar.skills.email.monitor._fetch_gmail_since",
            lambda svc, since: [],
        )
        monkeypatch.setattr(
            "familiar.skills.email.monitor._fetch_imap_since",
            lambda cfg, since: [],
        )
        check_new_priority_emails()
        cfg = get_monitor_config()
        assert cfg["last_check_time"] is not None

    def test_urgent_email_returned(self, isolated_monitor, monkeypatch):
        monkeypatch.setattr("familiar.skills.email.monitor._in_quiet_hours", lambda cfg: False)
        fake_email = {
            "id": "abc123",
            "from": "Boss <boss@co.com>",
            "subject": "URGENT: server down",
            "snippet": "Everything is on fire",
            "has_list_unsubscribe": False,
            "source": "imap",
        }
        monkeypatch.setattr(
            "familiar.skills.email.monitor._fetch_imap_since",
            lambda cfg, since: [fake_email],
        )
        # Patch gmail as unavailable so IMAP path runs
        monkeypatch.setattr(
            "familiar.skills.email.monitor._gmail_available",
            lambda: False,
            raising=False,
        )
        # Patch accounts so IMAP has a valid config
        mock_acct = {
            "address": "user@example.com",
            "password": "pass",
            "imap_server": "imap.example.com",
            "imap_port": 993,
        }
        monkeypatch.setattr(
            "familiar.skills.email.monitor.get_account",
            lambda: mock_acct,
            raising=False,
        )

        # Pre-set alert_on to include urgent so it triggers
        cfg = get_monitor_config()
        cfg["alert_on"] = ["urgent"]
        save_monitor_config(cfg)

        # Mock _classify_email to return urgent
        monkeypatch.setattr(
            "familiar.skills.email.monitor._classify_email",
            lambda *a, **kw: ("Other", "urgent"),
        )

        results = check_new_priority_emails()
        assert len(results) == 1
        assert results[0]["priority"] == "urgent"


# ── Proactive skill tools ──────────────────────────────────────────────────────


class TestProactiveWatchTools:
    def test_add_priority_contact(self, monkeypatch):
        from familiar.skills.proactive.skill import add_priority_contact

        result = add_priority_contact({"sender": "writer@publisher.com"})
        assert "writer@publisher.com" in result

        cfg = get_monitor_config()
        assert "writer@publisher.com" in cfg["watch_senders"]

    def test_add_priority_contact_deduplicates(self, monkeypatch):
        from familiar.skills.proactive.skill import add_priority_contact

        add_priority_contact({"sender": "writer@publisher.com"})
        result = add_priority_contact({"sender": "writer@publisher.com"})
        assert "Already watching" in result

        cfg = get_monitor_config()
        assert cfg["watch_senders"].count("writer@publisher.com") == 1

    def test_remove_priority_contact(self):
        from familiar.skills.proactive.skill import (
            add_priority_contact,
            remove_priority_contact,
        )

        add_priority_contact({"sender": "editor@mag.com"})
        result = remove_priority_contact({"sender": "editor@mag.com"})
        assert "Removed" in result

        cfg = get_monitor_config()
        assert "editor@mag.com" not in cfg["watch_senders"]

    def test_remove_nonexistent(self):
        from familiar.skills.proactive.skill import remove_priority_contact

        result = remove_priority_contact({"sender": "ghost@nowhere.com"})
        assert "not in the watch list" in result

    def test_watch_inbox_status_disabled(self):
        from familiar.skills.proactive.skill import watch_inbox_status

        result = watch_inbox_status({})
        assert "Paused" in result or "Active" in result

    def test_snooze_email(self, monkeypatch):
        from familiar.core.scheduler import reset_scheduler
        from familiar.skills.proactive.skill import snooze_email

        reset_scheduler()
        result = snooze_email({"subject": "Manuscript feedback", "hours": 2})
        assert "2h" in result
        assert "Manuscript feedback" in result
        reset_scheduler()
