"""Notifications are fail-open, single-message-per-outcome, and test-isolated.

`conftest.py` sets FACTORY_NOTIFY=off for the whole test session so no test can
reach the real botmaster/Telegram channel. Tests that assert on the dispatch
path itself opt back in with the `_notify_on` fixture, which flips the env var
only for that test — a real send still never happens because subprocess.run
is mocked underneath it.
"""

from __future__ import annotations

import subprocess
from unittest.mock import MagicMock, patch
from pathlib import Path

import pytest

from adw_modules import notify, supervisor
from adw_modules.data_types import ConfigDefaults, ObservabilityConfig, SSSFConfig
from adw_modules.runner import Run
from adw_modules.tracer import Tracer


@pytest.fixture
def _notify_on(monkeypatch):
    monkeypatch.setenv("FACTORY_NOTIFY", "on")


def _completed(returncode: int = 0, stderr: str = "") -> MagicMock:
    result = MagicMock()
    result.returncode = returncode
    result.stderr = stderr
    return result


class TestNotifyStart:
    def test_sends_to_overdeck_channel_with_slug_and_id(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_start("a1b2c3d4", "adw_prompt", "fix the login bug")
        args = run.call_args.args[0]
        assert args[:3] == ["botmaster", "--channel", "Overdeck"]
        message = args[3]
        assert "started" in message
        assert "a1b2c3d4" in message
        assert "adw_prompt" in message
        assert "fix the login bug" in message

    def test_falls_back_to_adw_id_when_no_name_or_hint(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_start("a1b2c3d4", None, None)
        message = run.call_args.args[0][3]
        assert "a1b2c3d4" in message


class TestNotifyFinish:
    def test_reports_pass_with_duration(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_finish("a1b2c3d4", "adw_prompt", ok=True, duration_s=42.4)
        message = run.call_args.args[0][3]
        assert "finished" in message
        assert "passed" in message
        assert "42" in message

    def test_reports_failure(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_finish("a1b2c3d4", None, ok=False, duration_s=None)
        message = run.call_args.args[0][3]
        assert "failed" in message

    def test_finish_crossing_marks_supervisor_terminal_state_once(self, tmp_path: Path,
                                                                    monkeypatch):
        status = tmp_path / "supervisor-status"
        monkeypatch.setenv("FACTORY_SUPERVISOR_STATUS_FILE", str(status))
        cfg = SSSFConfig(
            defaults=ConfigDefaults(data_dir=str(tmp_path / "data")),
            observability=ObservabilityConfig(db=str(tmp_path / "trace.db")),
        )
        tracer = Tracer(tmp_path / "trace.db", tmp_path / "events.jsonl")
        tracer.session_start("finish123", "tester", repo=tmp_path)
        run = Run(cfg, "finish123", tracer, "tester")
        try:
            supervisor.started(run.adw_id)
            assert status.read_text() == "started finish123\n"
            assert run.finish(accepted=False) == 1
            assert status.read_text() == "finished finish123\n"
        finally:
            tracer.conn.close()


class TestNotifyCrash:
    def test_owner_language_only_no_phase_names_or_error_dumps(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_crash("a1b2c3d4", "adw_prompt", duration_s=12.0)
        message = run.call_args.args[0][3]
        assert "CRASHED" in message
        assert "a1b2c3d4" in message
        assert "12" in message
        assert "quality" not in message
        assert "phase" not in message
        assert "command log" not in message

    def test_detail_is_a_single_trailing_line_when_given(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", return_value=_completed()) as run:
            notify.notify_crash("a1b2c3d4", None, detail="run vanished without finishing")
        message = run.call_args.args[0][3]
        lines = message.splitlines()
        assert len(lines) == 2
        assert lines[1] == "run vanished without finishing"


class TestFailOpen:
    def test_never_raises_when_botmaster_missing(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", side_effect=FileNotFoundError()):
            notify.notify_start("a1", "adw_prompt", None)  # must not raise

    def test_never_raises_on_nonzero_exit(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run",
                   return_value=_completed(returncode=1, stderr="telegram down")):
            notify.notify_finish("a1", None, ok=True, duration_s=1.0)  # must not raise

    def test_never_raises_on_timeout(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run",
                   side_effect=subprocess.TimeoutExpired(cmd="botmaster", timeout=10)):
            notify.notify_crash("a1", None)  # must not raise

    def test_never_raises_on_unexpected_error(self, _notify_on):
        with patch("adw_modules.notify.subprocess.run", side_effect=RuntimeError("boom")):
            notify.notify_start("a1", None, None)  # must not raise


class TestDisabledByDefaultInTests:
    """Canary: without opting in, no test can reach the real send path."""

    def test_send_is_a_no_op_when_factory_notify_is_off(self):
        with patch("adw_modules.notify.subprocess.run") as run:
            notify.notify_start("a1", "adw_prompt", "should never be sent")
            notify.notify_finish("a1", "adw_prompt", ok=True, duration_s=1.0)
            notify.notify_crash("a1", "adw_prompt", duration_s=1.0)
        run.assert_not_called()
