from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from leshy.cli.openapi_parser import (
    OpenAPIParseError,
    _build_body_template,
    _build_response_path,
    _detect_spec_version,
    _resolve_ref,
    _resolve_schema,
    load_spec,
    parse_openapi_spec,
)

# ---------------------------------------------------------------------------
# Helpers to build minimal specs
# ---------------------------------------------------------------------------


def _v3_spec(
    path: str = "/chat",
    request_properties: dict[str, Any] | None = None,
    response_properties: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a minimal OpenAPI 3.0 spec."""
    spec: dict[str, Any] = {"openapi": "3.0.0", "paths": {}}
    post: dict[str, Any] = {"summary": "Chat endpoint"}
    if request_properties is not None:
        post["requestBody"] = {
            "content": {
                "application/json": {
                    "schema": {"type": "object", "properties": request_properties}
                }
            }
        }
    if response_properties is not None:
        post["responses"] = {
            "200": {
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": response_properties,
                        }
                    }
                }
            }
        }
    spec["paths"][path] = {"post": post}
    return spec


def _v2_spec(
    path: str = "/chat",
    request_properties: dict[str, Any] | None = None,
    response_properties: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a minimal Swagger 2.0 spec."""
    spec: dict[str, Any] = {"swagger": "2.0", "paths": {}}
    post: dict[str, Any] = {"summary": "Chat endpoint"}
    if request_properties is not None:
        post["parameters"] = [
            {
                "in": "body",
                "name": "body",
                "schema": {"type": "object", "properties": request_properties},
            }
        ]
    if response_properties is not None:
        post["responses"] = {
            "200": {
                "schema": {"type": "object", "properties": response_properties}
            }
        }
    spec["paths"][path] = {"post": post}
    return spec


# ---------------------------------------------------------------------------
# TestLoadSpec
# ---------------------------------------------------------------------------


class TestLoadSpec:
    def test_load_from_json_file(self, tmp_path: Path) -> None:
        spec = {"openapi": "3.0.0", "paths": {}}
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))
        result = load_spec(str(f))
        assert result["openapi"] == "3.0.0"

    def test_load_from_yaml_file(self, tmp_path: Path) -> None:
        f = tmp_path / "spec.yaml"
        f.write_text("openapi: '3.0.0'\npaths: {}\n")
        result = load_spec(str(f))
        assert result["openapi"] == "3.0.0"

    def test_file_not_found(self) -> None:
        with pytest.raises(OpenAPIParseError, match="File not found"):
            load_spec("/nonexistent/path/spec.yaml")

    def test_invalid_yaml(self, tmp_path: Path) -> None:
        f = tmp_path / "bad.yaml"
        f.write_text(": : :\n  invalid\n[[[")
        with pytest.raises(OpenAPIParseError, match="Invalid YAML"):
            load_spec(str(f))


# ---------------------------------------------------------------------------
# TestDetectSpecVersion
# ---------------------------------------------------------------------------


class TestDetectSpecVersion:
    def test_openapi_3_0(self) -> None:
        assert _detect_spec_version({"openapi": "3.0.0"}) == "3.x"

    def test_openapi_3_1(self) -> None:
        assert _detect_spec_version({"openapi": "3.1.0"}) == "3.x"

    def test_swagger_2_0(self) -> None:
        assert _detect_spec_version({"swagger": "2.0"}) == "2.0"

    def test_unknown_version(self) -> None:
        with pytest.raises(OpenAPIParseError, match="Could not detect"):
            _detect_spec_version({"version": "1.0"})


# ---------------------------------------------------------------------------
# TestResolveRef
# ---------------------------------------------------------------------------


