"""Tests for AgentDrop.register() method - zero-knowledge guarantees.

TDD: Tests written BEFORE integration testing. Mocks network + disk.

Contract:
  1. register() MUST generate the keypair locally (client-side).
  2. register() MUST send the public_key to the server, never the private_key.
  3. The response MUST NEVER be expected to contain a private_key - the
     server doesn't know it.
  4. On success: agent_id/agent_uuid are set, config is saved, instructions
     are emitted.
  5. On failure: keys are cleared so a retry starts clean.
"""

import base64
import json
from unittest.mock import patch, MagicMock

import pytest

from agentdrop.client import AgentDrop, AgentDropError
from agentdrop.crypto import KeyPair


def _make_client(tmp_path, **overrides) -> AgentDrop:
    defaults = dict(
        api_key="agd_test_key",
        shield=False,
        config_dir=str(tmp_path / ".agentdrop"),
    )
    defaults.update(overrides)
    with patch.object(AgentDrop, "_load_config"):
        return AgentDrop(**defaults)


def _mock_response(status_code: int, body: dict) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = body
    resp.text = json.dumps(body)
    return resp


class TestRegisterZeroKnowledge:
    """Zero-knowledge contract: server never learns the private key."""

    @patch("agentdrop.client.requests.post")
    def test_generates_keypair_locally_before_posting(self, mock_post, tmp_path):
        """The client MUST have keys populated before the HTTP call happens."""
        mock_post.return_value = _mock_response(201, {
            "agent_id": "my-agent",
            "agent_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
            "connection_status": "connected",
            "key_version": 1,
        })

        client = _make_client(tmp_path)
        assert client._keys is None

        with patch.object(AgentDrop, "_save_config"), patch.object(
            AgentDrop, "_generate_instructions"
        ):
            client.register("my-agent")

        assert client._keys is not None
        assert isinstance(client._keys, KeyPair)
        assert client._keys.public_key_b64
        assert client._keys.private_key_b64

    @patch("agentdrop.client.requests.post")
    def test_sends_public_key_but_never_private_key(self, mock_post, tmp_path):
        """The HTTP body MUST include public_key and MUST NOT include private_key."""
        mock_post.return_value = _mock_response(201, {
            "agent_id": "zk-agent",
            "agent_uuid": "uuid-xyz",
            "connection_status": "connected",
        })

        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config"), patch.object(
            AgentDrop, "_generate_instructions"
        ):
            client.register("zk-agent")

        assert mock_post.called
        kwargs = mock_post.call_args.kwargs
        body = kwargs.get("json") or {}
        assert "public_key" in body
        assert body["public_key"] == client._keys.public_key_b64
        assert body["public_key_algorithm"] == "X25519"
        # The private key must NEVER touch the wire.
        assert "private_key" not in body
        # And not hidden anywhere else either.
        serialized = json.dumps(body)
        assert "private_key" not in serialized.lower()

    @patch("agentdrop.client.requests.post")
    def test_posts_to_register_endpoint_with_bearer_auth(self, mock_post, tmp_path):
        """register() hits /v1/agents/register with the api key as Bearer token."""
        mock_post.return_value = _mock_response(201, {
            "agent_id": "a1",
            "agent_uuid": "u1",
            "connection_status": "connected",
        })
        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config"), patch.object(
            AgentDrop, "_generate_instructions"
        ):
            client.register("a1")

        url = mock_post.call_args.args[0]
        assert url.endswith("/v1/agents/register")
        headers = mock_post.call_args.kwargs["headers"]
        assert headers["Authorization"] == "Bearer agd_test_key"
        assert headers["Content-Type"] == "application/json"


class TestRegisterSuccess:
    """Successful registration flow."""

    @patch("agentdrop.client.requests.post")
    def test_sets_agent_id_and_uuid_from_response(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(201, {
            "agent_id": "returned-id",
            "agent_uuid": "returned-uuid",
            "connection_status": "connected",
        })
        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config"), patch.object(
            AgentDrop, "_generate_instructions"
        ):
            result = client.register("desired-id")

        assert client.agent_id == "returned-id"
        assert client.agent_uuid == "returned-uuid"
        assert result["connection_status"] == "connected"

    @patch("agentdrop.client.requests.post")
    def test_saves_config_and_generates_instructions(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(201, {
            "agent_id": "a1",
            "agent_uuid": "u1",
            "connection_status": "connected",
        })
        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config") as save, patch.object(
            AgentDrop, "_generate_instructions"
        ) as gen:
            client.register("a1")

        save.assert_called_once()
        gen.assert_called_once()

    @patch("agentdrop.client.requests.post")
    def test_forwards_optional_fields(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(201, {
            "agent_id": "a1",
            "agent_uuid": "u1",
            "connection_status": "connected",
        })
        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config"), patch.object(
            AgentDrop, "_generate_instructions"
        ):
            client.register(
                "a1",
                name="My Agent",
                description="Does things",
                webhook_url="https://example.com/hook",
                metadata={"kind": "test"},
            )

        body = mock_post.call_args.kwargs["json"]
        assert body["name"] == "My Agent"
        assert body["description"] == "Does things"
        assert body["webhook_url"] == "https://example.com/hook"
        assert body["metadata"] == {"kind": "test"}


class TestRegisterFailure:
    """Error handling."""

    @patch("agentdrop.client.requests.post")
    def test_raises_agentdrop_error_on_4xx(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(400, {
            "error": {"code": "INVALID_PUBLIC_KEY", "message": "bad key"},
        })
        client = _make_client(tmp_path)

        with pytest.raises(AgentDropError) as exc:
            client.register("x")
        assert exc.value.code == "INVALID_PUBLIC_KEY"
        assert exc.value.status == 400

    @patch("agentdrop.client.requests.post")
    def test_clears_keys_on_failure_so_retry_is_clean(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(409, {
            "error": {"code": "AGENT_ID_TAKEN", "message": "already exists"},
        })
        client = _make_client(tmp_path)

        with pytest.raises(AgentDropError):
            client.register("dup")
        assert client._keys is None

    @patch("agentdrop.client.requests.post")
    def test_does_not_save_config_on_failure(self, mock_post, tmp_path):
        mock_post.return_value = _mock_response(400, {
            "error": {"code": "VALIDATION_ERROR", "message": "nope"},
        })
        client = _make_client(tmp_path)
        with patch.object(AgentDrop, "_save_config") as save, patch.object(
            AgentDrop, "_generate_instructions"
        ) as gen:
            with pytest.raises(AgentDropError):
                client.register("x")
        save.assert_not_called()
        gen.assert_not_called()
