"""Tests for b2alpha.cli – Click commands (mocked backends)."""

from __future__ import annotations

import json
from unittest.mock import patch, MagicMock

import pytest
from click.testing import CliRunner

from b2alpha.cli import cli
from b2alpha import __version__


@pytest.fixture
def runner():
    return CliRunner()


# ---------------------------------------------------------------------------
# Version & help
# ---------------------------------------------------------------------------

class TestBasic:
    def test_version(self, runner):
        result = runner.invoke(cli, ["--version"])
        assert result.exit_code == 0
        assert __version__ in result.output

    def test_help(self, runner):
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "B2Alpha" in result.output


# ---------------------------------------------------------------------------
# logout
# ---------------------------------------------------------------------------

class TestLogout:
    def test_logout_clears_file(self, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        auth_file = tmp_path / "auth.json"
        auth_file.write_text('{"access_token": "tok"}')
        monkeypatch.setattr(cfg, "AUTH_FILE", auth_file)

        result = runner.invoke(cli, ["logout"])
        assert result.exit_code == 0
        assert "Logged out" in result.output
        assert not auth_file.exists()

    def test_logout_no_file(self, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        monkeypatch.setattr(cfg, "AUTH_FILE", tmp_path / "nope.json")

        result = runner.invoke(cli, ["logout"])
        assert result.exit_code == 0
        assert "Logged out" in result.output


# ---------------------------------------------------------------------------
# whoami (now includes auth status)
# ---------------------------------------------------------------------------

class TestWhoami:
    def test_not_logged_in(self, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        monkeypatch.setattr(cfg, "AUTH_FILE", tmp_path / "auth.json")
        monkeypatch.setattr(cfg, "PROFILE_FILE", tmp_path / "profile.json")

        result = runner.invoke(cli, ["whoami"])
        assert result.exit_code == 0
        assert "Not logged in" in result.output

    def test_logged_in_no_agent(self, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        auth_file = tmp_path / "auth.json"
        auth_file.write_text(json.dumps({"access_token": "tok"}))
        monkeypatch.setattr(cfg, "AUTH_FILE", auth_file)
        monkeypatch.setattr(cfg, "PROFILE_FILE", tmp_path / "profile.json")

        result = runner.invoke(cli, ["whoami"])
        assert result.exit_code == 0
        assert "Authenticated" in result.output
        assert "No agent registered" in result.output

    def test_with_profile(self, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        auth_file = tmp_path / "auth.json"
        auth_file.write_text(json.dumps({"access_token": "tok"}))
        monkeypatch.setattr(cfg, "AUTH_FILE", auth_file)
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zTest", "display_name": "Bot", "agent_type": "user"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        result = runner.invoke(cli, ["whoami"])
        assert result.exit_code == 0
        assert "Authenticated" in result.output
        assert "did:b2a:zTest" in result.output
        assert "Bot" in result.output


# ---------------------------------------------------------------------------
# search (now absorbs lookup)
# ---------------------------------------------------------------------------

class TestSearch:
    @patch("b2alpha.api.search_phonebook")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_search_results(self, _tok, mock_search, runner):
        mock_search.return_value = {
            "agents": [
                {"did": "did:b2a:z1", "display_name": "WeatherBot", "agent_type": 1, "capabilities": ["weather"]},
            ],
            "count": 1,
        }

        result = runner.invoke(cli, ["search", "weather"])
        assert result.exit_code == 0
        assert "did:b2a:z1" in result.output
        assert "WeatherBot" in result.output
        assert "weather" in result.output
        mock_search.assert_called_once_with(query="weather", agent_type=None, limit=20, token="tok")

    @patch("b2alpha.api.search_phonebook")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_search_empty(self, _tok, mock_search, runner):
        mock_search.return_value = {"agents": [], "count": 0}

        result = runner.invoke(cli, ["search", "nonexistent"])
        assert result.exit_code == 0
        assert "No agents found" in result.output

    @patch("b2alpha.api.search_phonebook")
    @patch("b2alpha.auth.get_valid_token", side_effect=RuntimeError("Not logged in"))
    def test_search_without_auth(self, _tok, mock_search, runner):
        """Search still works without auth (falls back to anon)."""
        mock_search.return_value = {"agents": [], "count": 0}

        result = runner.invoke(cli, ["search", "test"])
        assert result.exit_code == 0
        mock_search.assert_called_once_with(query="test", agent_type=None, limit=20, token=None)

    @patch("b2alpha.api.search_phonebook")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_search_error(self, _tok, mock_search, runner):
        mock_search.side_effect = RuntimeError("search_failed")

        result = runner.invoke(cli, ["search", "test"])
        assert result.exit_code == 1

    @patch("b2alpha.api.lookup_agent")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_search_did_does_lookup(self, _tok, mock_lookup, runner):
        """Passing a DID to search does a direct lookup."""
        mock_lookup.return_value = {
            "agent": {
                "did": "did:b2a:zABC",
                "display_name": "TestBot",
                "agent_type": 0,
                "capabilities": ["chat"],
                "region": "us-east-1",
                "status": 1,
                "registered_at": "2026-01-01T00:00:00Z",
            },
        }

        result = runner.invoke(cli, ["search", "did:b2a:zABC"])
        assert result.exit_code == 0
        assert "did:b2a:zABC" in result.output
        assert "TestBot" in result.output
        assert "active" in result.output
        mock_lookup.assert_called_once_with("did:b2a:zABC", token="tok")

    @patch("b2alpha.api.lookup_agent")
    @patch("b2alpha.auth.get_valid_token", side_effect=RuntimeError("Not logged in"))
    def test_search_did_without_auth(self, _tok, mock_lookup, runner):
        mock_lookup.return_value = {
            "agent": {
                "did": "did:b2a:zABC",
                "display_name": "TestBot",
                "agent_type": 0,
                "capabilities": [],
                "region": "",
                "status": 1,
                "registered_at": "2026-01-01T00:00:00Z",
            },
        }

        result = runner.invoke(cli, ["search", "did:b2a:zABC"])
        assert result.exit_code == 0
        mock_lookup.assert_called_once_with("did:b2a:zABC", token=None)


# ---------------------------------------------------------------------------
# send
# ---------------------------------------------------------------------------

class TestSend:
    @patch("b2alpha.api.send_message")
    @patch("b2alpha.crypto.sign_message")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_send_success(self, _tok, mock_sign, mock_send, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zSender"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_sign.return_value = {"message_id": "m1", "payload": "x", "signature": "s", "protocol_version": "0.1"}
        mock_send.return_value = {"ok": True, "interaction_id": "conv1", "message_id": "m1"}

        result = runner.invoke(cli, ["send", "--to", "did:b2a:zRecip", "-m", "Hello!"])
        assert result.exit_code == 0
        assert "Message sent" in result.output
        assert "conv1" in result.output

    @patch("b2alpha.auth.get_valid_token", side_effect=RuntimeError("Not logged in"))
    def test_send_not_logged_in(self, _tok, runner):
        result = runner.invoke(cli, ["send", "--to", "did:b2a:zR", "-m", "Hi"])
        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# inbox (replaces conversations)
# ---------------------------------------------------------------------------

class TestInbox:
    @patch("b2alpha.api.list_conversations")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_inbox_with_messages(self, _tok, mock_list, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        import base64
        payload = base64.urlsafe_b64encode(json.dumps({"text": "Hey there!"}).encode()).decode().rstrip("=")

        mock_list.return_value = {
            "conversations": [
                {
                    "interaction_id": "abc123456789",
                    "participant_a_did": "did:b2a:zMe",
                    "participant_b_did": "did:b2a:zOther",
                    "message_count": 5,
                    "my_message_count": 3,
                    "last_message": {
                        "sender_did": "did:b2a:zOther",
                        "payload": payload,
                        "created_at": "2026-02-20T00:00:00Z",
                    },
                    "updated_at": "2026-02-20T00:00:00Z",
                    "ended_at": None,
                },
            ],
        }

        result = runner.invoke(cli, ["inbox"])
        assert result.exit_code == 0
        assert "did:b2a:zOther" in result.output
        assert "messages: 5" in result.output
        assert "Hey there!" in result.output

    @patch("b2alpha.api.list_conversations")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_inbox_empty(self, _tok, mock_list, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_list.return_value = {"conversations": []}

        result = runner.invoke(cli, ["inbox"])
        assert result.exit_code == 0
        assert "Inbox empty" in result.output


# ---------------------------------------------------------------------------
# read
# ---------------------------------------------------------------------------

class TestRead:
    @patch("b2alpha.api.get_conversation")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_read_conversation(self, _tok, mock_get, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        import base64
        p1 = base64.urlsafe_b64encode(json.dumps({"text": "Hello!"}).encode()).decode().rstrip("=")
        p2 = base64.urlsafe_b64encode(json.dumps({"text": "Hi back!"}).encode()).decode().rstrip("=")

        mock_get.return_value = {
            "conversation": {
                "interaction_id": "conv123",
                "participant_a_did": "did:b2a:zMe",
                "participant_b_did": "did:b2a:zOther",
                "messages": [
                    {"sender_did": "did:b2a:zOther", "payload": p1, "created_at": "2026-02-20T10:00:00Z"},
                    {"sender_did": "did:b2a:zMe", "payload": p2, "created_at": "2026-02-20T10:01:00Z"},
                ],
                "started_at": "2026-02-20T10:00:00Z",
                "updated_at": "2026-02-20T10:01:00Z",
                "ended_at": None,
            },
        }

        result = runner.invoke(cli, ["read", "conv123"])
        assert result.exit_code == 0
        assert "Hello!" in result.output
        assert "Hi back!" in result.output
        assert "did:b2a:zOther" in result.output
        mock_get.assert_called_once_with("tok", "conv123")

    @patch("b2alpha.api.get_conversation")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_read_not_found(self, _tok, mock_get, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_get.side_effect = RuntimeError("conversation_not_found")

        result = runner.invoke(cli, ["read", "nonexistent"])
        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# requests
# ---------------------------------------------------------------------------

class TestRequests:
    @patch("b2alpha.api.list_conversations")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_requests_pending(self, _tok, mock_list, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        import base64
        payload = base64.urlsafe_b64encode(json.dumps({"text": "Can we chat?"}).encode()).decode().rstrip("=")

        mock_list.return_value = {
            "conversations": [
                {
                    "interaction_id": "req123456789",
                    "participant_a_did": "did:b2a:zMe",
                    "participant_b_did": "did:b2a:zStranger",
                    "message_count": 1,
                    "my_message_count": 0,
                    "last_message": {
                        "sender_did": "did:b2a:zStranger",
                        "payload": payload,
                        "created_at": "2026-02-20T00:00:00Z",
                    },
                    "updated_at": "2026-02-20T00:00:00Z",
                    "ended_at": None,
                },
            ],
        }

        result = runner.invoke(cli, ["requests"])
        assert result.exit_code == 0
        assert "1 pending request" in result.output
        assert "did:b2a:zStranger" in result.output
        assert "Can we chat?" in result.output
        assert "b2a chat --to" in result.output
        mock_list.assert_called_once_with("tok", limit=20, filter="requests")

    @patch("b2alpha.api.list_conversations")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_requests_none(self, _tok, mock_list, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_list.return_value = {"conversations": []}

        result = runner.invoke(cli, ["requests"])
        assert result.exit_code == 0
        assert "No pending requests" in result.output


# ---------------------------------------------------------------------------
# serve
# ---------------------------------------------------------------------------

class TestServe:
    @patch("b2alpha.runner.AgentRunner.run_forever")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_serve_starts_runner(self, _tok, mock_run_forever, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg

        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zServe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        result = runner.invoke(cli, ["serve", "--handler-cmd", "python handler.py"])

        assert result.exit_code == 0
        assert "Serving auto-replies for did:b2a:zServe" in result.output
        mock_run_forever.assert_called_once()


# ---------------------------------------------------------------------------
# register (mocked)
# ---------------------------------------------------------------------------

class TestRegister:
    @patch("b2alpha.api.register_agent")
    @patch("b2alpha.crypto.generate_keypair")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_register_success(self, _tok, mock_keygen, mock_reg, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        monkeypatch.setattr(cfg, "PROFILE_FILE", tmp_path / "profile.json")
        monkeypatch.setattr(cfg, "B2A_DIR", tmp_path)
        monkeypatch.setattr(cfg, "KEYS_DIR", tmp_path / "keys")

        mock_keygen.return_value = ("did:b2a:zNew", "pubkey123", str(tmp_path / "keys" / "default.key"))
        mock_reg.return_value = {"ok": True, "agent": {"did": "did:b2a:zNew"}}

        result = runner.invoke(cli, [
            "register", "--name", "TestBot", "--type", "business",
            "--capabilities", "chat,search", "--phonebook", "--phone", "+14155550123",
        ])
        assert result.exit_code == 0
        assert "Agent registered" in result.output
        assert "did:b2a:zNew" in result.output
        assert "Phone: +14155550123" in result.output
        payload = mock_reg.call_args.args[1]
        assert payload["phone_e164"] == "+14155550123"

    @patch("b2alpha.auth.get_valid_token", side_effect=RuntimeError("Not logged in"))
    def test_register_not_logged_in(self, _tok, runner):
        result = runner.invoke(cli, ["register", "--name", "X", "--type", "user"])
        assert result.exit_code == 1

    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_register_rejects_bad_phone(self, _tok, runner):
        result = runner.invoke(
            cli,
            ["register", "--name", "X", "--type", "business", "--phone", "4155550123"],
        )
        assert result.exit_code == 1
        assert "Phone number must be E.164 format" in result.output


# ---------------------------------------------------------------------------
# update (mocked)
# ---------------------------------------------------------------------------

class TestUpdate:
    @patch("b2alpha.api.update_agent")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_update_name(self, _tok, mock_update, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_update.return_value = {"ok": True, "did": "did:b2a:zMe"}

        result = runner.invoke(cli, ["update", "--name", "NewName"])
        assert result.exit_code == 0
        assert "updated" in result.output

    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_update_nothing(self, _tok, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        result = runner.invoke(cli, ["update"])
        assert result.exit_code == 1
        assert "Nothing to update" in result.output

    @patch("b2alpha.api.update_agent")
    @patch("b2alpha.auth.get_valid_token", return_value="tok")
    def test_update_phone(self, _tok, mock_update, runner, tmp_path, monkeypatch):
        import b2alpha.config as cfg
        pf = tmp_path / "profile.json"
        pf.write_text(json.dumps({"did": "did:b2a:zMe"}))
        monkeypatch.setattr(cfg, "PROFILE_FILE", pf)

        mock_update.return_value = {"ok": True, "did": "did:b2a:zMe"}
        result = runner.invoke(cli, ["update", "--phone", "+14155550123"])
        assert result.exit_code == 0
        payload = mock_update.call_args.args[1]
        assert payload["phone_e164"] == "+14155550123"


# ---------------------------------------------------------------------------
# proxy
# ---------------------------------------------------------------------------

class TestProxy:
    @patch("b2alpha.bridge.adapter.httpx.request")
    def test_proxy_dispatches_with_config(self, mock_request, runner, tmp_path):
        resp = MagicMock()
        resp.status_code = 201
        resp.json.return_value = {"id": "ord_1", "status": "created"}
        mock_request.return_value = resp

        config_path = tmp_path / "bridge-config.json"
        config_path.write_text(
            json.dumps(
                {
                    "base_url": "https://api.acme.example/v1",
                    "headers": {"User-Agent": "b2a-test"},
                }
            )
        )

        result = runner.invoke(
            cli,
            [
                "proxy",
                "--adapter",
                "acme-orders",
                "--config",
                str(config_path),
                "--intent",
                "order.create",
                "--params",
                '{"sku":"SKU-1","quantity":2}',
                "--metadata",
                '{"acme_api_key":"key123"}',
                "--header",
                "X-Trace=trace-7",
            ],
        )

        assert result.exit_code == 0
        assert '"adapter": "acme-orders"' in result.output
        assert '"status_code": 201' in result.output

        kwargs = mock_request.call_args.kwargs
        assert mock_request.call_args.args[0] == "POST"
        assert mock_request.call_args.args[1] == "https://api.acme.example/v1/orders"
        assert kwargs["headers"]["User-Agent"] == "b2a-test"
        assert kwargs["headers"]["X-Trace"] == "trace-7"
        assert kwargs["headers"]["X-API-Key"] == "key123"

    def test_proxy_requires_base_url(self, runner):
        result = runner.invoke(
            cli,
            [
                "proxy",
                "--adapter",
                "acme-orders",
                "--intent",
                "order.status",
                "--params",
                '{"order_id":"ord_1"}',
            ],
        )
        assert result.exit_code == 1
        assert "Missing base URL" in result.output

    def test_proxy_rejects_invalid_json(self, runner):
        result = runner.invoke(
            cli,
            [
                "proxy",
                "--adapter",
                "acme-orders",
                "--base-url",
                "https://api.acme.example/v1",
                "--intent",
                "order.status",
                "--params",
                "{invalid",
            ],
        )
        assert result.exit_code == 1
        assert "Invalid JSON for params" in result.output

    def test_proxy_reports_unknown_intent(self, runner):
        result = runner.invoke(
            cli,
            [
                "proxy",
                "--adapter",
                "acme-orders",
                "--base-url",
                "https://api.acme.example/v1",
                "--intent",
                "menu.list",
            ],
        )
        assert result.exit_code == 1
        assert "no adapter registered for intent" in result.output


# ---------------------------------------------------------------------------
# Removed commands should not exist
# ---------------------------------------------------------------------------

class TestRemovedCommands:
    """Commands that were removed should no longer be registered."""

    @pytest.mark.parametrize("cmd", ["did", "auth-status", "listen", "sync", "lookup", "conversations"])
    def test_removed_command(self, runner, cmd):
        result = runner.invoke(cli, [cmd])
        assert result.exit_code != 0
        assert "No such command" in result.output or "Error" in result.output
