# SPDX-License-Identifier: Apache-2.0
"""
Test cases for the /backends/* API endpoints.

Tests cover:
- GET /backends  — list active backends
- DELETE /backends/{name} — close and remove a backend
- POST /backends — create new backends from current config
- POST /backends/{name}/recreate — atomic close + create
- End-to-end flow: close → conf update → create → verify
"""

# Standard
from collections import OrderedDict
from unittest.mock import MagicMock

# Third Party
from fastapi.testclient import TestClient
import pytest

# First Party
from lmcache.v1.internal_api_server.api_server import app

# ------------------------------------------------------------------ #
#  Fixtures
# ------------------------------------------------------------------ #


class FakeBackend:
    """Minimal fake backend for testing."""

    def __init__(self, name: str = "FakeBackend"):
        self._name = name
        self.closed = False

    def close(self):
        self.closed = True

    def __str__(self):
        return self._name


@pytest.fixture
def mock_storage_manager():
    """Create a mock StorageManager with two fake backends."""
    sm = MagicMock()
    fake_cpu = FakeBackend("LocalCPUBackend")
    fake_remote = FakeBackend("RemoteBackend")
    backends = OrderedDict(
        [
            ("LocalCPUBackend", fake_cpu),
            ("RemoteBackend", fake_remote),
        ]
    )
    sm.storage_backends = backends

    # Wire real-ish list_backends
    sm.list_backends.side_effect = lambda: {
        n: type(b).__name__ for n, b in sm.storage_backends.items()
    }
    return sm


@pytest.fixture
def mock_engine(mock_storage_manager):
    engine = MagicMock()
    engine.storage_manager = mock_storage_manager
    return engine


@pytest.fixture
def client_with_engine(mock_engine):
    adapter = MagicMock()
    adapter.lmcache_engine = mock_engine
    app.state.lmcache_adapter = adapter
    return TestClient(app)


@pytest.fixture
def client_without_engine():
    adapter = MagicMock()
    adapter.lmcache_engine = None
    app.state.lmcache_adapter = adapter
    return TestClient(app)


# ------------------------------------------------------------------ #
#  GET /backends
# ------------------------------------------------------------------ #


class TestListBackends:
    def test_list_success(self, client_with_engine, mock_storage_manager):
        resp = client_with_engine.get("/backends")
        assert resp.status_code == 200
        data = resp.json()
        assert "LocalCPUBackend" in data
        assert "RemoteBackend" in data

    def test_list_no_engine(self, client_without_engine):
        resp = client_without_engine.get("/backends")
        assert resp.status_code == 503

    def test_list_exception(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.list_backends.side_effect = RuntimeError("boom")
        resp = client_with_engine.get("/backends")
        assert resp.status_code == 500
        data = resp.json()
        assert "boom" in data["message"]


# ------------------------------------------------------------------ #
#  DELETE /backends/{name}
# ------------------------------------------------------------------ #


class TestCloseBackend:
    def test_close_success(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.close_backend.return_value = True
        resp = client_with_engine.delete("/backends/RemoteBackend")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "success"
        mock_storage_manager.close_backend.assert_called_once_with("RemoteBackend")

    def test_close_not_found(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.close_backend.return_value = False
        resp = client_with_engine.delete("/backends/NonExistent")
        assert resp.status_code == 404
        data = resp.json()
        assert data["status"] == "not_found"

    def test_close_no_engine(self, client_without_engine):
        resp = client_without_engine.delete("/backends/RemoteBackend")
        assert resp.status_code == 503

    def test_close_exception(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.close_backend.side_effect = RuntimeError("close error")
        resp = client_with_engine.delete("/backends/RemoteBackend")
        assert resp.status_code == 500
        data = resp.json()
        assert "close error" in data["message"]


# ------------------------------------------------------------------ #
#  POST /backends
# ------------------------------------------------------------------ #


class TestCreateBackends:
    def test_create_success(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.create_backends.return_value = {"GdsBackend": "GdsBackend"}
        resp = client_with_engine.post("/backends")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "success"
        assert "GdsBackend" in data["created"]

    def test_create_no_engine(self, client_without_engine):
        resp = client_without_engine.post("/backends")
        assert resp.status_code == 503

    def test_create_exception(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.create_backends.side_effect = RuntimeError("create error")
        resp = client_with_engine.post("/backends")
        assert resp.status_code == 500
        data = resp.json()
        assert "create error" in data["message"]


# ------------------------------------------------------------------ #
#  POST /backends/{name}/recreate
# ------------------------------------------------------------------ #


class TestRecreateBackend:
    def test_recreate_success(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.recreate_backend.return_value = {
            "RemoteBackend": "RemoteBackend"
        }
        resp = client_with_engine.post("/backends/RemoteBackend/recreate")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "success"
        assert "RemoteBackend" in data["recreated"]
        mock_storage_manager.recreate_backend.assert_called_once_with("RemoteBackend")

    def test_recreate_not_found(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.recreate_backend.side_effect = KeyError(
            "Backend NonExistent not found"
        )
        resp = client_with_engine.post("/backends/NonExistent/recreate")
        assert resp.status_code == 404
        data = resp.json()
        assert data["status"] == "not_found"

    def test_recreate_no_engine(self, client_without_engine):
        resp = client_without_engine.post("/backends/RemoteBackend/recreate")
        assert resp.status_code == 503

    def test_recreate_exception(self, client_with_engine, mock_storage_manager):
        mock_storage_manager.recreate_backend.side_effect = RuntimeError(
            "recreate error"
        )
        resp = client_with_engine.post("/backends/RemoteBackend/recreate")
        assert resp.status_code == 500
        data = resp.json()
        assert "recreate error" in data["message"]


# ------------------------------------------------------------------ #
#  End-to-end flow: close → conf → create
# ------------------------------------------------------------------ #


class TestBackendSwitchFlow:
    """Simulate the full backend-switch workflow via API."""

    def test_close_update_conf_create_flow(
        self, client_with_engine, mock_storage_manager
    ):
        """
        1. DELETE /backends/RemoteBackend
        2. POST /conf to update remote_url
        3. POST /backends to create new RemoteBackend
        4. GET /backends to verify
        """
        # Step 1: close RemoteBackend
        mock_storage_manager.close_backend.return_value = True
        resp = client_with_engine.delete("/backends/RemoteBackend")
        assert resp.status_code == 200

        # Step 2: update config via /conf (uses conf_api)
        # We patch the config object on the adapter
        mock_config = MagicMock()
        client_with_engine.app.state.lmcache_adapter.config = mock_config

        # Step 3: create new backends
        mock_storage_manager.create_backends.return_value = {
            "RemoteBackend": "RemoteBackend"
        }
        resp = client_with_engine.post("/backends")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "success"
        assert "RemoteBackend" in data["created"]

        # Step 4: list backends
        resp = client_with_engine.get("/backends")
        assert resp.status_code == 200