class TestResolveRef:
    def test_simple_ref(self) -> None:
        spec = {"components": {"schemas": {"Msg": {"type": "object"}}}}
        result = _resolve_ref(spec, "#/components/schemas/Msg")
        assert result == {"type": "object"}

    def test_swagger2_definitions_ref(self) -> None:
        spec = {"definitions": {"Msg": {"type": "object"}}}
        result = _resolve_ref(spec, "#/definitions/Msg")
        assert result == {"type": "object"}

    def test_nested_ref(self) -> None:
        spec = {
            "components": {
                "schemas": {
                    "A": {"$ref": "#/components/schemas/B"},
                    "B": {"type": "string"},
                }
            }
        }
        result = _resolve_ref(spec, "#/components/schemas/A")
        assert result == {"type": "string"}

    def test_circular_ref_raises(self) -> None:
        spec = {
            "components": {
                "schemas": {
                    "A": {"$ref": "#/components/schemas/B"},
                    "B": {"$ref": "#/components/schemas/A"},
                }
            }
        }
        with pytest.raises(OpenAPIParseError, match="Circular"):
            _resolve_ref(spec, "#/components/schemas/A")


# ---------------------------------------------------------------------------
# TestResolveSchema
# ---------------------------------------------------------------------------


class TestResolveSchema:
    def test_allof_merges_properties(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "allOf": [
                {"type": "object", "properties": {"a": {"type": "string"}}},
                {"properties": {"b": {"type": "integer"}}},
            ]
        }
        result = _resolve_schema(spec, schema)
        assert "a" in result["properties"]
        assert "b" in result["properties"]

    def test_oneof_uses_first(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "oneOf": [
                {"type": "object", "properties": {"x": {"type": "string"}}},
                {"type": "object", "properties": {"y": {"type": "string"}}},
            ]
        }
        result = _resolve_schema(spec, schema)
        assert "x" in result.get("properties", {})


# ---------------------------------------------------------------------------
# TestBuildBodyTemplate
# ---------------------------------------------------------------------------


class TestBuildBodyTemplate:
    def test_simple_prompt_field(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {"prompt": {"type": "string"}},
        }
        result = _build_body_template(spec, schema)
        parsed = json.loads(result)
        assert parsed["prompt"] == "{{payload}}"

    def test_message_field_with_extras(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "message": {"type": "string"},
                "model": {"type": "string"},
            },
        }
        result = _build_body_template(spec, schema)
        parsed = json.loads(result)
        assert parsed["message"] == "{{payload}}"
        assert parsed["model"] == ""

    def test_messages_array_pattern(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "messages": {"type": "array", "items": {"type": "object"}},
                "model": {"type": "string"},
            },
        }
        result = _build_body_template(spec, schema)
        parsed = json.loads(result)
        assert parsed["messages"] == [{"role": "user", "content": "{{payload}}"}]
        assert parsed["model"] == ""

    def test_no_known_field_uses_first_string(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "custom_field": {"type": "string"},
                "count": {"type": "integer"},
            },
        }
        result = _build_body_template(spec, schema)
        parsed = json.loads(result)
        assert parsed["custom_field"] == "{{payload}}"
        assert parsed["count"] == 0

    def test_empty_schema_returns_default(self) -> None:
        result = _build_body_template({}, {"type": "object"})
        assert "{{payload}}" in result

    def test_with_ref_properties(self) -> None:
        spec = {
            "components": {
                "schemas": {
                    "ChatRequest": {
                        "type": "object",
                        "properties": {"query": {"type": "string"}},
                    }
                }
            }
        }
        schema = {"$ref": "#/components/schemas/ChatRequest"}
        result = _build_body_template(spec, schema)
        parsed = json.loads(result)
        assert parsed["query"] == "{{payload}}"


# ---------------------------------------------------------------------------
# TestBuildResponsePath
# ---------------------------------------------------------------------------


