"""Tests for OpenAPIAdapter.introspect() — verifies that a mock OpenAPI spec
is correctly parsed into Brynq tool descriptors."""

import json
import pytest
from unittest.mock import patch, MagicMock

from runtime.adapters.openapi import OpenAPIAdapter


# ── Mock OpenAPI spec ─────────────────────────────────────────────────────

MOCK_OPENAPI_SPEC = {
    "openapi": "3.0.0",
    "info": {"title": "Pet Store", "version": "1.0.0"},
    "servers": [{"url": "https://api.petstore.io/v1"}],
    "paths": {
        "/pets": {
            "get": {
                "operationId": "listPets",
                "summary": "List all pets",
                "parameters": [
                    {
                        "name": "limit",
                        "in": "query",
                        "description": "How many items to return",
                        "required": False,
                    }
                ],
            },
            "post": {
                "operationId": "createPet",
                "summary": "Create a pet",
                "parameters": [
                    {
                        "name": "name",
                        "in": "body",
                        "description": "Pet name",
                        "required": True,
                    }
                ],
            },
        },
        "/pets/{petId}": {
            "parameters": [
                {
                    "name": "petId",
                    "in": "path",
                    "description": "The id of the pet",
                    "required": True,
                }
            ],
            "get": {
                "operationId": "getPet",
                "summary": "Get a pet by ID",
            },
            "delete": {
                "operationId": "deletePet",
                "description": "Remove a pet from the store",
            },
        },
    },
}


@pytest.fixture
def adapter():
    """Create an adapter with spec pre-loaded (bypass network fetch)."""
    a = OpenAPIAdapter("https://example.com/openapi.json")
    a._spec = MOCK_OPENAPI_SPEC
    a._base_url = "https://api.petstore.io/v1"
    a._tools = OpenAPIAdapter._extract_tools(MOCK_OPENAPI_SPEC)
    return a


