from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import neo4j
import neo4j.exceptions
import pytest
from api.attack_paths import database as graph_database
from api.attack_paths import views_helpers
from rest_framework.exceptions import APIException, PermissionDenied, ValidationError
from tasks.jobs.attack_paths.config import (
    PROVIDER_ELEMENT_ID_PROPERTY,
    get_provider_label,
)


def _make_neo4j_error(message, code):
    """Build a Neo4jError with the given message and code."""
    return neo4j.exceptions.Neo4jError._hydrate_neo4j(code=code, message=message)


def test_normalize_query_payload_extracts_attributes_section():
    payload = {
        "data": {
            "id": "ignored",
            "attributes": {
                "id": "aws-rds",
                "parameters": {"ip": "192.0.2.0"},
            },
        }
    }

    result = views_helpers.normalize_query_payload(payload)

    assert result == {"id": "aws-rds", "parameters": {"ip": "192.0.2.0"}}


def test_normalize_query_payload_passthrough_for_non_dict():
    sentinel = "not-a-dict"
    assert views_helpers.normalize_query_payload(sentinel) is sentinel


def test_prepare_parameters_includes_provider_and_casts(
    attack_paths_query_definition_factory,
):
    definition = attack_paths_query_definition_factory(cast_type=int)
    result = views_helpers.prepare_parameters(
        definition,
        {"limit": "5"},
        provider_uid="123456789012",
        provider_id="test-provider-id",
    )

    assert result["provider_uid"] == "123456789012"
    assert "provider_id" not in result
    assert result["limit"] == 5


@pytest.mark.parametrize(
    "provided,expected_message",
    [
        ({}, "Missing required parameter"),
        ({"limit": 10, "extra": True}, "Unknown parameter"),
    ],
)
def test_prepare_parameters_validates_names(
    attack_paths_query_definition_factory, provided, expected_message
):
    definition = attack_paths_query_definition_factory()

    with pytest.raises(ValidationError) as exc:
        views_helpers.prepare_parameters(
            definition, provided, provider_uid="1", provider_id="p1"
        )

    assert expected_message in str(exc.value)


def test_prepare_parameters_validates_cast(
    attack_paths_query_definition_factory,
):
    definition = attack_paths_query_definition_factory(cast_type=int)

    with pytest.raises(ValidationError) as exc:
        views_helpers.prepare_parameters(
            definition,
            {"limit": "not-an-int"},
            provider_uid="1",
            provider_id="p1",
        )

    assert "Invalid value" in str(exc.value)


def test_execute_query_serializes_graph(
    attack_paths_query_definition_factory,
    attack_paths_graph_stub_classes,
    sink_backend_stub,
):
    definition = attack_paths_query_definition_factory(
        id="aws-rds",
        name="RDS",
        short_description="Short desc",
        description="",
        cypher="MATCH (n) RETURN n",
        parameters=[],
    )
    parameters = {"provider_uid": "123"}

    provider_id = "test-provider-123"
    plabel = get_provider_label(provider_id)
    node = attack_paths_graph_stub_classes.Node(
        element_id="node-1",
        labels=["AWSAccount", plabel],
        properties={
            "name": "account",
            "complex": {
                "items": [
                    attack_paths_graph_stub_classes.NativeValue("value"),
                    {"nested": 1},
                ]
            },
        },
    )
    node_2 = attack_paths_graph_stub_classes.Node("node-2", ["RDSInstance", plabel], {})
    relationship = attack_paths_graph_stub_classes.Relationship(
        element_id="rel-1",
        rel_type="OWNS",
        start_node=node,
        end_node=node_2,
        properties={"weight": 1},
    )
    graph = SimpleNamespace(nodes=[node, node_2], relationships=[relationship])

    graph_result = MagicMock()
    graph_result.nodes = graph.nodes
    graph_result.relationships = graph.relationships

    database_name = "db-tenant-test-tenant-id"

    sink_backend_stub.execute_read_query.return_value = graph_result
    result = views_helpers.execute_query(
        database_name,
        definition,
        parameters,
        provider_id=provider_id,
        scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
    )

    sink_backend_stub.execute_read_query.assert_called_once_with(
        database_name, definition.cypher, parameters
    )
    assert result["nodes"][0]["id"] == "node-1"
    assert result["nodes"][0]["properties"]["complex"]["items"][0] == "value"
    assert result["relationships"][0]["label"] == "OWNS"