class TestBuildResponsePath:
    def test_direct_text_field(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {"text": {"type": "string"}},
        }
        assert _build_response_path(spec, schema) == "$.text"

    def test_direct_response_field(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "status": {"type": "integer"},
                "response": {"type": "string"},
            },
        }
        assert _build_response_path(spec, schema) == "$.response"

    def test_nested_content_field(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "payload": {
                    "type": "object",
                    "properties": {"content": {"type": "string"}},
                }
            },
        }
        assert _build_response_path(spec, schema) == "$.payload.content"

    def test_array_indexed_field(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "choices": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {"content": {"type": "string"}},
                    },
                }
            },
        }
        assert _build_response_path(spec, schema) == "$.choices[0].content"

    def test_fallback_first_string(self) -> None:
        spec: dict[str, Any] = {}
        schema = {
            "type": "object",
            "properties": {
                "code": {"type": "integer"},
                "body": {"type": "string"},
            },
        }
        assert _build_response_path(spec, schema) == "$.body"

    def test_empty_schema_returns_none(self) -> None:
        assert _build_response_path({}, {"type": "object"}) is None


# ---------------------------------------------------------------------------
# TestParseOpenAPISpec (integration)
# ---------------------------------------------------------------------------


class TestParseOpenAPISpec:
    def test_full_openapi3_spec(self, tmp_path: Path) -> None:
        spec = _v3_spec(
            path="/v1/chat",
            request_properties={"prompt": {"type": "string"}},
            response_properties={"text": {"type": "string"}},
        )
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))

        endpoints = parse_openapi_spec(str(f))
        assert len(endpoints) == 1
        ep = endpoints[0]
        assert ep.path == "/v1/chat"
        assert "{{payload}}" in ep.body_template
        assert ep.response_path == "$.text"

    def test_full_swagger2_spec(self, tmp_path: Path) -> None:
        spec = _v2_spec(
            path="/api/ask",
            request_properties={"query": {"type": "string"}},
            response_properties={"answer": {"type": "string"}},
        )
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))

        endpoints = parse_openapi_spec(str(f))
        assert len(endpoints) == 1
        ep = endpoints[0]
        assert ep.path == "/api/ask"
        parsed = json.loads(ep.body_template)
        assert parsed["query"] == "{{payload}}"
        assert ep.response_path == "$.answer"

    def test_multiple_post_endpoints(self, tmp_path: Path) -> None:
        spec: dict[str, Any] = {
            "openapi": "3.0.0",
            "paths": {
                "/chat": {
                    "post": {
                        "summary": "Chat",
                        "requestBody": {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "message": {"type": "string"},
                                        },
                                    }
                                }
                            }
                        },
                        "responses": {},
                    }
                },
                "/search": {
                    "post": {
                        "summary": "Search",
                        "requestBody": {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "query": {"type": "string"},
                                        },
                                    }
                                }
                            }
                        },
                        "responses": {},
                    }
                },
            },
        }
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))

        endpoints = parse_openapi_spec(str(f))
        assert len(endpoints) == 2
        paths = {ep.path for ep in endpoints}
        assert "/chat" in paths
        assert "/search" in paths

    def test_no_post_endpoints(self, tmp_path: Path) -> None:
        spec = {
            "openapi": "3.0.0",
            "paths": {
                "/health": {"get": {"summary": "Health check"}},
            },
        }
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))

        endpoints = parse_openapi_spec(str(f))
        assert endpoints == []

    def test_openai_style_messages(self, tmp_path: Path) -> None:
        spec = _v3_spec(
            path="/v1/chat/completions",
            request_properties={
                "model": {"type": "string"},
                "messages": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "role": {"type": "string"},
                            "content": {"type": "string"},
                        },
                    },
                },
            },
            response_properties={
                "choices": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "message": {
                                "type": "object",
                                "properties": {
                                    "content": {"type": "string"},
                                },
                            }
                        },
                    },
                },
            },
        )
        f = tmp_path / "spec.json"
        f.write_text(json.dumps(spec))

        endpoints = parse_openapi_spec(str(f))
        assert len(endpoints) == 1
        ep = endpoints[0]
        parsed = json.loads(ep.body_template)
        assert parsed["messages"] == [
            {"role": "user", "content": "{{payload}}"}
        ]
        assert ep.response_path == "$.choices[0].message"
