from unittest.mock import (
    patch,
)

import pytest

from blends.models import (
    Graph,
    Language,
    NId,
    SyntaxGraph,
)
from blends.stack.node_helpers import (
    scope_node_attributes,
)
from blends.stack.node_kinds import (
    StackGraphNodeKind,
)
from blends.stack.scope_types import ScopeType
from blends.stack.view import (
    StackGraphView,
)
from blends.syntax.builders.class_decl import (
    build_class_node,
)
from blends.syntax.builders.file import (
    build_file_node,
)
from blends.syntax.builders.method_declaration import (
    build_method_declaration_node,
)
from blends.syntax.builders.symbol_lookup import (
    build_symbol_lookup_node,
)
from blends.syntax.builders.variable_declaration import (
    build_variable_declaration_node,
)
from blends.syntax.models import (
    SyntaxGraphArgs,
    SyntaxMetadata,
)


def _make_args(
    *,
    syntax_graph: SyntaxGraph,
    n_id: NId,
    metadata: SyntaxMetadata,
    language: Language = Language.PYTHON,
) -> SyntaxGraphArgs:
    ast_graph = Graph()

    def generic(args: SyntaxGraphArgs) -> NId:
        return args.n_id

    return SyntaxGraphArgs(
        generic=generic,
        path="test.py",
        language=language,
        ast_graph=ast_graph,
        syntax_graph=syntax_graph,
        n_id=n_id,
        metadata=metadata,
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_extracts_reference_to_definition_edges() -> None:
    syntax_graph = SyntaxGraph()
    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": ["1"], "next_synthetic_id": 100}
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node("1", scope_node_attributes(is_exported=True))

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        def_args = _make_args(syntax_graph=syntax_graph, n_id="2", metadata=metadata)
        build_variable_declaration_node(
            def_args,
            variable_name="x",
            variable_type=None,
            value_id=None,
        )

        ref_args = _make_args(syntax_graph=syntax_graph, n_id="3", metadata=metadata)
        build_symbol_lookup_node(ref_args, symbol="x")

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    file_index = view.nid_to_index["1"]
    def_index = view.nid_to_index["2"]
    ref_index = view.nid_to_index["3"]

    assert view.node_kind[file_index] == StackGraphNodeKind.SCOPE.value
    assert view.node_kind[def_index] == StackGraphNodeKind.POP_SCOPED_SYMBOL.value
    assert view.node_kind[ref_index] == StackGraphNodeKind.PUSH_SCOPED_SYMBOL.value

    assert (file_index, 0) in view.outgoing[ref_index]
    assert (def_index, 1) in view.outgoing[file_index]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_extracts_nested_scope_edges() -> None:
    syntax_graph = SyntaxGraph()
    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": ["1"], "next_synthetic_id": 100}
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node("1", scope_node_attributes(is_exported=True))

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        class_args = _make_args(syntax_graph=syntax_graph, n_id="10", metadata=metadata)
        build_class_node(
            class_args,
            name="MyClass",
            block_id=None,
            attrl_ids=None,
            inherited_class=None,
            modifiers_id=None,
        )

        method_metadata: SyntaxMetadata = {
            "class_path": [],
            "scope_stack": ["1", "10"],
            "next_synthetic_id": 101,
        }
        method_args = _make_args(syntax_graph=syntax_graph, n_id="20", metadata=method_metadata)
        build_method_declaration_node(
            method_args,
            name="foo",
            direct_children={},
            list_children={},
        )

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    file_index = view.nid_to_index["1"]
    class_index = view.nid_to_index["10"]
    method_index = view.nid_to_index["20"]

    assert view.node_kind[file_index] == StackGraphNodeKind.SCOPE.value
    assert view.node_kind[class_index] == StackGraphNodeKind.SCOPE.value
    assert view.node_kind[method_index] == StackGraphNodeKind.SCOPE.value

    assert (file_index, 0) in view.outgoing[class_index]
    assert (class_index, 0) in view.outgoing[method_index]


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_marks_exported_scopes() -> None:
    syntax_graph = SyntaxGraph()
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node("1", scope_node_attributes(is_exported=True))
    syntax_graph.add_node("2", label_type="Class")
    syntax_graph.update_node("2", scope_node_attributes(is_exported=False))

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    file_index = view.nid_to_index["1"]
    class_index = view.nid_to_index["2"]

    assert file_index in view.exported_scopes
    assert class_index not in view.exported_scopes


@pytest.mark.blends_test_group("stack_unittesting")
def test_stack_graph_view_preserves_edge_precedence_values() -> None:
    syntax_graph = SyntaxGraph()
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node("1", scope_node_attributes(is_exported=True))

    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": ["1"], "next_synthetic_id": 100}
    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        def_args_a = _make_args(syntax_graph=syntax_graph, n_id="2", metadata=metadata)
        build_variable_declaration_node(
            def_args_a,
            variable_name="a",
            variable_type=None,
            value_id=None,
        )
        def_args_b = _make_args(syntax_graph=syntax_graph, n_id="3", metadata=metadata)
        build_variable_declaration_node(
            def_args_b,
            variable_name="b",
            variable_type=None,
            value_id=None,
        )

    syntax_graph.add_edge("1", "2", precedence=10)
    syntax_graph.add_edge("1", "3", precedence=1)

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    file_index = view.nid_to_index["1"]
    def_index_a = view.nid_to_index["2"]
    def_index_b = view.nid_to_index["3"]

    outgoing = set(view.outgoing[file_index])
    assert (def_index_a, 10) in outgoing
    assert (def_index_b, 1) in outgoing


@pytest.mark.blends_test_group("stack_unittesting")
def test_python_file_scope_exported_with_scope_type() -> None:
    syntax_graph = SyntaxGraph()
    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": [], "next_synthetic_id": 100}

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        file_args = _make_args(syntax_graph=syntax_graph, n_id="1", metadata=metadata)
        build_file_node(file_args, c_ids=[])

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    file_index = view.nid_to_index["1"]

    assert file_index in view.exported_scopes
    assert view.node_scope_type[file_index] == ScopeType.FILE.value


@pytest.mark.blends_test_group("stack_unittesting")
def test_python_function_scope_not_exported_with_scope_type() -> None:
    syntax_graph = SyntaxGraph()
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node(
        "1", scope_node_attributes(is_exported=True, scope_type=ScopeType.FILE)
    )
    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": ["1"], "next_synthetic_id": 100}

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        method_args = _make_args(syntax_graph=syntax_graph, n_id="10", metadata=metadata)
        build_method_declaration_node(
            method_args, name="my_func", direct_children={}, list_children={}
        )

    view = StackGraphView.from_syntax_graph(syntax_graph, path="test.py")
    method_index = view.nid_to_index["10"]

    assert method_index not in view.exported_scopes
    assert view.node_scope_type[method_index] == ScopeType.FUNCTION.value


@pytest.mark.blends_test_group("stack_unittesting")
@pytest.mark.parametrize(  # type: ignore[misc,attr-defined]
    ("access_modifiers", "expected_exported"),
    [
        ("public", True),
        (None, False),
    ],
)
def test_java_class_scope_exported_based_on_visibility(  # type: ignore[misc]
    access_modifiers: str | None,
    *,
    expected_exported: bool,
) -> None:
    syntax_graph = SyntaxGraph()
    syntax_graph.add_node("1", label_type="File")
    syntax_graph.update_node(
        "1", scope_node_attributes(is_exported=True, scope_type=ScopeType.FILE)
    )
    metadata: SyntaxMetadata = {"class_path": [], "scope_stack": ["1"], "next_synthetic_id": 100}

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        class_args = _make_args(
            syntax_graph=syntax_graph,
            n_id="10",
            metadata=metadata,
            language=Language.JAVA,
        )
        build_class_node(
            class_args,
            name="MyClass",
            block_id=None,
            attrl_ids=None,
            access_modifiers=access_modifiers,
        )

    view = StackGraphView.from_syntax_graph(syntax_graph, path="Test.java")
    class_index = view.nid_to_index["10"]

    assert (class_index in view.exported_scopes) is expected_exported
    assert view.node_scope_type[class_index] == ScopeType.CLASS.value
