"""Unit tests for MemoryClientV2 — 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 (
    MemoryClientV2,
    NotFoundError,
    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() -> MemoryClientV2:
    return MemoryClientV2(
        base_url="http://localhost:3000",
        api_key="test-key",
        workspace_id="ws-1",
        actor_id="agent-1",
    )


class TestInit:
    def test_strips_trailing_slash(self) -> None:
        c = MemoryClientV2(base_url="http://host/", workspace_id="ws")
        assert c.base_url == "http://host"

    def test_defaults(self) -> None:
        c = MemoryClientV2(workspace_id="ws")
        assert c.actor_id == "sdk"


class TestV2Headers:
    def test_includes_workspace_and_actor(self, client: MemoryClientV2) -> None:
        headers = client._v2_headers()
        assert headers["x-workspace-id"] == "ws-1"
        assert headers["x-actor-id"] == "agent-1"


class TestIngest:
    @patch("urllib.request.urlopen")
    def test_sync(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        expected = {"workspace_id": "ws-1", "ingested": [{"artifact_id": "a1"}]}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.ingest([{"artifact_type": "document", "raw_payload": {"content": "hi"}}])

        assert result == expected
        req = mock_urlopen.call_args[0][0]
        assert "/v2/ingest" in req.full_url

    @patch("urllib.request.urlopen")
    def test_async(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        expected = {"workspace_id": "ws-1", "queued_jobs": [{"job_id": "j1"}]}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.ingest([{"artifact_type": "document", "raw_payload": {}}], async_index=True)

        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["async_index"] is True


class TestContextPack:
    @patch("urllib.request.urlopen")
    def test_basic(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        expected = {"pack_hash": "abc", "snippets": []}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.context_pack("What happened?")

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

    @patch("urllib.request.urlopen")
    def test_with_profile(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        mock_urlopen.return_value = _mock_response({"pack_hash": "x"})

        client.context_pack("q", profile="high_recall", max_tokens=8192)

        body = json.loads(mock_urlopen.call_args[0][0].data)
        assert body["profile"] == "high_recall"
        assert body["max_tokens"] == 8192


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

        result = client.feedback("pin", artifact_id="a1")
        assert result == expected


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

        result = client.list_receipts(limit=10)

        assert result == expected
        req = mock_urlopen.call_args[0][0]
        assert "/v2/receipts/ws-1" in req.full_url

    @patch("urllib.request.urlopen")
    def test_get(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        expected = {"pack_hash": "abc"}
        mock_urlopen.return_value = _mock_response(expected)

        result = client.get_receipt("abc")
        assert "/v2/receipts/ws-1/abc" in mock_urlopen.call_args[0][0].full_url


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

        result = client.artifact_status("a1")
        assert result["indexed"] is True

    @patch("urllib.request.urlopen")
    def test_job(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        mock_urlopen.return_value = _mock_response({"job_id": "j1", "state": "done"})

        result = client.job_status("j1")
        assert result["state"] == "done"


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

        result = client.ask("What do I do?")
        assert result == expected


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

        result = client.delete_workspace()
        assert result["deleted_artifacts"] == 3
        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: MemoryClientV2) -> None:
        mock_urlopen.return_value = _mock_response({"status": "ok"})
        assert client.health()["status"] == "ok"

    @patch("urllib.request.urlopen")
    def test_ready(self, mock_urlopen: MagicMock, client: MemoryClientV2) -> None:
        mock_urlopen.return_value = _mock_response({"status": "ready"})
        assert client.ready()["status"] == "ready"


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

    def test_429(self, client: MemoryClientV2) -> None:
        with patch("urllib.request.urlopen", side_effect=_mock_http_error(429, {"detail": "Too many"})):
            with pytest.raises(RateLimitError):
                client.ingest([])
