import pytest

from blends.models import StackGraph
from blends.stack.attributes import (
    STACK_GRAPH_IS_EXPORTED,
    STACK_GRAPH_KIND,
    STACK_GRAPH_SCOPE_TYPE,
)
from blends.stack.node_kinds import StackGraphNodeKind
from blends.stack.view import (
    JUMP_TO_INDEX,
    ROOT_INDEX,
    Degree,
    StackGraphView,
    SymbolInterner,
)


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_ignores_nodes_with_none_kind() -> None:
    graph = StackGraph()
    graph.add_node(1, **{STACK_GRAPH_KIND: None})
    graph.add_node(2)
    graph.add_node(3, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})

    view = StackGraphView.from_stack_graph(graph)

    assert 3 in view.nid_to_index  # noqa: PLR2004
    assert 1 not in view.nid_to_index
    assert 2 not in view.nid_to_index  # noqa: PLR2004


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_singleton_indices_are_fixed() -> None:
    graph = StackGraph()

    view = StackGraphView.from_stack_graph(graph)

    assert view.node_kind[ROOT_INDEX] == StackGraphNodeKind.ROOT.value
    assert view.node_kind[JUMP_TO_INDEX] == StackGraphNodeKind.JUMP_TO.value
    assert len(view.index_to_nid) >= 2  # noqa: PLR2004


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_sorts_numeric_node_ids_by_int_value() -> None:
    graph = StackGraph()
    graph.add_node(10, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(20, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})

    view = StackGraphView.from_stack_graph(graph)

    assert view.nid_to_index[2] < view.nid_to_index[10]
    assert view.nid_to_index[10] < view.nid_to_index[20]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_includes_edges_only_between_stack_nodes() -> None:
    graph = StackGraph()
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(3, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(4)
    graph.add_edge(2, 3, precedence=0)
    graph.add_edge(2, 4, precedence=0)
    graph.add_edge(4, 3, precedence=0)

    view = StackGraphView.from_stack_graph(graph)

    source_index = view.nid_to_index[2]
    sink_index = view.nid_to_index[3]

    assert view.outgoing[source_index] == [(sink_index, 0)]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_sorts_outgoing_edges_by_sink_index() -> None:
    graph = StackGraph()
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(3, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(4, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_edge(2, 4, precedence=0)
    graph.add_edge(2, 3, precedence=0)

    view = StackGraphView.from_stack_graph(graph)

    source_index = view.nid_to_index[2]
    first_sink_index = view.nid_to_index[3]
    second_sink_index = view.nid_to_index[4]

    assert view.outgoing[source_index] == [
        (first_sink_index, 0),
        (second_sink_index, 0),
    ]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_computes_incoming_degree_summary() -> None:
    graph = StackGraph()
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(3, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_node(4, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})
    graph.add_edge(2, 3, precedence=0)
    graph.add_edge(4, 3, precedence=0)
    graph.add_edge(2, 4, precedence=0)

    view = StackGraphView.from_stack_graph(graph)

    first_source_index = view.nid_to_index[2]
    shared_sink_index = view.nid_to_index[3]
    second_source_index = view.nid_to_index[4]

    assert view.incoming_degree[first_source_index] == Degree.ZERO
    assert view.incoming_degree[second_source_index] == Degree.ONE
    assert view.incoming_degree[shared_sink_index] == Degree.MULTIPLE


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_collects_exported_scopes() -> None:
    graph = StackGraph()
    graph.add_node(
        2, **{STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value, STACK_GRAPH_IS_EXPORTED: True}
    )
    graph.add_node(
        3, **{STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value, STACK_GRAPH_IS_EXPORTED: False}
    )

    view = StackGraphView.from_stack_graph(graph)

    assert view.nid_to_index[2] in view.exported_scopes
    assert view.nid_to_index[3] not in view.exported_scopes


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_uses_path_argument_and_builds_file_maps() -> None:
    graph = StackGraph()
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})

    view = StackGraphView.from_stack_graph(graph, path="override.py")

    assert view.file_path == "override.py"
    assert "override.py" in view.file_to_nodes
    assert view.nid_to_index[2] in view.file_to_nodes["override.py"]
    assert view.node_to_file[view.nid_to_index[2]] == "override.py"
    assert view.node_to_file[JUMP_TO_INDEX] == ""


@pytest.mark.blends_test_group("stack_unittesting")
def test_symbol_interner_reuses_cached_symbol_id_zero() -> None:
    interner = SymbolInterner(symbol_to_id={}, id_to_symbol=[])

    first_id = interner.intern("x")
    second_id = interner.intern("x")

    assert first_id == 0
    assert second_id == 0
    assert interner.symbol_to_id["x"] == 0
    assert interner.id_to_symbol == ["x"]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_uses_explicit_path_parameter() -> None:
    graph = StackGraph()
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value})

    view = StackGraphView.from_stack_graph(graph, path="explicit.py")

    assert view.file_path == "explicit.py"
    assert "explicit.py" in view.file_to_nodes


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_extracts_node_scope_type() -> None:
    graph = StackGraph()
    graph.add_node(
        2,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_IS_EXPORTED: True,
            STACK_GRAPH_SCOPE_TYPE: "file",
        },
    )
    graph.add_node(
        3,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_IS_EXPORTED: False,
            STACK_GRAPH_SCOPE_TYPE: "class",
        },
    )
    graph.add_node(
        4,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_IS_EXPORTED: False,
        },
    )

    view = StackGraphView.from_stack_graph(graph)

    assert view.node_scope_type[view.nid_to_index[2]] == "file"
    assert view.node_scope_type[view.nid_to_index[3]] == "class"
    assert view.node_scope_type[view.nid_to_index[4]] is None
