import pytest
from cognite.client.data_classes.data_modeling.ids import EdgeId, NodeId

from cog_mcp.parsing import (
    inject_space_filter,
    parse_edge_id,
    parse_node_id,
    parse_node_ids,
    parse_through,
)


def test_parse_node_id_accepts_nodeid_and_both_external_id_key_styles() -> None:
    existing = NodeId(space="s", external_id="n1")
    from_camel = parse_node_id({"space": "s", "externalId": "n2"}, "field")
    from_snake = parse_node_id({"space": "s", "external_id": "n3"}, "field")

    assert parse_node_id(existing, "field") is existing
    assert from_camel.space == "s" and from_camel.external_id == "n2"
    assert from_snake.space == "s" and from_snake.external_id == "n3"


@pytest.mark.parametrize(
    "value",
    [
        "not-an-object",
        {"space": "", "externalId": "x"},
        {"space": "s", "externalId": ""},
        {"space": "s"},
    ],
)
def test_parse_node_id_rejects_invalid_inputs(value: object) -> None:
    with pytest.raises(ValueError):
        parse_node_id(value, "instance_id")


def test_parse_edge_id_accepts_edgeid_and_both_key_styles() -> None:
    existing = EdgeId(space="s", external_id="e1")
    from_camel = parse_edge_id({"space": "s", "externalId": "e2"}, "field")
    from_snake = parse_edge_id({"space": "s", "external_id": "e3"}, "field")

    assert parse_edge_id(existing, "field") is existing
    assert from_camel.space == "s" and from_camel.external_id == "e2"
    assert from_snake.space == "s" and from_snake.external_id == "e3"


def test_parse_node_ids_normalizes_single_object_and_limits_size() -> None:
    single = parse_node_ids({"space": "docs", "externalId": "doc-1"})
    assert len(single) == 1
    assert single[0].space == "docs"
    assert single[0].external_id == "doc-1"

    too_many = [{"space": "docs", "externalId": f"doc-{idx}"} for idx in range(3)]
    with pytest.raises(ValueError, match="maximum of 2"):
        parse_node_ids(too_many, max_items=2)


def test_parse_node_ids_requires_non_empty_list() -> None:
    with pytest.raises(ValueError, match="non-empty list"):
        parse_node_ids([])


def test_parse_through_infers_source_type_for_view_and_container() -> None:
    view_through = parse_through(
        {"source": {"space": "s", "externalId": "View", "version": "v1"}, "identifier": "asset"}
    )
    container_through = parse_through(
        {"source": {"space": "s", "externalId": "Container"}, "identifier": "asset"}
    )

    assert view_through is not None
    assert view_through.dump(camel_case=True)["source"]["type"] == "view"
    assert container_through is not None
    assert container_through.dump(camel_case=True)["source"]["type"] == "container"
    assert parse_through(None) is None


class TestInjectSpaceFilter:
    """Direct unit tests for the space-scoping safety function."""

    def test_no_user_filter_returns_space_in_filter(self) -> None:
        result = inject_space_filter("node", None, ["space_a", "space_b"])
        dumped = result.dump()

        assert dumped == {"in": {"property": ["node", "space"], "values": ["space_a", "space_b"]}}

    def test_with_user_filter_returns_and_of_space_and_user(self) -> None:
        user_filter = {"equals": {"property": ["node", "name"], "value": "my-node"}}
        result = inject_space_filter("node", user_filter, ["space_a"])
        dumped = result.dump()

        assert dumped == {
            "and": [
                {"in": {"property": ["node", "space"], "values": ["space_a"]}},
                {"equals": {"property": ["node", "name"], "value": "my-node"}},
            ]
        }

    @pytest.mark.parametrize(
        ("instance_type", "expected_property"),
        [
            ("node", ["node", "space"]),
            ("edge", ["edge", "space"]),
        ],
    )
    def test_instance_type_sets_property_reference(
        self, instance_type: str, expected_property: list[str]
    ) -> None:
        result = inject_space_filter(instance_type, None, ["s1"])
        dumped = result.dump()

        assert dumped["in"]["property"] == expected_property

    def test_empty_spaces_list_produces_empty_in_values(self) -> None:
        result = inject_space_filter("node", None, [])
        dumped = result.dump()

        assert dumped == {"in": {"property": ["node", "space"], "values": []}}
