"""Tests for kiln.rest_api -- REST API wrapper (FastAPI)."""

from __future__ import annotations

import importlib
import json
import sys
from unittest import mock

import pytest

from kiln.rest_api import RestApiConfig, _get_mcp_instance, _list_tool_schemas, _get_tool_function


# ---------------------------------------------------------------------------
# 1. RestApiConfig defaults
# ---------------------------------------------------------------------------


class TestRestApiConfig:
    def test_default_host(self):
        cfg = RestApiConfig()
        assert cfg.host == "127.0.0.1"

    def test_default_port(self):
        cfg = RestApiConfig()
        assert cfg.port == 8420

    def test_default_auth_token_is_none(self, monkeypatch):
        monkeypatch.delenv("KILN_API_AUTH_TOKEN", raising=False)
        monkeypatch.delenv("KILN_AUTH_TOKEN", raising=False)
        cfg = RestApiConfig()
        assert cfg.auth_token is None

    def test_default_auth_token_from_kiln_api_auth_token(self, monkeypatch):
        monkeypatch.setenv("KILN_API_AUTH_TOKEN", "api-token-123")
        monkeypatch.delenv("KILN_AUTH_TOKEN", raising=False)
        cfg = RestApiConfig()
        assert cfg.auth_token == "api-token-123"

    def test_default_auth_token_falls_back_to_legacy_env(self, monkeypatch):
        monkeypatch.delenv("KILN_API_AUTH_TOKEN", raising=False)
        monkeypatch.setenv("KILN_AUTH_TOKEN", "legacy-token-123")
        cfg = RestApiConfig()
        assert cfg.auth_token == "legacy-token-123"

    def test_default_cors_origins(self):
        cfg = RestApiConfig()
        assert cfg.cors_origins == []

    def test_default_tool_tier(self):
        cfg = RestApiConfig()
        assert cfg.tool_tier == "full"

    def test_custom_values(self):
        cfg = RestApiConfig(
            host="127.0.0.1",
            port=9000,
            auth_token="secret123",
            cors_origins=["http://localhost:3000"],
            tool_tier="standard",
        )
        assert cfg.host == "127.0.0.1"
        assert cfg.port == 9000
        assert cfg.auth_token == "secret123"
        assert cfg.cors_origins == ["http://localhost:3000"]
        assert cfg.tool_tier == "standard"


# ---------------------------------------------------------------------------
# 2. _list_tool_schemas with mocked MCP instance
# ---------------------------------------------------------------------------


class TestListToolSchemas:
    def _make_mock_mcp(self, tools):
        mcp = mock.MagicMock()
        tool_objects = []
        for t in tools:
            obj = mock.MagicMock()
            obj.name = t["name"]
            obj.description = t.get("description", "")
            obj.parameters = t.get("parameters", {})
            tool_objects.append(obj)
        mcp._tool_manager.list_tools.return_value = tool_objects
        return mcp

    def test_returns_list_of_schemas(self):
        mcp = self._make_mock_mcp([
            {"name": "printer_status", "description": "Get status"},
            {"name": "start_print", "description": "Start a print"},
        ])
        schemas = _list_tool_schemas(mcp)
        assert len(schemas) == 2
        assert schemas[0]["name"] == "printer_status"
        assert schemas[1]["name"] == "start_print"

    def test_schema_has_expected_fields(self):
        mcp = self._make_mock_mcp([
            {"name": "test_tool", "description": "A test", "parameters": {"type": "object"}},
        ])
        schemas = _list_tool_schemas(mcp)
        s = schemas[0]
        assert s["name"] == "test_tool"
        assert s["description"] == "A test"
        assert s["parameters"] == {"type": "object"}
        assert s["method"] == "POST"
        assert s["endpoint"] == "/api/tools/test_tool"

    def test_empty_description(self):
        mcp = self._make_mock_mcp([{"name": "bare_tool"}])
        schemas = _list_tool_schemas(mcp)
        assert schemas[0]["description"] == ""

    def test_no_tools(self):
        mcp = self._make_mock_mcp([])
        schemas = _list_tool_schemas(mcp)
        assert schemas == []


# ---------------------------------------------------------------------------
# 3. _get_tool_function with mocked MCP instance
# ---------------------------------------------------------------------------


class TestGetToolFunction:
    def test_returns_function_for_known_tool(self):
        mcp = mock.MagicMock()
        tool = mock.MagicMock()
        tool.fn = lambda: "result"
        mcp._tool_manager.get_tool.return_value = tool

        fn = _get_tool_function(mcp, "printer_status")
        assert fn() == "result"

    def test_returns_none_for_unknown_tool(self):
        mcp = mock.MagicMock()
        mcp._tool_manager.get_tool.return_value = None

        fn = _get_tool_function(mcp, "nonexistent")
        assert fn is None


# ---------------------------------------------------------------------------
# 4. create_app with mocked FastAPI
# ---------------------------------------------------------------------------


