import httpx
import pytest

from orchagent import storage
from orchagent.storage import StorageError, StorageQuotaError


def _response(status_code: int, payload: dict) -> httpx.Response:
    return httpx.Response(
        status_code=status_code,
        json=payload,
        request=httpx.Request("GET", "http://test/storage"),
    )


def test_get_returns_value_only(monkeypatch):
    def fake_request(method, path, *, json_body=None, headers=None):
        assert method == "GET"
        assert path == "/signals/today"
        return _response(
            200,
            {
                "namespace": "signals",
                "key": "today",
                "value": {"pending": []},
                "version": 3,
            },
        )

    monkeypatch.setattr(storage, "_request", fake_request)
    assert storage.get("signals", "today") == {"pending": []}


def test_get_returns_none_for_404(monkeypatch):
    monkeypatch.setattr(
        storage,
        "_request",
        lambda method, path, *, json_body=None, headers=None: _response(
            404, {"error": {"code": "NOT_FOUND", "message": "missing"}}
        ),
    )
    assert storage.get("signals", "missing") is None


def test_update_uses_cas_version(monkeypatch):
    calls = {"get": 0, "put": 0}

    def fake_request(method, path, *, json_body=None, headers=None):
        if method == "GET":
            calls["get"] += 1
            return _response(
                200,
                {
                    "namespace": "signals",
                    "key": "today",
                    "value": {"pending": []},
                    "version": 7,
                },
            )
        if method == "PUT":
            calls["put"] += 1
            assert headers == {"If-Match": "7"}
            assert json_body == {"pending": ["a"]}
            return _response(
                200,
                {
                    "namespace": "signals",
                    "key": "today",
                    "version": 8,
                    "size_bytes": 24,
                },
            )
        raise AssertionError(f"Unexpected method: {method}")

    monkeypatch.setattr(storage, "_request", fake_request)

    result = storage.update(
        "signals",
        "today",
        lambda current: {"pending": current["pending"] + ["a"]},
    )
    assert result["version"] == 8
    assert calls == {"get": 1, "put": 1}


def test_update_passes_none_when_document_missing(monkeypatch):
    seen_values = []

    def fake_request(method, path, *, json_body=None, headers=None):
        if method == "GET":
            return _response(404, {"error": {"code": "NOT_FOUND", "message": "missing"}})
        if method == "PUT":
            assert headers == {}
            assert json_body == {"seed": True}
            return _response(201, {"version": 1})
        raise AssertionError(f"Unexpected method: {method}")

    monkeypatch.setattr(storage, "_request", fake_request)

    storage.update(
        "signals",
        "new",
        lambda current: seen_values.append(current) or {"seed": True},
    )
    assert seen_values == [None]


def test_update_retries_on_conflict(monkeypatch):
    sequence = []

    def fake_request(method, path, *, json_body=None, headers=None):
        if method == "GET":
            sequence.append(("get", None))
            version = 1 if len(sequence) == 1 else 2
            return _response(200, {"value": {"count": version - 1}, "version": version})
        if method == "PUT":
            sequence.append(("put", headers.get("If-Match") if headers else None))
            if headers == {"If-Match": "1"}:
                return _response(409, {"error": {"code": "CONFLICT", "message": "race"}})
            return _response(200, {"version": 3})
        raise AssertionError(f"Unexpected method: {method}")

    monkeypatch.setattr(storage, "_request", fake_request)
    monkeypatch.setattr(storage.time, "sleep", lambda _: None)

    result = storage.update("signals", "counter", lambda current: {"count": current["count"] + 1})
    assert result["version"] == 3
    assert sequence == [("get", None), ("put", "1"), ("get", None), ("put", "2")]


def test_maps_storage_quota_429_to_quota_error(monkeypatch):
    monkeypatch.setattr(
        storage,
        "_request",
        lambda method, path, *, json_body=None, headers=None: _response(
            429,
            {"error": {"code": "STORAGE_NAMESPACE_LIMIT_EXCEEDED", "message": "quota"}},
        ),
    )
    with pytest.raises(StorageQuotaError):
        storage.set("signals", "k", {"v": 1})


def test_keeps_non_quota_429_as_generic_error(monkeypatch):
    monkeypatch.setattr(
        storage,
        "_request",
        lambda method, path, *, json_body=None, headers=None: _response(
            429,
            {"error": {"code": "RATE_LIMITED", "message": "slow down"}},
        ),
    )
    with pytest.raises(StorageError):
        storage.set("signals", "k", {"v": 1})
