import pytest

from blends.models import SyntaxGraph
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 = SyntaxGraph()
    graph.add_node("node_with_none_kind")
    graph.update_node_attribute("node_with_none_kind", STACK_GRAPH_KIND, None)

    graph.add_node("node_without_kind")

    graph.add_node("valid_node")
    graph.update_node_attribute("valid_node", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    view = StackGraphView.from_syntax_graph(graph)

    assert "valid_node" in view.nid_to_index
    assert "node_with_none_kind" not in view.nid_to_index
    assert "node_without_kind" not in view.nid_to_index


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

    view = StackGraphView.from_syntax_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 = SyntaxGraph()

    graph.add_node("10")
    graph.update_node_attribute("10", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("a")
    graph.update_node_attribute("a", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    view = StackGraphView.from_syntax_graph(graph)

    assert view.nid_to_index["2"] < view.nid_to_index["10"]
    assert view.nid_to_index["10"] < view.nid_to_index["a"]


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

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("3")
    graph.update_node_attribute("3", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("non_stack")

    graph.add_edge("2", "3", precedence=0)
    graph.add_edge("2", "non_stack", precedence=0)
    graph.add_edge("non_stack", "3", precedence=0)

    view = StackGraphView.from_syntax_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 = SyntaxGraph()

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("3")
    graph.update_node_attribute("3", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("4")
    graph.update_node_attribute("4", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_edge("2", "4", precedence=0)
    graph.add_edge("2", "3", precedence=0)

    view = StackGraphView.from_syntax_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 = SyntaxGraph()

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("3")
    graph.update_node_attribute("3", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    graph.add_node("4")
    graph.update_node_attribute("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_syntax_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 = SyntaxGraph()

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.SCOPE.value)
    graph.update_node_attribute("2", STACK_GRAPH_IS_EXPORTED, value=True)

    graph.add_node("3")
    graph.update_node_attribute("3", STACK_GRAPH_KIND, StackGraphNodeKind.SCOPE.value)
    graph.update_node_attribute("3", STACK_GRAPH_IS_EXPORTED, value=False)

    view = StackGraphView.from_syntax_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 = SyntaxGraph()
    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    view = StackGraphView.from_syntax_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_resolves_path_from_metadata_node_zero() -> None:
    graph = SyntaxGraph()
    graph.add_node("0")
    graph.update_node_attribute("0", "path", "from_metadata.py")

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.ROOT.value)

    view = StackGraphView.from_syntax_graph(graph)

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


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

    graph.add_node("2")
    graph.update_node_attribute("2", STACK_GRAPH_KIND, StackGraphNodeKind.SCOPE.value)
    graph.update_node_attribute("2", STACK_GRAPH_IS_EXPORTED, value=True)
    graph.update_node_attribute("2", STACK_GRAPH_SCOPE_TYPE, "file")

    graph.add_node("3")
    graph.update_node_attribute("3", STACK_GRAPH_KIND, StackGraphNodeKind.SCOPE.value)
    graph.update_node_attribute("3", STACK_GRAPH_IS_EXPORTED, value=False)
    graph.update_node_attribute("3", STACK_GRAPH_SCOPE_TYPE, "class")

    graph.add_node("4")
    graph.update_node_attribute("4", STACK_GRAPH_KIND, StackGraphNodeKind.SCOPE.value)
    graph.update_node_attribute("4", STACK_GRAPH_IS_EXPORTED, value=False)

    view = StackGraphView.from_syntax_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
