"""Unit tests for MemoryClient (v1) — all HTTP mocked."""

from __future__ import annotations

import io
import json
import urllib.error
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from nined.memory import (
    AuthorizationError,
    MemoryAPIError,
    MemoryClient,
    NotFoundError,
    PolicyDeniedError,
    RateLimitError,
)


def _mock_response(body: dict[str, Any]) -> MagicMock:
    resp = MagicMock()
    resp.read.return_value = json.dumps(body).encode("utf-8")
    resp.__enter__ = lambda s: s
    resp.__exit__ = MagicMock(return_value=False)
    return resp


def _mock_http_error(status: int, body: dict[str, Any] | str = "") -> urllib.error.HTTPError:
    body_bytes = json.dumps(body).encode("utf-8") if isinstance(body, dict) else body.encode("utf-8")
    return urllib.error.HTTPError(
        url="http://test", code=status, msg="error", hdrs=None, fp=io.BytesIO(body_bytes),  # type: ignore[arg-type]
    )


@pytest.fixture
def client() -> MemoryClient:
    return MemoryClient(base_url="http://localhost:3000", api_key="test-key")


class TestInit:
    def test_strips_trailing_slash(self) -> None:
        c = MemoryClient(base_url="http://host:3000/")
        assert c.base_url == "http://host:3000"

    def test_defaults(self) -> None:
        c = MemoryClient()
        assert c.base_url == "http://localhost:3000"
        assert c.timeout == 30


class TestIngest:
    @patch("urllib.request.urlopen")
    def test_ingest(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"artifact_id": "a1", "content_hash": "abc", "indexed": True}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.ingest(
            workspace_id="ws-1", actor_id="agent-1",
            artifact_type="document", raw_payload={"content": "hello"},
        )

        assert result == expected
        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["workspace_id"] == "ws-1"
        assert body["artifact_type"] == "document"
        assert mock_urlopen.call_args[0][0].full_url == "http://localhost:3000/v1/ingest"


class TestContextPack:
    @patch("urllib.request.urlopen")
    def test_basic(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"context_text": "...", "confidence": 0.9, "receipt_id": "r1"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.context_pack(query="What?", workspace_id="ws-1")

        assert result == expected
        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["query"] == "What?"
        assert body["max_tokens"] == 4096
        assert "mode" not in body

    @patch("urllib.request.urlopen")
    def test_with_options(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        mock_urlopen.return_value = _mock_response({"context_text": ""})

        client.context_pack(
            query="q", workspace_id="ws-1", mode="coverage",
            max_tokens=8192, diversity_decay=0.5,
        )

        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["mode"] == "coverage"
        assert body["max_tokens"] == 8192
        assert body["diversity_decay"] == 0.5


class TestFeedback:
    @patch("urllib.request.urlopen")
    def test_feedback(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"feedback_id": "f1", "applied": True, "action": "pin"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.feedback(
            workspace_id="ws-1", actor_id="a1", action="pin",
            artifact_id="art1", reason="important",
        )

        assert result == expected
        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["action"] == "pin"
        assert body["artifact_id"] == "art1"


class TestReceipts:
    @patch("urllib.request.urlopen")
    def test_list(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"receipts": [], "total_count": 0}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.list_receipts(workspace_id="ws-1", limit=10)

        assert result == expected
        req = mock_urlopen.call_args[0][0]
        assert "limit=10" in req.full_url

    @patch("urllib.request.urlopen")
    def test_get(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"id": "r1", "receipt": {}}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.get_receipt("r1")

        assert result == expected
        assert "/v1/receipts/r1" in mock_urlopen.call_args[0][0].full_url


class TestStatus:
    @patch("urllib.request.urlopen")
    def test_artifact_status(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"artifact_id": "a1", "indexed": True}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.artifact_status("a1")
        assert result == expected


class TestStudy:
    @patch("urllib.request.urlopen")
    def test_ask(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"answer": "Restart the pod.", "confidence": 0.9}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.ask(query="What do I do?", workspace_id="ws-1")

        assert result == expected
        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["max_tokens"] == 16000


class TestWorkspace:
    @patch("urllib.request.urlopen")
    def test_delete(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"workspace_id": "ws-1", "deleted_artifacts": 5}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.delete_workspace("ws-1")

        assert result == expected
        req = mock_urlopen.call_args[0][0]
        assert req.method == "DELETE"


class TestHealth:
    @patch("urllib.request.urlopen")
    def test_health(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        mock_urlopen.return_value = _mock_response({"status": "ok"})

        result = client.health()
        assert result["status"] == "ok"
        req = mock_urlopen.call_args[0][0]
        assert "X-API-Key" not in req.headers


class TestBilling:
    @patch("urllib.request.urlopen")
    def test_register(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"tenant_id": "t1", "api_key": "mr_abc", "message": "ok"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.register("user@example.com")
        assert result == expected

    @patch("urllib.request.urlopen")
    def test_billing_usage(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"tenant_id": "t1", "plan": "free"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.billing_usage()
        assert result == expected

    @patch("urllib.request.urlopen")
    def test_billing_plan(self, mock_urlopen: MagicMock, client: MemoryClient) -> None:
        expected = {"tenant_id": "t1", "plan_id": "free"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.billing_plan()
        assert result == expected


class TestErrorMapping:
    def test_401(self, client: MemoryClient) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(401, {"detail": "Invalid"})):
            with pytest.raises(AuthorizationError):
                client.health()

    def test_403_policy(self, client: MemoryClient) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(403, {"detail": "policy_denied"})):
            with pytest.raises(PolicyDeniedError):
                client.context_pack(query="q", workspace_id="ws")

    def test_404(self, client: MemoryClient) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(404, {"detail": "Not found"})):
            with pytest.raises(NotFoundError):
                client.get_receipt("nonexistent")

    def test_429(self, client: MemoryClient) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(429, {"detail": "Too many"})):
            with pytest.raises(RateLimitError):
                client.ingest(workspace_id="ws", actor_id="a", artifact_type="document")

    def test_500(self, client: MemoryClient) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(500, {"detail": "Internal"})):
            with pytest.raises(MemoryAPIError):
                client.ingest(workspace_id="ws", actor_id="a", artifact_type="document")