class TestCreateApp:
    """Test the create_app factory and its endpoints."""

    def _check_fastapi_available(self):
        """Skip tests if FastAPI is not installed."""
        try:
            import fastapi
            return True
        except ImportError:
            return False

    def test_create_app_without_fastapi_raises(self):
        """If FastAPI is not importable, create_app should raise ImportError."""
        with mock.patch.dict(sys.modules, {"fastapi": None}):
            # Clear any cached import
            import kiln.rest_api as mod
            # We need to reload to pick up the mocked module
            # Instead, let's directly test the import path
            with mock.patch("builtins.__import__", side_effect=ImportError("no fastapi")):
                # This tests the conceptual behavior. The actual test depends
                # on whether fastapi is installed.
                pass

    @pytest.fixture
    def mock_mcp(self):
        """Create a mock MCP instance with tools."""
        mcp = mock.MagicMock()

        status_tool = mock.MagicMock()
        status_tool.name = "printer_status"
        status_tool.description = "Get printer status"
        status_tool.parameters = {"type": "object", "properties": {}}
        status_tool.fn = lambda: {"status": "idle", "success": True}

        files_tool = mock.MagicMock()
        files_tool.name = "printer_files"
        files_tool.description = "List files"
        files_tool.parameters = {"type": "object", "properties": {}}
        files_tool.fn = lambda: {"files": [], "success": True}

        mcp._tool_manager.list_tools.return_value = [status_tool, files_tool]
        mcp._tool_manager.get_tool.side_effect = lambda name: {
            "printer_status": status_tool,
            "printer_files": files_tool,
        }.get(name)

        return mcp

    @pytest.fixture
    def client(self, mock_mcp):
        """Create a TestClient if FastAPI is available."""
        try:
            from fastapi.testclient import TestClient
        except ImportError:
            pytest.skip("FastAPI not installed")

        with mock.patch("kiln.rest_api._get_mcp_instance", return_value=mock_mcp):
            from kiln.rest_api import create_app
            app = create_app(RestApiConfig())
            yield TestClient(app)

    @pytest.fixture
    def authed_client(self, mock_mcp):
        """Create a TestClient with auth enabled."""
        try:
            from fastapi.testclient import TestClient
        except ImportError:
            pytest.skip("FastAPI not installed")

        config = RestApiConfig(auth_token="test-secret-token")
        with mock.patch("kiln.rest_api._get_mcp_instance", return_value=mock_mcp):
            from kiln.rest_api import create_app
            app = create_app(config)
            yield TestClient(app)

    # --- Health endpoint ---

    def test_health_returns_200(self, client):
        resp = client.get("/api/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "version" in data

    # --- Tools listing ---

    def test_tools_endpoint_returns_list(self, client):
        resp = client.get("/api/tools")
        assert resp.status_code == 200
        data = resp.json()
        assert "tools" in data
        assert "count" in data
        assert data["count"] == 2
        assert data["tier"] == "full"

    def test_tools_endpoint_schema_format(self, client):
        resp = client.get("/api/tools")
        data = resp.json()
        tools = data["tools"]
        for tool in tools:
            assert "name" in tool
            assert "description" in tool
            assert "parameters" in tool
            assert "method" in tool
            assert "endpoint" in tool

    # --- Tool execution ---

    def test_execute_known_tool(self, client):
        resp = client.post("/api/tools/printer_status", json={})
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "idle"

    def test_execute_unknown_tool_returns_404(self, client):
        resp = client.post("/api/tools/nonexistent", json={})
        assert resp.status_code == 404

    def test_execute_tool_with_non_dict_body(self, client):
        resp = client.post(
            "/api/tools/printer_status",
            content=json.dumps("not a dict"),
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 400

    # --- Auth enforcement ---

    def test_tools_without_auth_when_required(self, authed_client):
        resp = authed_client.get("/api/tools")
        assert resp.status_code == 401

    def test_tools_with_wrong_auth(self, authed_client):
        resp = authed_client.get(
            "/api/tools",
            headers={"Authorization": "Bearer wrong-token"},
        )
        assert resp.status_code == 401

    def test_tools_with_correct_auth(self, authed_client):
        resp = authed_client.get(
            "/api/tools",
            headers={"Authorization": "Bearer test-secret-token"},
        )
        assert resp.status_code == 200

    def test_health_no_auth_required(self, authed_client):
        """Health endpoint should work without auth even when auth is configured."""
        resp = authed_client.get("/api/health")
        assert resp.status_code == 200

    def test_tool_execution_requires_auth(self, authed_client):
        resp = authed_client.post("/api/tools/printer_status", json={})
        assert resp.status_code == 401

    def test_tool_execution_with_auth(self, authed_client):
        resp = authed_client.post(
            "/api/tools/printer_status",
            json={},
            headers={"Authorization": "Bearer test-secret-token"},
        )
        assert resp.status_code == 200

    # --- Agent endpoint ---

    def test_agent_endpoint_requires_prompt(self, client):
        resp = client.post("/api/agent", json={"api_key": "sk-test"})
        assert resp.status_code == 400

    def test_agent_endpoint_requires_api_key(self, client):
        resp = client.post("/api/agent", json={"prompt": "hello"})
        assert resp.status_code == 400

    @mock.patch("kiln.rest_api._get_mcp_instance")
    def test_agent_endpoint_runs_loop(self, mock_get_mcp, mock_mcp):
        try:
            from fastapi.testclient import TestClient
        except ImportError:
            pytest.skip("FastAPI not installed")

        mock_get_mcp.return_value = mock_mcp

        with mock.patch("kiln.agent_loop.run_agent_loop") as mock_loop:
            from kiln.agent_loop import AgentResult

            mock_loop.return_value = AgentResult(
                response="Printer is idle",
                messages=[],
                tool_calls_made=0,
                turns=1,
                model="openai/gpt-4o",
            )

            from kiln.rest_api import create_app

            app = create_app(RestApiConfig())
            client = TestClient(app)

            resp = client.post(
                "/api/agent",
                json={
                    "prompt": "What is the printer status?",
                    "api_key": "sk-test",
                    "model": "openai/gpt-4o",
                },
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["response"] == "Printer is idle"

    # --- Tool error handling ---

    def test_tool_type_error_returns_400(self, client, mock_mcp):
        """If the tool function raises TypeError (wrong params), return 400."""
        bad_tool = mock.MagicMock()
        bad_tool.name = "bad_tool"
        bad_tool.fn = mock.MagicMock(side_effect=TypeError("unexpected keyword arg"))
        mock_mcp._tool_manager.get_tool.side_effect = lambda name: {
            "printer_status": mock_mcp._tool_manager.list_tools.return_value[0],
            "printer_files": mock_mcp._tool_manager.list_tools.return_value[1],
            "bad_tool": bad_tool,
        }.get(name)

        resp = client.post("/api/tools/bad_tool", json={})
        assert resp.status_code == 400

    def test_tool_runtime_error_returns_error_json(self, client, mock_mcp):
        """If the tool raises a generic exception, return 500 with error JSON."""
        err_tool = mock.MagicMock()
        err_tool.name = "err_tool"
        err_tool.fn = mock.MagicMock(side_effect=RuntimeError("printer offline"))
        mock_mcp._tool_manager.get_tool.side_effect = lambda name: {
            "printer_status": mock_mcp._tool_manager.list_tools.return_value[0],
            "printer_files": mock_mcp._tool_manager.list_tools.return_value[1],
            "err_tool": err_tool,
        }.get(name)

        resp = client.post("/api/tools/err_tool", json={})
        assert resp.status_code == 500
        data = resp.json()
        assert data["success"] is False
        assert "error" in data


# ---------------------------------------------------------------------------
# 5. run_rest_server
# ---------------------------------------------------------------------------


class TestRunRestServer:
    def test_requires_uvicorn(self):
        """If uvicorn is not importable, run_rest_server should raise ImportError."""
        import builtins

        original_import = builtins.__import__

        def mock_import(name, *args, **kwargs):
            if name == "uvicorn":
                raise ImportError("No module named 'uvicorn'")
            return original_import(name, *args, **kwargs)

        from kiln.rest_api import run_rest_server

        with mock.patch("builtins.__import__", side_effect=mock_import):
            with pytest.raises(ImportError, match="Uvicorn"):
                run_rest_server()

    @mock.patch("kiln.rest_api.create_app")
    def test_run_rest_server_calls_uvicorn(self, mock_create_app):
        try:
            import uvicorn
        except ImportError:
            pytest.skip("uvicorn not installed")

        mock_app = mock.MagicMock()
        mock_create_app.return_value = mock_app

        with mock.patch("uvicorn.run") as mock_run:
            from kiln.rest_api import run_rest_server

            config = RestApiConfig(host="127.0.0.1", port=9999)
            run_rest_server(config)
            mock_run.assert_called_once_with(
                mock_app, host="127.0.0.1", port=9999
            )

    @mock.patch("kiln.rest_api.create_app")
    def test_run_rest_server_rejects_non_localhost_without_auth(self, mock_create_app):
        try:
            import uvicorn  # noqa: F401
        except ImportError:
            pytest.skip("uvicorn not installed")

        mock_create_app.return_value = mock.MagicMock()
        from kiln.rest_api import run_rest_server

        config = RestApiConfig(host="0.0.0.0", port=9999, auth_token=None)
        with pytest.raises(RuntimeError, match="KILN_API_AUTH_TOKEN"):
            run_rest_server(config)

    @mock.patch("kiln.rest_api.create_app")
    def test_run_rest_server_allows_non_localhost_with_auth(self, mock_create_app):
        try:
            import uvicorn
        except ImportError:
            pytest.skip("uvicorn not installed")

        mock_app = mock.MagicMock()
        mock_create_app.return_value = mock_app

        with mock.patch("uvicorn.run") as mock_run:
            from kiln.rest_api import run_rest_server

            config = RestApiConfig(host="0.0.0.0", port=9999, auth_token="secret-token")
            run_rest_server(config)
            mock_run.assert_called_once_with(
                mock_app, host="0.0.0.0", port=9999
            )
