import json
from types import SimpleNamespace


def test_list_instances_parses_filter_sort_and_total_count(
    tools, cognite_client_mock, sample_config, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.list.return_value = [
        dumpable_factory({"externalId": "n1"}),
        dumpable_factory({"externalId": "n2"}),
    ]
    cognite_client_mock.data_modeling.instances.aggregate.return_value = SimpleNamespace(value=7)

    payload = json.loads(
        tools["list_instances"](
            **view_args,
            filter='{"equals": {"property": ["node", "space"], "value": "space-a"}}',
            limit=2,
            sort='[{"property": ["my-space", "MyView/v1", "name"], "direction": "descending", "nullsFirst": true}]',
        )
    )

    assert payload["count"] == 2
    assert payload["total"] == 7
    assert payload["message"] == "Showing 2 of 7 instances."

    kwargs = cognite_client_mock.data_modeling.instances.list.call_args.kwargs
    assert kwargs["space"] == sample_config.instance_spaces
    assert kwargs["filter"]["equals"]["value"] == "space-a"
    assert kwargs["limit"] == 2
    assert kwargs["sort"][0].direction == "descending"
    assert kwargs["sort"][0].nulls_first is True


def test_list_instances_accepts_single_sort_object_and_list_aggregate_count(
    tools, cognite_client_mock, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.list.return_value = [
        dumpable_factory({"externalId": "n1"})
    ]
    cognite_client_mock.data_modeling.instances.aggregate.return_value = [SimpleNamespace(value=11)]

    payload = json.loads(
        tools["list_instances"](
            **view_args,
            limit=1,
            sort={"property": ["my-space", "MyView/v1", "name"]},
        )
    )

    kwargs = cognite_client_mock.data_modeling.instances.list.call_args.kwargs
    assert kwargs["sort"].property == ["my-space", "MyView/v1", "name"]
    assert kwargs["sort"].direction == "ascending"
    assert payload["total"] == 11


def test_list_instances_limit_minus_one_requests_all_and_skips_total_count(
    tools, cognite_client_mock, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.list.return_value = [
        dumpable_factory({"externalId": "n1"})
    ]

    payload = json.loads(tools["list_instances"](**view_args, limit=-1))

    assert payload["count"] == 1
    kwargs = cognite_client_mock.data_modeling.instances.list.call_args.kwargs
    assert kwargs["limit"] is None
    cognite_client_mock.data_modeling.instances.aggregate.assert_not_called()


def test_list_instances_ignores_total_count_failures_and_still_returns_items(
    tools, cognite_client_mock, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.list.return_value = [
        dumpable_factory({"externalId": "n1"})
    ]
    cognite_client_mock.data_modeling.instances.aggregate.side_effect = RuntimeError(
        "aggregation failed"
    )

    payload = json.loads(tools["list_instances"](**view_args, limit=1))

    assert payload["count"] == 1
    assert "total" not in payload


def test_search_instances_parses_properties_and_forwards_operator(
    tools, cognite_client_mock, sample_config, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.search.return_value = [
        dumpable_factory({"externalId": "n1"})
    ]

    payload = json.loads(
        tools["search_instances"](
            **view_args,
            query="pump",
            properties='["name", "description"]',
            filter={"exists": {"property": ["node", "space"]}},
            operator="AND",
        )
    )

    assert payload["count"] == 1
    kwargs = cognite_client_mock.data_modeling.instances.search.call_args.kwargs
    assert kwargs["properties"] == ["name", "description"]
    assert kwargs["operator"] == "AND"
    assert kwargs["space"] == sample_config.instance_spaces


def test_aggregate_instances_rejects_unknown_aggregate(tools, view_args) -> None:
    result = json.loads(
        tools["aggregate_instances"](**view_args, aggregate="median", property="externalId")
    )
    assert "Unknown aggregate" in result["error"]


def test_aggregate_instances_histogram_requires_interval(tools, view_args) -> None:
    result = json.loads(
        tools["aggregate_instances"](**view_args, aggregate="histogram", property="priority")
    )
    assert "histogram requires 'property' and 'histogram_interval'" in result["error"]


def test_aggregate_instances_group_by_empty_results_provides_hint(
    tools, cognite_client_mock, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.aggregate.side_effect = [
        [],
        SimpleNamespace(value=3),
    ]

    payload = json.loads(
        tools["aggregate_instances"](
            **view_args, aggregate="count", property="externalId", group_by="status"
        )
    )

    assert payload["results"] == []
    assert "group_by 'status' returned no results" in payload["hint"]
    assert cognite_client_mock.data_modeling.instances.aggregate.call_count == 2


def test_aggregate_instances_requires_property_for_non_count_ops(tools, view_args) -> None:
    payload = json.loads(tools["aggregate_instances"](**view_args, aggregate="sum"))
    assert payload["error"] == "'sum' requires 'property'"


def test_aggregate_instances_supports_histogram_and_multi_group_by(
    tools, cognite_client_mock, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.aggregate.return_value = [SimpleNamespace(value=2)]

    payload = json.loads(
        tools["aggregate_instances"](
            **view_args,
            aggregate="histogram",
            property="priority",
            histogram_interval=2.5,
            group_by="status, type",
        )
    )

    kwargs = cognite_client_mock.data_modeling.instances.aggregate.call_args.kwargs
    assert kwargs["group_by"] == ["status", "type"]
    assert kwargs["aggregates"].property == "priority"
    assert kwargs["aggregates"].interval == 2.5
    assert payload[0]["value"] == 2


def test_aggregate_instances_group_by_zero_count_hint(
    tools, cognite_client_mock, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.aggregate.side_effect = [
        [],
        SimpleNamespace(value=0),
    ]

    payload = json.loads(
        tools["aggregate_instances"](
            **view_args, aggregate="count", property="externalId", group_by="status"
        )
    )

    assert "no instances match the provided filter" in payload["hint"]


def test_aggregate_instances_group_by_diagnostic_failure_falls_back(
    tools, cognite_client_mock, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.aggregate.side_effect = [[], RuntimeError("boom")]

    payload = json.loads(
        tools["aggregate_instances"](
            **view_args, aggregate="count", property="externalId", group_by="status"
        )
    )

    assert payload == []


def test_retrieve_instances_parses_ids_scopes_view_and_merges_outputs(
    tools, cognite_client_mock, dumpable_factory, view_args
) -> None:
    cognite_client_mock.data_modeling.instances.retrieve.return_value = SimpleNamespace(
        nodes=[dumpable_factory({"externalId": "n1"})],
        edges=[dumpable_factory({"externalId": "e1"})],
    )

    payload = json.loads(
        tools["retrieve_instances"](
            nodes='[{"space": "space-a", "externalId": "n1"}]',
            edges=[{"space": "space-a", "externalId": "e1"}],
            **view_args,
        )
    )

    kwargs = cognite_client_mock.data_modeling.instances.retrieve.call_args.kwargs
    assert kwargs["nodes"][0].space == "space-a"
    assert kwargs["edges"][0].external_id == "e1"
    assert kwargs["sources"][0].external_id == "MyView"
    assert payload["count"] == 2


def test_retrieve_instances_supports_edges_only_without_view_scope(
    tools, cognite_client_mock, dumpable_factory
) -> None:
    cognite_client_mock.data_modeling.instances.retrieve.return_value = SimpleNamespace(
        nodes=[],
        edges=[dumpable_factory({"externalId": "e-only"})],
    )

    payload = json.loads(
        tools["retrieve_instances"](edges='[{"space": "space-a", "externalId": "e-only"}]')
    )

    kwargs = cognite_client_mock.data_modeling.instances.retrieve.call_args.kwargs
    assert kwargs["nodes"] is None
    assert kwargs["edges"][0].external_id == "e-only"
    assert kwargs["sources"] is None
    assert payload["count"] == 1
