"""
Tests for brynq.cli — CLI command parsing and handlers.

Tests verify that argparse subcommands parse correctly and that
CLI handler functions produce expected output using isolated
broker directories.
"""

import json
import os
import uuid
from argparse import Namespace
from datetime import datetime, timezone
from io import StringIO
from unittest.mock import patch

import pytest


# ── Isolation fixtures ───────────────────────────────────────────────────


@pytest.fixture(autouse=True)
def _isolate_cli(broker_dirs, monkeypatch):
    """Redirect CLI's server imports to the temp broker dirs."""
    import brynq.cli as cli_mod
    monkeypatch.setattr(cli_mod, "BROKER_DIR", broker_dirs["channels"].parent)
    monkeypatch.setattr(cli_mod, "CHANNELS_DIR", broker_dirs["channels"])
    monkeypatch.setattr(cli_mod, "SESSIONS_DIR", broker_dirs["sessions"])
    monkeypatch.setattr(cli_mod, "CURSORS_DIR", broker_dirs["cursors"])


# ── Parser Tests ─────────────────────────────────────────────────────────


class TestParserPost:
    def test_post_requires_message(self):
        """The 'post' subcommand requires --message."""
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("post")
        p.add_argument("--message", "-m", required=True)
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--msg-type", "-t", default="info")
        with pytest.raises(SystemExit):
            parser.parse_args(["post"])

    def test_post_parses_all_flags(self):
        """Verify post flags parse into the expected namespace."""
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("post")
        p.add_argument("--message", "-m", required=True)
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--msg-type", "-t", default="info")

        args = parser.parse_args(["post", "-m", "hello", "-c", "dev", "-t", "alert"])
        assert args.message == "hello"
        assert args.channel == "dev"
        assert args.msg_type == "alert"

    def test_post_defaults(self):
        """Channel defaults to 'general', msg_type defaults to 'info'."""
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("post")
        p.add_argument("--message", "-m", required=True)
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--msg-type", "-t", default="info")

        args = parser.parse_args(["post", "-m", "hi"])
        assert args.channel == "general"
        assert args.msg_type == "info"