def test_execute_query_wraps_graph_errors(
    attack_paths_query_definition_factory,
    sink_backend_stub,
):
    definition = attack_paths_query_definition_factory(
        id="aws-rds",
        name="RDS",
        short_description="Short desc",
        description="",
        cypher="MATCH (n) RETURN n",
        parameters=[],
    )
    database_name = "db-tenant-test-tenant-id"
    parameters = {"provider_uid": "123"}

    sink_backend_stub.execute_read_query.side_effect = (
        graph_database.GraphDatabaseQueryException("boom")
    )
    with patch("api.attack_paths.views_helpers.logger") as mock_logger:
        with pytest.raises(APIException):
            views_helpers.execute_query(
                database_name,
                definition,
                parameters,
                provider_id="test-provider-123",
                scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
            )

    mock_logger.error.assert_called_once()


def test_execute_query_raises_permission_denied_on_read_only(
    attack_paths_query_definition_factory,
    sink_backend_stub,
):
    definition = attack_paths_query_definition_factory(
        id="aws-rds",
        name="RDS",
        short_description="Short desc",
        description="",
        cypher="MATCH (n) RETURN n",
        parameters=[],
    )
    database_name = "db-tenant-test-tenant-id"
    parameters = {"provider_uid": "123"}

    sink_backend_stub.execute_read_query.side_effect = (
        graph_database.WriteQueryNotAllowedException(
            message="Read query not allowed",
            code="Neo.ClientError.Statement.AccessMode",
        )
    )
    with pytest.raises(PermissionDenied):
        views_helpers.execute_query(
            database_name,
            definition,
            parameters,
            provider_id="test-provider-123",
            scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
        )


def test_serialize_graph_filters_by_provider_label(attack_paths_graph_stub_classes):
    provider_id = "provider-keep"
    plabel = get_provider_label(provider_id)
    other_label = get_provider_label("provider-other")

    node_keep = attack_paths_graph_stub_classes.Node("n1", ["AWSAccount", plabel], {})
    node_drop = attack_paths_graph_stub_classes.Node(
        "n2", ["AWSAccount", other_label], {}
    )

    rel_keep = attack_paths_graph_stub_classes.Relationship(
        "r1", "OWNS", node_keep, node_keep, {}
    )
    # Relationship connecting a kept node to a dropped node — filtered by endpoint check
    rel_drop_orphaned = attack_paths_graph_stub_classes.Relationship(
        "r2", "OWNS", node_keep, node_drop, {}
    )

    graph = SimpleNamespace(
        nodes=[node_keep, node_drop],
        relationships=[rel_keep, rel_drop_orphaned],
    )

    result = views_helpers._serialize_graph(graph, provider_id)

    assert len(result["nodes"]) == 1
    assert result["nodes"][0]["id"] == "n1"
    assert len(result["relationships"]) == 1
    assert result["relationships"][0]["id"] == "r1"


# -- serialize_graph_as_text -------------------------------------------------------


