"""
Tests for Phase 45: Notification Preferences.
"""

import json
from datetime import datetime, timezone, timedelta

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all storage to tmp_path so tests are isolated."""
    prefs_dir = tmp_path / "notification_prefs"
    digests_dir = prefs_dir / "digests"
    for d in (prefs_dir, digests_dir):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.notification_prefs as np
    monkeypatch.setattr(np, "PREFS_DIR", prefs_dir)
    monkeypatch.setattr(np, "DIGESTS_DIR", digests_dir)
    monkeypatch.setattr(np, "BROKER_DIR", tmp_path)


# ── Defaults ──────────────────────────────────────────────────────────────


class TestDefaults:
    def test_get_default_preferences_returns_dict(self):
        from brynq.notification_prefs import get_default_preferences
        defaults = get_default_preferences()
        assert isinstance(defaults, dict)
        assert "channels" in defaults
        assert "events" in defaults
        assert "quiet_hours" in defaults
        assert "delivery_methods" in defaults

    def test_defaults_quiet_hours_structure(self):
        from brynq.notification_prefs import get_default_preferences
        qh = get_default_preferences()["quiet_hours"]
        assert qh["enabled"] is False
        assert qh["start"] == "22:00"
        assert qh["end"] == "08:00"

    def test_defaults_dnd_disabled(self):
        from brynq.notification_prefs import get_default_preferences
        dnd = get_default_preferences()["dnd"]
        assert dnd["enabled"] is False
        assert dnd["until"] is None


# ── Set / Get / Reset Preferences ─────────────────────────────────────────


class TestSetGetReset:
    def test_set_and_get_preferences(self):
        from brynq.notification_prefs import set_preferences, get_preferences
        set_preferences("agent-1", {"digest_interval_hours": 6})
        prefs = get_preferences("agent-1")
        assert prefs["agent_id"] == "agent-1"
        assert prefs["digest_interval_hours"] == 6

    def test_set_merges_with_defaults(self):
        from brynq.notification_prefs import set_preferences
        result = set_preferences("agent-2", {"digest_interval_hours": 2})
        # delivery_methods should still have the default
        assert "delivery_methods" in result
        assert result["delivery_methods"] == ["broker", "webhook", "email"]

    def test_get_unsaved_returns_defaults(self):
        from brynq.notification_prefs import get_preferences
        prefs = get_preferences("nobody")
        assert prefs["agent_id"] == "nobody"
        assert prefs["muted_agents"] == []
        assert prefs["dnd"]["enabled"] is False

    def test_set_nested_dict_merges(self):
        from brynq.notification_prefs import set_preferences, get_preferences
        set_preferences("agent-3", {"quiet_hours": {"enabled": True}})
        prefs = get_preferences("agent-3")
        assert prefs["quiet_hours"]["enabled"] is True
        # Original defaults for start/end should still be there
        assert prefs["quiet_hours"]["start"] == "22:00"

    def test_reset_preferences(self):
        from brynq.notification_prefs import set_preferences, reset_preferences, get_preferences
        set_preferences("agent-r", {"digest_interval_hours": 1})
        reset_preferences("agent-r")
        prefs = get_preferences("agent-r")
        assert prefs["digest_interval_hours"] == 4  # default

    def test_set_preserves_updated_at(self):
        from brynq.notification_prefs import set_preferences
        result = set_preferences("agent-ts", {})
        assert "updated_at" in result


# ── Should Notify ─────────────────────────────────────────────────────────


class TestShouldNotify:
    def test_default_allows(self):
        from brynq.notification_prefs import should_notify
        send, method, reason = should_notify("agent-x", "task_match")
        assert send is True
        assert method == "broker"
        assert reason == "allowed"

    def test_dnd_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("dnd-agent", {"dnd": {"enabled": True, "until": None}})
        send, method, reason = should_notify("dnd-agent", "task_match")
        assert send is False
        assert reason == "dnd_permanent"

    def test_dnd_timed_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        future = (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat()
        set_preferences("dnd-t", {"dnd": {"enabled": True, "until": future}})
        send, _, reason = should_notify("dnd-t", "task_match")
        assert send is False
        assert reason == "dnd_active"

    def test_dnd_expired_allows(self):
        from brynq.notification_prefs import set_preferences, should_notify
        past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        set_preferences("dnd-exp", {"dnd": {"enabled": True, "until": past}})
        send, _, reason = should_notify("dnd-exp", "task_match")
        assert send is True

    def test_muted_sender_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("recv", {"muted_agents": ["spammer"]})
        send, _, reason = should_notify("recv", "task_match", sender_id="spammer")
        assert send is False
        assert reason == "sender_muted"

    def test_muted_channel_blocks(self):
        from brynq.notification_prefs import mute_channel, should_notify
        mute_channel("ch-agent", "noisy-channel")
        send, _, reason = should_notify("ch-agent", "task_match", channel="noisy-channel")
        assert send is False
        assert reason == "channel_muted"

    def test_event_disabled_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("ev-agent", {
            "events": {"follow_new": {"enabled": False, "delivery": "instant"}},
        })
        send, _, reason = should_notify("ev-agent", "follow_new")
        assert send is False
        assert reason == "event_disabled"

    def test_event_off_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("ev-off", {
            "events": {"mention": {"enabled": True, "delivery": "off"}},
        })
        send, _, reason = should_notify("ev-off", "mention")
        assert send is False
        assert reason == "event_off"

    def test_event_digest_delivery(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("ev-dig", {
            "events": {"task_match": {"enabled": True, "delivery": "digest"}},
        })
        send, method, reason = should_notify("ev-dig", "task_match")
        assert send is True
        assert reason == "digest"

    def test_channel_disabled_blocks(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("ch-dis", {
            "channels": {"alerts": {"enabled": False, "priority_only": False}},
        })
        send, _, reason = should_notify("ch-dis", "task_match", channel="alerts")
        assert send is False
        assert reason == "channel_disabled"


# ── Mute / Unmute ─────────────────────────────────────────────────────────


class TestMuteUnmute:
    def test_mute_channel_permanent(self):
        from brynq.notification_prefs import mute_channel, list_muted
        mute_channel("m1", "spam-channel")
        muted = list_muted("m1")
        assert "spam-channel" in muted["muted_channels"]
        assert muted["muted_channels"]["spam-channel"]["until"] is None

    def test_mute_channel_temporary(self):
        from brynq.notification_prefs import mute_channel, list_muted
        mute_channel("m2", "busy-channel", duration_hours=2)
        muted = list_muted("m2")
        assert "busy-channel" in muted["muted_channels"]
        assert muted["muted_channels"]["busy-channel"]["until"] is not None

    def test_unmute_channel(self):
        from brynq.notification_prefs import mute_channel, unmute_channel, list_muted
        mute_channel("m3", "test-ch")
        unmute_channel("m3", "test-ch")
        muted = list_muted("m3")
        assert "test-ch" not in muted["muted_channels"]

    def test_mute_agent(self):
        from brynq.notification_prefs import mute_agent, list_muted
        mute_agent("me", "annoying-bot")
        muted = list_muted("me")
        assert "annoying-bot" in muted["muted_agents"]

    def test_mute_agent_idempotent(self):
        from brynq.notification_prefs import mute_agent
        mute_agent("me2", "bot-x")
        result = mute_agent("me2", "bot-x")
        assert result.count("bot-x") == 1

    def test_unmute_agent(self):
        from brynq.notification_prefs import mute_agent, unmute_agent, list_muted
        mute_agent("me3", "bot-y")
        unmute_agent("me3", "bot-y")
        muted = list_muted("me3")
        assert "bot-y" not in muted["muted_agents"]

    def test_list_muted_empty(self):
        from brynq.notification_prefs import list_muted
        muted = list_muted("fresh-agent")
        assert muted["muted_channels"] == {}
        assert muted["muted_agents"] == []


# ── Digest Queue ──────────────────────────────────────────────────────────


class TestDigest:
    def test_queue_for_digest(self):
        from brynq.notification_prefs import queue_for_digest, get_digest
        queue_for_digest("d1", {"title": "Task A", "event_type": "task_match"})
        queue_for_digest("d1", {"title": "Task B", "event_type": "follow_new"})
        items = get_digest("d1", clear=False)
        assert len(items) == 2

    def test_get_digest_clears(self):
        from brynq.notification_prefs import queue_for_digest, get_digest
        queue_for_digest("d2", {"title": "X"})
        items = get_digest("d2", clear=True)
        assert len(items) == 1
        # Second call should be empty
        assert get_digest("d2") == []

    def test_get_digest_no_clear(self):
        from brynq.notification_prefs import queue_for_digest, get_digest
        queue_for_digest("d3", {"title": "Y"})
        get_digest("d3", clear=False)
        assert len(get_digest("d3", clear=False)) == 1

    def test_get_digest_summary(self):
        from brynq.notification_prefs import queue_for_digest, get_digest_summary
        queue_for_digest("d4", {"title": "Notif 1"})
        queue_for_digest("d4", {"title": "Notif 2"})
        queue_for_digest("d4", {"title": "Notif 3"})
        summary = get_digest_summary("d4")
        assert summary["count"] == 3
        assert len(summary["preview"]) == 3

    def test_digest_summary_empty(self):
        from brynq.notification_prefs import get_digest_summary
        summary = get_digest_summary("ghost")
        assert summary["count"] == 0
        assert summary["preview"] == []

    def test_digest_summary_preview_limit(self):
        from brynq.notification_prefs import queue_for_digest, get_digest_summary
        for i in range(8):
            queue_for_digest("d5", {"title": f"N{i}"})
        summary = get_digest_summary("d5")
        assert summary["count"] == 8
        assert len(summary["preview"]) == 5  # capped at 5


# ── Do Not Disturb ────────────────────────────────────────────────────────


class TestDND:
    def test_enable_dnd_permanent(self):
        from brynq.notification_prefs import enable_dnd, is_dnd
        enable_dnd("dnd1")
        assert is_dnd("dnd1") is True

    def test_enable_dnd_timed(self):
        from brynq.notification_prefs import enable_dnd, is_dnd
        enable_dnd("dnd2", duration_hours=1)
        assert is_dnd("dnd2") is True

    def test_disable_dnd(self):
        from brynq.notification_prefs import enable_dnd, disable_dnd, is_dnd
        enable_dnd("dnd3")
        disable_dnd("dnd3")
        assert is_dnd("dnd3") is False

    def test_is_dnd_default_false(self):
        from brynq.notification_prefs import is_dnd
        assert is_dnd("nobody") is False

    def test_dnd_expired(self, monkeypatch):
        from brynq.notification_prefs import set_preferences, is_dnd
        past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        set_preferences("dnd-exp", {"dnd": {"enabled": True, "until": past}})
        assert is_dnd("dnd-exp") is False

    def test_dnd_blocks_should_notify(self):
        from brynq.notification_prefs import enable_dnd, should_notify
        enable_dnd("dnd-sn")
        send, _, reason = should_notify("dnd-sn", "task_match")
        assert send is False
        assert "dnd" in reason


# ── Quiet Hours Helper ────────────────────────────────────────────────────


class TestQuietHours:
    def test_quiet_hours_blocks_should_notify(self, monkeypatch):
        """Force _in_quiet_hours to return True and verify should_notify blocks."""
        import brynq.notification_prefs as np
        from brynq.notification_prefs import set_preferences, should_notify

        set_preferences("qh-agent", {"quiet_hours": {"enabled": True, "start": "00:00", "end": "23:59", "timezone": "UTC"}})
        # Patch _in_quiet_hours to always return True (avoids clock-dependent flakiness)
        monkeypatch.setattr(np, "_in_quiet_hours", lambda qh: True)
        send, _, reason = should_notify("qh-agent", "task_match")
        assert send is False
        assert reason == "quiet_hours"

    def test_quiet_hours_disabled_allows(self):
        from brynq.notification_prefs import set_preferences, should_notify
        set_preferences("qh-off", {"quiet_hours": {"enabled": False}})
        send, _, _ = should_notify("qh-off", "task_match")
        assert send is True