class TestParserRead:
    def test_read_defaults(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("read")
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--peek", action="store_true")

        args = parser.parse_args(["read"])
        assert args.channel == "general"
        assert args.peek is False

    def test_read_with_peek_flag(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("read")
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--peek", action="store_true")

        args = parser.parse_args(["read", "--peek", "-c", "alerts"])
        assert args.peek is True
        assert args.channel == "alerts"


class TestParserHistory:
    def test_history_defaults(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("history")
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--limit", "-n", type=int, default=20)

        args = parser.parse_args(["history"])
        assert args.channel == "general"
        assert args.limit == 20

    def test_history_custom_limit(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("history")
        p.add_argument("--channel", "-c", default="general")
        p.add_argument("--limit", "-n", type=int, default=20)

        args = parser.parse_args(["history", "-c", "dev", "-n", "5"])
        assert args.channel == "dev"
        assert args.limit == 5


class TestParserBroadcast:
    def test_broadcast_requires_message(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("broadcast")
        p.add_argument("--message", "-m", required=True)
        p.add_argument("--msg-type", "-t", default="alert")

        with pytest.raises(SystemExit):
            parser.parse_args(["broadcast"])

    def test_broadcast_defaults_to_alert(self):
        import argparse
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        p = sub.add_parser("broadcast")
        p.add_argument("--message", "-m", required=True)
        p.add_argument("--msg-type", "-t", default="alert")

        args = parser.parse_args(["broadcast", "-m", "restart"])
        assert args.msg_type == "alert"


class TestParserSearch:
    def test_search_all_filters(self):
        import argparse
        from brynq.search import add_search_subparser
        parser = argparse.ArgumentParser()
        sub = parser.add_subparsers(dest="command")
        add_search_subparser(sub)

        args = parser.parse_args([
            "search", "-q", "backfill", "-c", "gen21",
            "-s", "sigma", "-p", "gemini", "-t", "alert", "-n", "5",
        ])
        assert args.query == "backfill"
        assert args.channel == "gen21"
        assert args.sender == "sigma"
        assert args.platform == "gemini"
        assert args.msg_type == "alert"
        assert args.limit == 5


# ── CLI Command Handler Tests ─────────────────────────────────────────────


class TestCmdPost:
    def test_posts_message_to_channel_file(self, broker_dirs):
        from brynq.cli import cmd_post
        args = Namespace(channel="dev", message="hello world", msg_type="info")
        cmd_post(args)
        path = broker_dirs["channels"] / "dev.jsonl"
        assert path.exists()
        data = json.loads(path.read_text("utf-8").strip())
        assert data["message"] == "hello world"
        assert data["channel"] == "dev"
        assert data["msg_type"] == "info"

    def test_posts_with_custom_msg_type(self, broker_dirs):
        from brynq.cli import cmd_post
        args = Namespace(channel="alerts", message="critical", msg_type="alert")
        cmd_post(args)
        path = broker_dirs["channels"] / "alerts.jsonl"
        data = json.loads(path.read_text("utf-8").strip())
        assert data["msg_type"] == "alert"


class TestCmdRead:
    def test_reads_new_messages(self, broker_dirs, capsys):
        from brynq.cli import cmd_post, cmd_read
        import brynq.cli as cli_mod

        # Post a message from a different session to avoid self-filtering
        from brynq.server import _append_jsonl_sync
        path = broker_dirs["channels"] / "general.jsonl"
        _append_jsonl_sync(path, {
            "session_id": "other-session",
            "session_name": "OtherBot",
            "platform": "cli",
            "msg_type": "info",
            "message": "hey there",
            "timestamp": datetime.now(timezone.utc).isoformat(),
        })

        args = Namespace(channel="general", peek=False)
        cmd_read(args)
        output = capsys.readouterr().out
        assert "hey there" in output

    def test_peek_does_not_advance_cursor(self, broker_dirs):
        from brynq.cli import cmd_read, SESSION_ID
        from brynq.server import _append_jsonl_sync, _read_cursor_sync

        path = broker_dirs["channels"] / "general.jsonl"
        _append_jsonl_sync(path, {"session_id": "other", "message": "peek test"})

        args = Namespace(channel="general", peek=True)
        cmd_read(args)
        cursor = _read_cursor_sync(SESSION_ID, "general")
        assert cursor == 0  # Cursor not advanced in peek mode

    def test_no_new_messages(self, broker_dirs, capsys):
        from brynq.cli import cmd_read
        args = Namespace(channel="empty", peek=False)
        cmd_read(args)
        output = capsys.readouterr().out
        assert "No new messages" in output


class TestCmdInbox:
    def test_all_caught_up(self, broker_dirs, capsys):
        # Clear any auto-posted system messages (e.g., join notifications)
        for f in broker_dirs["channels"].glob("*.jsonl"):
            f.unlink()
        # Create the announce marker so _update_heartbeat_sync doesn't
        # re-post a join message to #_system
        from brynq.server import SESSION_ID, SESSIONS_DIR
        marker = SESSIONS_DIR / f".announced_{SESSION_ID}"
        marker.write_text(SESSION_ID)
        from brynq.cli import cmd_inbox
        cmd_inbox(Namespace())
        output = capsys.readouterr().out
        assert "All caught up" in output

    def test_shows_unread_channels(self, broker_dirs, capsys):
        from brynq.server import _append_jsonl_sync
        _append_jsonl_sync(
            broker_dirs["channels"] / "dev.jsonl",
            {"session_id": "other", "message": "stuff"},
        )
        from brynq.cli import cmd_inbox
        cmd_inbox(Namespace())
        output = capsys.readouterr().out
        assert "#dev" in output
        assert "unread" in output


class TestCmdChannels:
    def test_lists_channels(self, broker_dirs, capsys):
        from brynq.server import _append_jsonl_sync
        _append_jsonl_sync(
            broker_dirs["channels"] / "alpha.jsonl", {"message": "a"}
        )
        _append_jsonl_sync(
            broker_dirs["channels"] / "beta.jsonl", {"message": "b"}
        )
        from brynq.cli import cmd_channels
        cmd_channels(Namespace())
        output = capsys.readouterr().out
        assert "#alpha" in output
        assert "#beta" in output
        assert "1 messages" in output


class TestCmdHistory:
    def test_shows_recent_messages(self, broker_dirs, capsys):
        from brynq.server import _append_jsonl_sync
        path = broker_dirs["channels"] / "dev.jsonl"
        for i in range(5):
            _append_jsonl_sync(path, {
                "session_id": "s1",
                "session_name": "bot",
                "platform": "cli",
                "msg_type": "info",
                "message": f"msg-{i}",
                "timestamp": datetime.now(timezone.utc).isoformat(),
            })

        from brynq.cli import cmd_history
        args = Namespace(channel="dev", limit=3)
        cmd_history(args)
        output = capsys.readouterr().out
        assert "msg-4" in output
        assert "msg-2" in output
        # msg-0 and msg-1 should not be in the last 3
        assert "last 3 of 5" in output

    def test_empty_channel(self, broker_dirs, capsys):
        from brynq.cli import cmd_history
        cmd_history(Namespace(channel="nonexistent", limit=20))
        output = capsys.readouterr().out
        assert "No messages" in output


class TestCmdBroadcast:
    def test_broadcasts_to_existing_channels(self, broker_dirs, capsys):
        from brynq.server import _append_jsonl_sync, _read_jsonl_sync

        # Seed two channels
        _append_jsonl_sync(broker_dirs["channels"] / "dev.jsonl", {"message": "seed"})
        _append_jsonl_sync(broker_dirs["channels"] / "ops.jsonl", {"message": "seed"})

        from brynq.cli import cmd_broadcast
        args = Namespace(message="system restart in 5", msg_type="alert")
        cmd_broadcast(args)

        output = capsys.readouterr().out
        assert "Broadcast" in output

        # Both channels should have the broadcast + seed
        dev_msgs = _read_jsonl_sync(broker_dirs["channels"] / "dev.jsonl")
        ops_msgs = _read_jsonl_sync(broker_dirs["channels"] / "ops.jsonl")
        assert any(m["message"] == "system restart in 5" for m in dev_msgs)
        assert any(m["message"] == "system restart in 5" for m in ops_msgs)

    def test_broadcast_creates_general_if_missing(self, broker_dirs, capsys):
        from brynq.cli import cmd_broadcast
        from brynq.server import _read_jsonl_sync

        args = Namespace(message="hello all", msg_type="alert")
        cmd_broadcast(args)

        general = broker_dirs["channels"] / "general.jsonl"
        assert general.exists()
        msgs = _read_jsonl_sync(general)
        assert any(m["message"] == "hello all" for m in msgs)


class TestCmdSessions:
    def test_lists_active_sessions(self, broker_dirs, capsys):
        from brynq.server import _write_json_sync

        _write_json_sync(broker_dirs["sessions"] / "s1.json", {
            "session_id": "s1",
            "name": "bot-alpha",
            "platform": "claude",
            "pid": 12345,
            "heartbeat": datetime.now(timezone.utc).isoformat(),
        })

        from brynq.cli import cmd_sessions
        cmd_sessions(Namespace())
        output = capsys.readouterr().out
        assert "bot-alpha" in output
        assert "@claude" in output
        assert "12345" in output


class TestCmdStatus:
    def test_shows_session_info(self, broker_dirs, capsys):
        from brynq.cli import cmd_status
        # Patch vault import to avoid import errors
        with patch("brynq.cli.cmd_inbox"):
            cmd_status(Namespace())
        output = capsys.readouterr().out
        assert "Session:" in output
        assert "Platform:" in output