def test_serialize_graph_as_text_renders_nodes_and_relationships():
    graph = {
        "nodes": [
            {
                "id": "n1",
                "labels": ["AWSAccount"],
                "properties": {"account_id": "123456789012", "name": "prod"},
            },
            {
                "id": "n2",
                "labels": ["EC2Instance", "NetworkExposed"],
                "properties": {"name": "web-server-1", "exposed_internet": True},
            },
        ],
        "relationships": [
            {
                "id": "r1",
                "label": "RESOURCE",
                "source": "n1",
                "target": "n2",
                "properties": {},
            },
        ],
        "total_nodes": 2,
        "truncated": False,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert result.startswith("## Nodes (2)")
    assert '- AWSAccount "n1" (account_id: "123456789012", name: "prod")' in result
    assert (
        '- EC2Instance, NetworkExposed "n2" (name: "web-server-1", exposed_internet: true)'
        in result
    )
    assert "## Relationships (1)" in result
    assert '- AWSAccount "n1" -[RESOURCE]-> EC2Instance, NetworkExposed "n2"' in result
    assert "## Summary" in result
    assert "- Total nodes: 2" in result
    assert "- Truncated: false" in result


def test_serialize_graph_as_text_empty_graph():
    graph = {
        "nodes": [],
        "relationships": [],
        "total_nodes": 0,
        "truncated": False,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert "## Nodes (0)" in result
    assert "## Relationships (0)" in result
    assert "- Total nodes: 0" in result
    assert "- Truncated: false" in result


def test_serialize_graph_as_text_truncated_flag():
    graph = {
        "nodes": [{"id": "n1", "labels": ["Node"], "properties": {}}],
        "relationships": [],
        "total_nodes": 500,
        "truncated": True,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert "- Total nodes: 500" in result
    assert "- Truncated: true" in result


def test_serialize_graph_as_text_relationship_with_properties():
    graph = {
        "nodes": [
            {"id": "n1", "labels": ["AWSRole"], "properties": {"name": "role-a"}},
            {"id": "n2", "labels": ["AWSRole"], "properties": {"name": "role-b"}},
        ],
        "relationships": [
            {
                "id": "r1",
                "label": "STS_ASSUMEROLE_ALLOW",
                "source": "n1",
                "target": "n2",
                "properties": {"weight": 1, "reason": "trust-policy"},
            },
        ],
        "total_nodes": 2,
        "truncated": False,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert '-[STS_ASSUMEROLE_ALLOW (weight: 1, reason: "trust-policy")]->' in result


def test_serialize_properties_filters_internal_fields():
    properties = {
        "name": "prod",
        # Cartography metadata
        "lastupdated": 1234567890,
        "firstseen": 1234567800,
        "_module_name": "cartography:aws",
        "_module_version": "0.98.0",
        # Provider isolation
        PROVIDER_ELEMENT_ID_PROPERTY: "42:abc123",
    }

    result = views_helpers._serialize_properties(properties)

    assert result == {"name": "prod"}


def test_filter_labels_strips_dynamic_isolation_labels():
    labels = ["AWSRole", "_Tenant_abc123", "_Provider_def456", "_ProviderResource"]

    result = views_helpers._filter_labels(labels)

    assert result == ["AWSRole"]


def test_serialize_graph_as_text_node_without_properties():
    graph = {
        "nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
        "relationships": [],
        "total_nodes": 1,
        "truncated": False,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert '- AWSAccount "n1"' in result
    # No trailing parentheses when no properties
    assert '- AWSAccount "n1" (' not in result


def test_serialize_graph_as_text_complex_property_values():
    graph = {
        "nodes": [
            {
                "id": "n1",
                "labels": ["SecurityGroup"],
                "properties": {
                    "ports": [80, 443],
                    "tags": {"env": "prod"},
                    "enabled": None,
                },
            },
        ],
        "relationships": [],
        "total_nodes": 1,
        "truncated": False,
    }

    result = views_helpers.serialize_graph_as_text(graph)

    assert "ports: [80, 443]" in result
    assert 'tags: {env: "prod"}' in result
    assert "enabled: null" in result


# -- normalize_custom_query_payload ------------------------------------------------


def test_normalize_custom_query_payload_extracts_query():
    payload = {
        "data": {
            "type": "attack-paths-custom-query-run-requests",
            "attributes": {
                "query": "MATCH (n) RETURN n",
            },
        }
    }

    result = views_helpers.normalize_custom_query_payload(payload)

    assert result == {"query": "MATCH (n) RETURN n"}


def test_normalize_custom_query_payload_passthrough_for_non_dict():
    sentinel = "not-a-dict"
    assert views_helpers.normalize_custom_query_payload(sentinel) is sentinel


def test_normalize_custom_query_payload_passthrough_for_flat_dict():
    payload = {"query": "MATCH (n) RETURN n"}

    result = views_helpers.normalize_custom_query_payload(payload)

    assert result == {"query": "MATCH (n) RETURN n"}


# -- execute_custom_query ----------------------------------------------


def test_execute_custom_query_serializes_graph(
    attack_paths_graph_stub_classes,
    sink_backend_stub,
):
    provider_id = "test-provider-123"
    plabel = get_provider_label(provider_id)
    node_1 = attack_paths_graph_stub_classes.Node("node-1", ["AWSAccount", plabel], {})
    node_2 = attack_paths_graph_stub_classes.Node("node-2", ["RDSInstance", plabel], {})
    relationship = attack_paths_graph_stub_classes.Relationship(
        "rel-1", "OWNS", node_1, node_2, {}
    )

    graph_result = MagicMock()
    graph_result.nodes = [node_1, node_2]
    graph_result.relationships = [relationship]

    sink_backend_stub.execute_read_query.return_value = graph_result
    result = views_helpers.execute_custom_query(
        "db-tenant-test",
        "MATCH (n) RETURN n",
        provider_id,
        scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
    )

    sink_backend_stub.execute_read_query.assert_called_once()
    call_args = sink_backend_stub.execute_read_query.call_args[0]
    assert call_args[0] == "db-tenant-test"
    # The cypher is rewritten with the provider label injection
    assert plabel in call_args[1]
    assert len(result["nodes"]) == 2
    assert result["relationships"][0]["label"] == "OWNS"
    assert result["truncated"] is False
    assert result["total_nodes"] == 2


def test_execute_custom_query_adds_timeout_for_neptune_scan(sink_backend_stub):
    graph_result = MagicMock()
    graph_result.nodes = []
    graph_result.relationships = []
    sink_backend_stub.execute_read_query.return_value = graph_result

    with patch(
        "api.attack_paths.views_helpers.sink_module.get_backend_for_scan",
        return_value=sink_backend_stub,
    ):
        views_helpers.execute_custom_query(
            "db-tenant-test",
            "MATCH (n) RETURN n",
            "provider-1",
            scan=MagicMock(is_migrated=True, sink_backend="neptune"),
        )

    cypher = sink_backend_stub.execute_read_query.call_args[0][1]
    assert cypher.startswith("USING QUERY:TIMEOUTMILLISECONDS")


def test_execute_custom_query_raises_permission_denied_on_write(sink_backend_stub):
    sink_backend_stub.execute_read_query.side_effect = (
        graph_database.WriteQueryNotAllowedException(
            message="Read query not allowed",
            code="Neo.ClientError.Statement.AccessMode",
        )
    )
    with pytest.raises(PermissionDenied):
        views_helpers.execute_custom_query(
            "db-tenant-test",
            "CREATE (n) RETURN n",
            "provider-1",
            scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
        )


def test_execute_custom_query_wraps_graph_errors(sink_backend_stub):
    sink_backend_stub.execute_read_query.side_effect = (
        graph_database.GraphDatabaseQueryException("boom")
    )
    with patch("api.attack_paths.views_helpers.logger") as mock_logger:
        with pytest.raises(APIException):
            views_helpers.execute_custom_query(
                "db-tenant-test",
                "MATCH (n) RETURN n",
                "provider-1",
                scan=MagicMock(is_migrated=False, sink_backend="neo4j"),
            )

    mock_logger.error.assert_called_once()


# -- _truncate_graph ----------------------------------------------------------


def test_truncate_graph_no_truncation_needed():
    graph = {
        "nodes": [{"id": f"n{i}"} for i in range(5)],
        "relationships": [{"id": "r1", "source": "n0", "target": "n1"}],
        "total_nodes": 5,
        "truncated": False,
    }

    result = views_helpers._truncate_graph(graph)

    assert result["truncated"] is False
    assert result["total_nodes"] == 5
    assert len(result["nodes"]) == 5
    assert len(result["relationships"]) == 1


def test_truncate_graph_truncates_nodes_and_removes_orphan_relationships():
    with patch.object(graph_database, "MAX_CUSTOM_QUERY_NODES", 3):
        graph = {
            "nodes": [{"id": f"n{i}"} for i in range(5)],
            "relationships": [
                {"id": "r1", "source": "n0", "target": "n1"},
                {"id": "r2", "source": "n0", "target": "n4"},
                {"id": "r3", "source": "n3", "target": "n4"},
            ],
            "total_nodes": 5,
            "truncated": False,
        }

        result = views_helpers._truncate_graph(graph)

    assert result["truncated"] is True
    assert result["total_nodes"] == 5
    assert len(result["nodes"]) == 3
    assert {n["id"] for n in result["nodes"]} == {"n0", "n1", "n2"}
    # r1 kept (both endpoints in n0-n2), r2 and r3 dropped (n4 not in kept set)
    assert len(result["relationships"]) == 1
    assert result["relationships"][0]["id"] == "r1"


def test_truncate_graph_empty_graph():
    graph = {"nodes": [], "relationships": [], "total_nodes": 0, "truncated": False}

    result = views_helpers._truncate_graph(graph)

    assert result["truncated"] is False
    assert result["total_nodes"] == 0
    assert result["nodes"] == []
    assert result["relationships"] == []


# -- execute_read_query read-only enforcement ---------------------------------


@pytest.fixture
def mock_neo4j_session():
    """Install a Neo4jSink with a mocked Bolt driver into the sink factory.

    The yielded mock is the `neo4j.Session` that the Neo4jSink will obtain via
    `driver.session(...)`. Tests configure `mock_neo4j_session.execute_read`
    return values / side effects to exercise the read-mode error translation
    path on the real `Neo4jSink.execute_read_query` and `get_session` code.
    """
    from api.attack_paths.sink import factory
    from api.attack_paths.sink.neo4j import Neo4jSink

    mock_session = MagicMock(spec=neo4j.Session)
    mock_driver = MagicMock(spec=neo4j.Driver)
    mock_driver.session.return_value = mock_session

    sink = Neo4jSink()
    sink._driver = mock_driver

    previous_backend = factory._backend
    previous_secondary = dict(factory._secondary_backends)
    factory._backend = sink
    factory._secondary_backends.clear()
    try:
        yield mock_session
    finally:
        factory._backend = previous_backend
        factory._secondary_backends.clear()
        factory._secondary_backends.update(previous_secondary)


def test_execute_read_query_succeeds_with_select(mock_neo4j_session):
    mock_graph = MagicMock(spec=neo4j.graph.Graph)
    mock_neo4j_session.execute_read.return_value = mock_graph

    result = graph_database.execute_read_query(
        database="test-db",
        cypher="MATCH (n:AWSAccount) RETURN n LIMIT 10",
    )

    assert result is mock_graph


def test_execute_read_query_rejects_create(mock_neo4j_session):
    mock_neo4j_session.execute_read.side_effect = _make_neo4j_error(
        "Writing in read access mode not allowed",
        "Neo.ClientError.Statement.AccessMode",
    )

    with pytest.raises(graph_database.WriteQueryNotAllowedException):
        graph_database.execute_read_query(
            database="test-db",
            cypher="CREATE (n:Node {name: 'test'}) RETURN n",
        )


def test_execute_read_query_rejects_update(mock_neo4j_session):
    mock_neo4j_session.execute_read.side_effect = _make_neo4j_error(
        "Writing in read access mode not allowed",
        "Neo.ClientError.Statement.AccessMode",
    )

    with pytest.raises(graph_database.WriteQueryNotAllowedException):
        graph_database.execute_read_query(
            database="test-db",
            cypher="MATCH (n:Node) SET n.name = 'updated' RETURN n",
        )


def test_execute_read_query_rejects_delete(mock_neo4j_session):
    mock_neo4j_session.execute_read.side_effect = _make_neo4j_error(
        "Writing in read access mode not allowed",
        "Neo.ClientError.Statement.AccessMode",
    )

    with pytest.raises(graph_database.WriteQueryNotAllowedException):
        graph_database.execute_read_query(
            database="test-db",
            cypher="MATCH (n:Node) DELETE n",
        )


@pytest.mark.parametrize(
    "cypher",
    [
        "CALL apoc.create.vNode(['Label'], {name: 'test'}) YIELD node RETURN node",
        "MATCH (a)-[r]->(b) CALL apoc.create.vRelationship(a, 'REL', {}, b) YIELD rel RETURN rel",
    ],
    ids=["apoc.create.vNode", "apoc.create.vRelationship"],
)
def test_execute_read_query_succeeds_with_apoc_virtual_create(
    mock_neo4j_session, cypher
):
    mock_graph = MagicMock(spec=neo4j.graph.Graph)
    mock_neo4j_session.execute_read.return_value = mock_graph

    result = graph_database.execute_read_query(database="test-db", cypher=cypher)

    assert result is mock_graph


@pytest.mark.parametrize(
    "cypher",
    [
        "CALL apoc.create.node(['Label'], {name: 'test'}) YIELD node RETURN node",
        "MATCH (a), (b) CALL apoc.create.relationship(a, 'REL', {}, b) YIELD rel RETURN rel",
    ],
    ids=["apoc.create.Node", "apoc.create.Relationship"],
)
def test_execute_read_query_rejects_apoc_real_create(mock_neo4j_session, cypher):
    mock_neo4j_session.execute_read.side_effect = _make_neo4j_error(
        "There is no procedure with the name `apoc.create.node` registered",
        "Neo.ClientError.Procedure.ProcedureNotFound",
    )

    with pytest.raises(graph_database.WriteQueryNotAllowedException):
        graph_database.execute_read_query(database="test-db", cypher=cypher)


# -- get_cartography_schema ---------------------------------------------------


@pytest.fixture
def mock_schema_session():
    """Mock the routed sink backend session for cartography schema tests."""
    mock_result = MagicMock()
    mock_session = MagicMock()
    mock_session.run.return_value = mock_result
    mock_backend = MagicMock()

    with patch(
        "api.attack_paths.views_helpers.sink_module.get_backend_for_scan",
        return_value=mock_backend,
    ):
        mock_backend.get_session.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_backend.get_session.return_value.__exit__ = MagicMock(return_value=False)
        yield mock_session, mock_result


def test_get_cartography_schema_returns_urls(mock_schema_session):
    mock_session, mock_result = mock_schema_session
    mock_result.single.return_value = {
        "module_name": "cartography:aws",
        "module_version": "0.129.0",
    }

    result = views_helpers.get_cartography_schema(
        "db-tenant-test", "provider-123", MagicMock(sink_backend="neo4j")
    )

    mock_session.run.assert_called_once()
    assert result["id"] == "aws-0.129.0"
    assert result["provider"] == "aws"
    assert result["cartography_version"] == "0.129.0"
    assert "0.129.0" in result["schema_url"]
    assert "/aws/" in result["schema_url"]
    assert "raw.githubusercontent.com" in result["raw_schema_url"]
    assert "/aws/" in result["raw_schema_url"]


def test_get_cartography_schema_returns_none_when_no_data(mock_schema_session):
    _, mock_result = mock_schema_session
    mock_result.single.return_value = None

    result = views_helpers.get_cartography_schema(
        "db-tenant-test", "provider-123", MagicMock(sink_backend="neo4j")
    )

    assert result is None


@pytest.mark.parametrize(
    "module_name,expected_provider",
    [
        ("cartography:aws", "aws"),
        ("cartography:azure", "azure"),
        ("cartography:gcp", "gcp"),
    ],
)
def test_get_cartography_schema_extracts_provider(
    mock_schema_session, module_name, expected_provider
):
    _, mock_result = mock_schema_session
    mock_result.single.return_value = {
        "module_name": module_name,
        "module_version": "1.0.0",
    }

    result = views_helpers.get_cartography_schema(
        "db-tenant-test", "provider-123", MagicMock(sink_backend="neo4j")
    )

    assert result["id"] == f"{expected_provider}-1.0.0"
    assert result["provider"] == expected_provider


def test_get_cartography_schema_wraps_database_error():
    mock_backend = MagicMock()
    mock_backend.get_session.side_effect = graph_database.GraphDatabaseQueryException(
        "boom"
    )
    with (
        patch(
            "api.attack_paths.views_helpers.sink_module.get_backend_for_scan",
            return_value=mock_backend,
        ),
        patch("api.attack_paths.views_helpers.logger") as mock_logger,
    ):
        with pytest.raises(APIException):
            views_helpers.get_cartography_schema(
                "db-tenant-test", "provider-123", MagicMock(sink_backend="neo4j")
            )

    mock_logger.error.assert_called_once()
