"""Tests for lapis_spec.parser.ref_resolver."""

import pytest

from lapis_spec.parser.ref_resolver import (
    RefResolutionError,
    count_refs,
    ref_to_schema_name,
    resolve_refs,
)


class TestResolveRefs:
    def test_simple_ref(self):
        doc = {
            "paths": {
                "/items": {
                    "get": {
                        "responses": {
                            "200": {
                                "schema": {"$ref": "#/components/schemas/Item"}
                            }
                        }
                    }
                }
            },
            "components": {
                "schemas": {
                    "Item": {
                        "type": "object",
                        "properties": {"id": {"type": "string"}},
                    }
                }
            },
        }
        resolved = resolve_refs(doc)
        schema = resolved["paths"]["/items"]["get"]["responses"]["200"]["schema"]
        assert schema["type"] == "object"
        assert "id" in schema["properties"]

    def test_nested_ref(self):
        doc = {
            "components": {
                "schemas": {
                    "Inner": {"type": "string"},
                    "Outer": {
                        "type": "object",
                        "properties": {
                            "value": {"$ref": "#/components/schemas/Inner"}
                        },
                    },
                }
            }
        }
        resolved = resolve_refs(doc)
        inner = resolved["components"]["schemas"]["Outer"]["properties"]["value"]
        assert inner["type"] == "string"

    def test_circular_ref_tolerated(self):
        """Circular refs should not raise; the cycle is cut and the
        node keeps its x-schema-name so the type can still be matched."""
        doc = {
            "components": {
                "schemas": {
                    "A": {"$ref": "#/components/schemas/B"},
                    "B": {"$ref": "#/components/schemas/A"},
                }
            }
        }
        resolved = resolve_refs(doc)
        # B resolved into A, then A's self-ref was cut
        assert "x-schema-name" in resolved["components"]["schemas"]["A"]

    def test_self_referencing_schema(self):
        """A schema that references itself (e.g. tree nodes) should
        resolve without error, keeping the type name at the cycle point."""
        doc = {
            "components": {
                "schemas": {
                    "TreeNode": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "children": {
                                "type": "array",
                                "items": {"$ref": "#/components/schemas/TreeNode"},
                            },
                        },
                    }
                }
            }
        }
        resolved = resolve_refs(doc)
        node = resolved["components"]["schemas"]["TreeNode"]
        assert node["type"] == "object"
        # First expansion resolves the ref into a full copy of TreeNode.
        # Inside that copy, the *nested* self-ref is cut with x-schema-name.
        expanded = node["properties"]["children"]["items"]
        nested_items = expanded["properties"]["children"]["items"]
        assert nested_items.get("x-schema-name") == "TreeNode"

    def test_missing_ref_raises(self):
        doc = {"paths": {"$ref": "#/components/schemas/NonExistent"}}
        with pytest.raises(RefResolutionError, match="Cannot resolve"):
            resolve_refs(doc)

    def test_original_not_mutated(self):
        doc = {
            "a": {"$ref": "#/b"},
            "b": {"value": 42},
        }
        original_a = doc["a"].copy()
        resolve_refs(doc)
        assert doc["a"] == original_a

    def test_array_refs(self):
        doc = {
            "items": [{"$ref": "#/definitions/A"}],
            "definitions": {"A": {"type": "integer"}},
        }
        resolved = resolve_refs(doc)
        assert resolved["items"][0]["type"] == "integer"


class TestCountRefs:
    def test_counts(self):
        doc = {
            "a": {"$ref": "#/components/schemas/X"},
            "b": {"$ref": "#/components/schemas/X"},
            "c": {"$ref": "#/components/schemas/Y"},
        }
        counts = count_refs(doc)
        assert counts["#/components/schemas/X"] == 2
        assert counts["#/components/schemas/Y"] == 1

    def test_empty(self):
        assert count_refs({}) == {}

    def test_nested_counts(self):
        doc = {
            "a": {
                "b": [{"$ref": "#/x"}, {"c": {"$ref": "#/x"}}]
            }
        }
        counts = count_refs(doc)
        assert counts["#/x"] == 2


class TestRefToSchemaName:
    def test_valid_ref(self):
        assert ref_to_schema_name("#/components/schemas/Invoice") == "Invoice"

    def test_non_schema_ref(self):
        assert ref_to_schema_name("#/components/parameters/Id") is None

    def test_nested_path(self):
        assert ref_to_schema_name("#/components/schemas/") == ""
