# SPDX-License-Identifier: Apache-2.0
# Standard
from unittest.mock import MagicMock
import json

# Third Party
from fastapi.testclient import TestClient
import pytest

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


class TestLookupAPI:
    """Test suite for the /lookup/* API endpoints."""

    @pytest.fixture
    def mock_scheduler_manager(self):
        """Create a mock LMCacheManager for scheduler role."""
        manager = MagicMock()
        manager.role = "scheduler"
        manager.get_lookup_info.return_value = {
            "client": "HitLimitLookupClient(LMCacheBypassLookupClient)",
            "server": "None",
            "role": "scheduler",
        }
        manager.close_lookup_client.return_value = {
            "old": "HitLimitLookupClient(LMCacheBypassLookupClient)"
        }
        manager.create_lookup_client.return_value = {"new": "LMCacheLookupClient"}
        manager.recreate_lookup_client.return_value = {
            "old": "HitLimitLookupClient(LMCacheBypassLookupClient)",
            "new": "LMCacheLookupClient",
        }
        return manager

    @pytest.fixture
    def mock_worker_manager(self):
        """Create a mock LMCacheManager for worker role."""
        manager = MagicMock()
        manager.role = "worker"
        manager.get_lookup_info.return_value = {
            "client": "None",
            "server": "LMCacheLookupServer",
            "role": "worker",
        }
        manager.close_lookup_server.return_value = {"old": "LMCacheLookupServer"}
        manager.create_lookup_server.return_value = {"new": "LMCacheAsyncLookupServer"}
        manager.recreate_lookup_server.return_value = {
            "old": "LMCacheLookupServer",
            "new": "LMCacheAsyncLookupServer",
        }
        return manager

    @pytest.fixture
    def scheduler_client(self, mock_scheduler_manager):
        """Create a test client with mocked scheduler manager."""
        app.state.lmcache_adapter = mock_scheduler_manager
        return TestClient(app)

    @pytest.fixture
    def worker_client(self, mock_worker_manager):
        """Create a test client with mocked worker manager."""
        app.state.lmcache_adapter = mock_worker_manager
        return TestClient(app)

    # ==================== GET /lookup/info Tests ====================

    def test_get_lookup_info_scheduler(self, scheduler_client, mock_scheduler_manager):
        """Test get lookup info for scheduler role."""
        response = scheduler_client.get("/lookup/info")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert "LMCacheBypassLookupClient" in data["client"]
        assert data["server"] == "None"
        assert data["role"] == "scheduler"
        mock_scheduler_manager.get_lookup_info.assert_called_once()

    def test_get_lookup_info_worker(self, worker_client, mock_worker_manager):
        """Test get lookup info for worker role."""
        response = worker_client.get("/lookup/info")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["client"] == "None"
        assert data["server"] == "LMCacheLookupServer"
        assert data["role"] == "worker"
        mock_worker_manager.get_lookup_info.assert_called_once()

    def test_get_lookup_info_not_supported(self):
        """Test get lookup info when manager doesn't support it."""

        class SimpleAdapter:
            pass

        app.state.lmcache_adapter = SimpleAdapter()
        client = TestClient(app)

        response = client.get("/lookup/info")

        assert response.status_code == 503
        data = json.loads(response.text)
        assert "error" in data

    # ==================== POST /lookup/close Tests ====================

    def test_close_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
        """Test close lookup client for scheduler."""
        response = scheduler_client.post("/lookup/close")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert "LMCacheBypassLookupClient" in data["old"]
        assert data["role"] == "scheduler"
        mock_scheduler_manager.close_lookup_client.assert_called_once()

    def test_close_lookup_worker(self, worker_client, mock_worker_manager):
        """Test close lookup server for worker."""
        response = worker_client.post("/lookup/close")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["old"] == "LMCacheLookupServer"
        assert data["role"] == "worker"
        mock_worker_manager.close_lookup_server.assert_called_once()

    # ==================== POST /lookup/create Tests ====================

    def test_create_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
        """Test create lookup client for scheduler."""
        response = scheduler_client.post("/lookup/create")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["new"] == "LMCacheLookupClient"
        assert data["role"] == "scheduler"
        mock_scheduler_manager.create_lookup_client.assert_called_once_with(
            dryrun=False
        )

    def test_create_lookup_worker(self, worker_client, mock_worker_manager):
        """Test create lookup server for worker."""
        response = worker_client.post("/lookup/create")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["new"] == "LMCacheAsyncLookupServer"
        assert data["role"] == "worker"
        mock_worker_manager.create_lookup_server.assert_called_once_with(dryrun=False)

    def test_create_lookup_dryrun_scheduler(
        self, scheduler_client, mock_scheduler_manager
    ):
        """Test dryrun create lookup client for scheduler."""
        mock_scheduler_manager.create_lookup_client.return_value = {
            "new": "LMCacheLookupClient",
            "dryrun": True,
        }

        response = scheduler_client.post("/lookup/create?dryrun=true")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["new"] == "LMCacheLookupClient"
        assert data["dryrun"] is True
        mock_scheduler_manager.create_lookup_client.assert_called_once_with(dryrun=True)

    def test_create_lookup_dryrun_worker(self, worker_client, mock_worker_manager):
        """Test dryrun create lookup server for worker."""
        mock_worker_manager.create_lookup_server.return_value = {
            "new": "LMCacheAsyncLookupServer",
            "dryrun": True,
        }

        response = worker_client.post("/lookup/create?dryrun=true")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["new"] == "LMCacheAsyncLookupServer"
        assert data["dryrun"] is True
        mock_worker_manager.create_lookup_server.assert_called_once_with(dryrun=True)

    def test_create_lookup_error(self, scheduler_client, mock_scheduler_manager):
        """Test create lookup with error."""
        mock_scheduler_manager.create_lookup_client.return_value = {
            "error": "metadata not available"
        }

        response = scheduler_client.post("/lookup/create")

        assert response.status_code == 400
        data = json.loads(response.text)
        assert data["error"] == "metadata not available"

    # ==================== POST /lookup/recreate Tests ====================

    def test_recreate_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
        """Test recreate lookup client for scheduler."""
        response = scheduler_client.post("/lookup/recreate")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert "LMCacheBypassLookupClient" in data["old"]
        assert data["new"] == "LMCacheLookupClient"
        assert data["role"] == "scheduler"
        mock_scheduler_manager.recreate_lookup_client.assert_called_once()

    def test_recreate_lookup_worker(self, worker_client, mock_worker_manager):
        """Test recreate lookup server for worker."""
        response = worker_client.post("/lookup/recreate")

        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["old"] == "LMCacheLookupServer"
        assert data["new"] == "LMCacheAsyncLookupServer"
        assert data["role"] == "worker"
        mock_worker_manager.recreate_lookup_server.assert_called_once()

    def test_recreate_lookup_error(self, scheduler_client, mock_scheduler_manager):
        """Test recreate lookup with error."""
        mock_scheduler_manager.recreate_lookup_client.return_value = {
            "error": "only supported for scheduler role"
        }

        response = scheduler_client.post("/lookup/recreate")

        assert response.status_code == 400
        data = json.loads(response.text)
        assert "error" in data

    def test_recreate_lookup_unknown_role(self):
        """Test recreation with unknown role."""
        manager = MagicMock()
        manager.role = "unknown"
        app.state.lmcache_adapter = manager
        client = TestClient(app)

        response = client.post("/lookup/recreate")

        assert response.status_code == 400
        data = json.loads(response.text)
        assert data["error"] == "Unknown role"

    def test_recreate_lookup_not_supported(self):
        """Test recreation when manager doesn't support it."""

        class SimpleAdapter:
            role = "scheduler"

        app.state.lmcache_adapter = SimpleAdapter()
        client = TestClient(app)

        response = client.post("/lookup/recreate")

        assert response.status_code == 503
        data = json.loads(response.text)
        assert "error" in data

    # ==================== Integration Flow Tests ====================

    def test_full_flow_worker_then_scheduler(
        self, mock_worker_manager, mock_scheduler_manager
    ):
        """
        Test the complete flow:
        1. POST /lookup/recreate on worker (recreate server)
        2. POST /lookup/recreate on scheduler (recreate client)

        Note: Config should be updated via /conf API before each step.
        """
        # Step 1: Worker side - recreate server
        app.state.lmcache_adapter = mock_worker_manager
        worker_client = TestClient(app)

        response = worker_client.post("/lookup/recreate")
        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["old"] == "LMCacheLookupServer"
        assert data["new"] == "LMCacheAsyncLookupServer"
        mock_worker_manager.recreate_lookup_server.assert_called_once()

        # Step 2: Scheduler side - recreate client
        app.state.lmcache_adapter = mock_scheduler_manager
        scheduler_client = TestClient(app)

        response = scheduler_client.post("/lookup/recreate")
        assert response.status_code == 200
        data = json.loads(response.text)
        assert "LMCacheBypassLookupClient" in data["old"]
        assert data["new"] == "LMCacheLookupClient"
        mock_scheduler_manager.recreate_lookup_client.assert_called_once()

    def test_step_by_step_flow(self, mock_scheduler_manager):
        """
        Test step-by-step flow: dryrun -> close -> create.
        """
        app.state.lmcache_adapter = mock_scheduler_manager
        client = TestClient(app)

        # Step 1: Dryrun - check what would be created
        mock_scheduler_manager.create_lookup_client.return_value = {
            "new": "LMCacheLookupClient",
            "dryrun": True,
        }
        response = client.post("/lookup/create?dryrun=true")
        assert response.status_code == 200
        data = json.loads(response.text)
        assert data["dryrun"] is True

        # Step 2: Close current client
        response = client.post("/lookup/close")
        assert response.status_code == 200
        mock_scheduler_manager.close_lookup_client.assert_called_once()

        # Step 3: Create new client
        mock_scheduler_manager.create_lookup_client.return_value = {
            "new": "LMCacheLookupClient"
        }
        response = client.post("/lookup/create")
        assert response.status_code == 200