class TestIntrospect:
    def test_returns_tools_list(self, adapter):
        result = adapter.introspect()
        assert "tools" in result
        assert isinstance(result["tools"], list)

    def test_returns_version(self, adapter):
        result = adapter.introspect()
        assert "version" in result
        assert isinstance(result["version"], str)

    def test_returns_source(self, adapter):
        result = adapter.introspect()
        assert result["source"] == "https://example.com/openapi.json"

    def test_extracts_correct_number_of_tools(self, adapter):
        tools = adapter.introspect()["tools"]
        # listPets, createPet, getPet, deletePet
        assert len(tools) == 4

    def test_tool_names_match_operation_ids(self, adapter):
        tools = adapter.introspect()["tools"]
        names = {t["name"] for t in tools}
        assert names == {"listPets", "createPet", "getPet", "deletePet"}

    def test_tool_descriptions_from_summary(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        assert by_name["listPets"]["description"] == "List all pets"
        assert by_name["createPet"]["description"] == "Create a pet"
        assert by_name["getPet"]["description"] == "Get a pet by ID"

    def test_tool_description_falls_back_to_description_field(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        # deletePet has no summary, only description
        assert by_name["deletePet"]["description"] == "Remove a pet from the store"

    def test_tool_method_is_preserved(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        assert by_name["listPets"]["method"] == "get"
        assert by_name["createPet"]["method"] == "post"
        assert by_name["getPet"]["method"] == "get"
        assert by_name["deletePet"]["method"] == "delete"

    def test_tool_path_is_preserved(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        assert by_name["listPets"]["path"] == "/pets"
        assert by_name["getPet"]["path"] == "/pets/{petId}"

    def test_operation_level_parameters_extracted(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        params = by_name["listPets"]["parameters"]
        assert len(params) == 1
        assert params[0]["name"] == "limit"
        assert params[0]["location"] == "query"
        assert params[0]["required"] is False

    def test_path_level_parameters_inherited(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        # getPet has no operation-level params but inherits petId from path
        params = by_name["getPet"]["parameters"]
        assert len(params) == 1
        assert params[0]["name"] == "petId"
        assert params[0]["location"] == "path"
        assert params[0]["required"] is True

    def test_path_params_inherited_by_delete(self, adapter):
        tools = adapter.introspect()["tools"]
        by_name = {t["name"]: t for t in tools}
        params = by_name["deletePet"]["parameters"]
        assert len(params) == 1
        assert params[0]["name"] == "petId"


class TestIntrospectEdgeCases:
    def test_empty_paths(self):
        spec = {"openapi": "3.0.0", "info": {}, "paths": {}}
        a = OpenAPIAdapter("file:///tmp/empty.json")
        a._spec = spec
        a._base_url = ""
        a._tools = OpenAPIAdapter._extract_tools(spec)
        assert a.introspect()["tools"] == []

    def test_no_paths_key(self):
        spec = {"openapi": "3.0.0", "info": {}}
        a = OpenAPIAdapter("file:///tmp/no_paths.json")
        a._spec = spec
        a._base_url = ""
        a._tools = OpenAPIAdapter._extract_tools(spec)
        assert a.introspect()["tools"] == []

    def test_unsupported_methods_ignored(self):
        spec = {
            "openapi": "3.0.0",
            "info": {},
            "paths": {
                "/health": {
                    "head": {"operationId": "headHealth"},
                    "options": {"operationId": "optionsHealth"},
                    "get": {"operationId": "getHealth", "summary": "Health check"},
                }
            },
        }
        a = OpenAPIAdapter("file:///tmp/methods.json")
        a._spec = spec
        a._base_url = ""
        a._tools = OpenAPIAdapter._extract_tools(spec)
        tools = a.introspect()["tools"]
        assert len(tools) == 1
        assert tools[0]["name"] == "getHealth"

    def test_fallback_name_when_no_operation_id(self):
        spec = {
            "openapi": "3.0.0",
            "info": {},
            "paths": {
                "/status": {
                    "get": {"summary": "Get status"},
                }
            },
        }
        a = OpenAPIAdapter("file:///tmp/noid.json")
        a._spec = spec
        a._base_url = ""
        a._tools = OpenAPIAdapter._extract_tools(spec)
        tools = a.introspect()["tools"]
        assert tools[0]["name"] == "GET /status"
        assert tools[0]["description"] == "Get status"

    def test_non_dict_path_item_skipped(self):
        spec = {
            "openapi": "3.0.0",
            "info": {},
            "paths": {
                "/ok": {"get": {"operationId": "ok"}},
                "/bad": "not a dict",
            },
        }
        a = OpenAPIAdapter("file:///tmp/bad.json")
        a._spec = spec
        a._base_url = ""
        a._tools = OpenAPIAdapter._extract_tools(spec)
        assert len(a.introspect()["tools"]) == 1


class TestBaseUrl:
    def test_openapi3_servers(self):
        spec = {"servers": [{"url": "https://api.example.com/v2/"}]}
        assert OpenAPIAdapter._resolve_base_url(spec) == "https://api.example.com/v2"

    def test_swagger2_host(self):
        spec = {"host": "api.example.com", "basePath": "/v1", "schemes": ["http"]}
        assert OpenAPIAdapter._resolve_base_url(spec) == "http://api.example.com/v1"

    def test_swagger2_defaults_to_https(self):
        spec = {"host": "api.example.com"}
        assert OpenAPIAdapter._resolve_base_url(spec) == "https://api.example.com"

    def test_no_server_info_returns_empty(self):
        assert OpenAPIAdapter._resolve_base_url({}) == ""


class TestFetchSpecFromJson:
    def test_parses_json_string(self):
        raw = json.dumps(MOCK_OPENAPI_SPEC)
        result = OpenAPIAdapter._parse_spec(raw)
        assert result["info"]["title"] == "Pet Store"

    def test_non_object_json_parsed_without_error(self):
        # _parse_spec doesn't validate structure — json.loads succeeds on arrays
        result = OpenAPIAdapter._parse_spec("[1, 2, 3]")
        assert result == [1, 2, 3]

    def test_rejects_empty_string(self):
        with pytest.raises(ValueError):
            OpenAPIAdapter._parse_spec("")


class TestBuildRequest:
    """Tests for _build_request path-parameter interpolation."""

    def test_single_path_param_interpolated(self, adapter):
        result = adapter._build_request("getPet", {"petId": "123"})
        assert result["url"] == "https://api.petstore.io/v1/pets/123"
        assert result["method"] == "GET"

    def test_path_param_integer_coerced_to_string(self, adapter):
        result = adapter._build_request("getPet", {"petId": 42})
        assert result["url"] == "https://api.petstore.io/v1/pets/42"

    def test_path_param_not_in_query_or_body(self, adapter):
        result = adapter._build_request("getPet", {"petId": "99"})
        assert result["query"] == {}
        assert result["body"] is None

    def test_multiple_path_params(self):
        """Spec with two path params — both get interpolated."""
        spec = {
            "openapi": "3.0.0",
            "info": {},
            "servers": [{"url": "https://api.example.com"}],
            "paths": {
                "/users/{userId}/posts/{postId}": {
                    "parameters": [
                        {"name": "userId", "in": "path", "required": True},
                        {"name": "postId", "in": "path", "required": True},
                    ],
                    "get": {
                        "operationId": "getUserPost",
                        "summary": "Get a user post",
                    },
                }
            },
        }
        a = OpenAPIAdapter("file:///tmp/multi.json")
        a._spec = spec
        a._base_url = "https://api.example.com"
        a._tools = OpenAPIAdapter._extract_tools(spec)

        result = a._build_request("getUserPost", {"userId": "123", "postId": "456"})
        assert result["url"] == "https://api.example.com/users/123/posts/456"

    def test_path_and_query_params_together(self, adapter):
        """getPet with extra query arg — path interpolated, query appended."""
        # listPets has a query param 'limit'; getPet has a path param 'petId'.
        # Use a spec that has both on one operation.
        spec = {
            "openapi": "3.0.0",
            "info": {},
            "servers": [{"url": "https://api.example.com"}],
            "paths": {
                "/users/{id}": {
                    "get": {
                        "operationId": "getUser",
                        "summary": "Get user",
                        "parameters": [
                            {"name": "id", "in": "path", "required": True},
                            {"name": "fields", "in": "query", "required": False},
                        ],
                    },
                }
            },
        }
        a = OpenAPIAdapter("file:///tmp/mixed.json")
        a._spec = spec
        a._base_url = "https://api.example.com"
        a._tools = OpenAPIAdapter._extract_tools(spec)

        result = a._build_request("getUser", {"id": "123", "fields": "name,email"})
        assert "/users/123?" in result["url"]
        assert "fields=name" in result["url"]
        assert result["query"] == {"fields": "name,email"}

    def test_unknown_operation_raises(self, adapter):
        with pytest.raises(ValueError, match="Unknown operation"):
            adapter._build_request("nonExistent", {})
