from unittest.mock import (
    patch,
)

import pytest

from blends.models import (
    Graph,
    Language,
    NId,
    SyntaxGraph,
)
from blends.stack.node_kinds import (
    StackGraphNodeKind,
)
from blends.syntax.builders.parameter import (
    build_parameter_node,
)
from blends.syntax.models import (
    SyntaxGraphArgs,
    SyntaxMetadata,
)


@pytest.fixture  # type: ignore[misc,attr-defined]
def mock_args() -> SyntaxGraphArgs:  # type: ignore[misc]
    syntax_graph = SyntaxGraph()
    ast_graph = Graph()
    metadata: SyntaxMetadata = {
        "class_path": [],
        "scope_stack": ["1"],
    }
    syntax_graph.add_node("1", label_type="File")

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

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


@pytest.mark.blends_test_group("stack_unittesting")
def test_parameter_emits_pop_symbol_and_scope_edge(
    mock_args: SyntaxGraphArgs,
) -> None:
    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(
            args=mock_args,
            variable="x",
            variable_type=None,
            value_id=None,
        )

        assert (
            mock_args.syntax_graph.nodes["2"]["stack_graph_kind"]
            == StackGraphNodeKind.POP_SCOPED_SYMBOL.value
        )
        assert mock_args.syntax_graph.nodes["2"]["stack_graph_symbol"] == "x"

        assert mock_args.syntax_graph.has_edge("1", "2")
        edge_data = mock_args.syntax_graph["1"]["2"]
        assert edge_data["precedence"] == 1


@pytest.mark.blends_test_group("stack_unittesting")
def test_parameter_no_stack_graph_emission_when_feature_preview_disabled(
    mock_args: SyntaxGraphArgs,
) -> None:
    with patch("blends.ctx.ctx.has_feature_flag", return_value=False):
        build_parameter_node(
            args=mock_args,
            variable="x",
            variable_type=None,
            value_id=None,
        )

        assert "stack_graph_kind" not in mock_args.syntax_graph.nodes["2"]
        assert "stack_graph_symbol" not in mock_args.syntax_graph.nodes["2"]
        assert not mock_args.syntax_graph.has_edge("1", "2")


@pytest.mark.blends_test_group("stack_unittesting")
def test_parameter_no_scope_edge_when_scope_stack_empty(
    mock_args: SyntaxGraphArgs,
) -> None:
    mock_args.metadata["scope_stack"] = []
    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(
            args=mock_args,
            variable="x",
            variable_type=None,
            value_id=None,
        )

        assert (
            mock_args.syntax_graph.nodes["2"]["stack_graph_kind"]
            == StackGraphNodeKind.POP_SCOPED_SYMBOL.value
        )
        assert mock_args.syntax_graph.nodes["2"]["stack_graph_symbol"] == "x"
        assert not mock_args.syntax_graph.has_edge("1", "2")


def _dot_gateway_nodes(args: SyntaxGraphArgs) -> list[str]:
    return [
        nid
        for nid in args.syntax_graph.nodes
        if args.syntax_graph.nodes[nid].get("label_type") == "SyntheticDotGateway"
    ]


@pytest.mark.blends_test_group("stack_unittesting")
def test_cls_without_classmethod_flag_does_not_get_dot_gateway(
    mock_args: SyntaxGraphArgs,
) -> None:
    mock_args.syntax_graph.add_node("class_scope", label_type="Class")
    mock_args.metadata["class_member_scope_stack"] = [("class_scope", "MyClass")]

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(args=mock_args, variable="cls", variable_type=None, value_id=None)

    assert _dot_gateway_nodes(mock_args) == [], (
        "cls without @classmethod metadata must not create a SyntheticDotGateway"
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_cls_with_classmethod_flag_gets_dot_gateway(
    mock_args: SyntaxGraphArgs,
) -> None:
    mock_args.syntax_graph.add_node("class_scope", label_type="Class")
    mock_args.metadata["class_member_scope_stack"] = [("class_scope", "MyClass")]
    mock_args.metadata["is_classmethod"] = True
    mock_args.metadata["next_synthetic_id"] = 100

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(args=mock_args, variable="cls", variable_type=None, value_id=None)

    gateways = _dot_gateway_nodes(mock_args)
    assert len(gateways) == 1, "cls with @classmethod must create exactly one SyntheticDotGateway"


@pytest.mark.blends_test_group("stack_unittesting")
def test_optional_annotation_creates_dot_gateway(mock_args: SyntaxGraphArgs) -> None:
    mock_args.syntax_graph.add_node("class_scope", label_type="Class")
    mock_args.metadata["class_member_scope_stack"] = [("class_scope", "Foo")]
    mock_args.metadata["next_synthetic_id"] = 100

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(
            args=mock_args,
            variable="other",
            variable_type='Optional["Foo"]',
            annotation_class_names={"Foo"},
            value_id=None,
        )

    assert len(_dot_gateway_nodes(mock_args)) == 1


@pytest.mark.blends_test_group("stack_unittesting")
def test_union_none_annotation_creates_dot_gateway(mock_args: SyntaxGraphArgs) -> None:
    mock_args.syntax_graph.add_node("class_scope", label_type="Class")
    mock_args.metadata["class_member_scope_stack"] = [("class_scope", "Foo")]
    mock_args.metadata["next_synthetic_id"] = 100

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(
            args=mock_args,
            variable="other",
            variable_type="Foo | None",
            annotation_class_names={"Foo"},
            value_id=None,
        )

    assert len(_dot_gateway_nodes(mock_args)) == 1


@pytest.mark.blends_test_group("stack_unittesting")
def test_qualified_name_annotation_creates_dot_gateway(mock_args: SyntaxGraphArgs) -> None:
    mock_args.syntax_graph.add_node("class_scope", label_type="Class")
    mock_args.metadata["class_member_scope_stack"] = [("class_scope", "Foo")]
    mock_args.metadata["next_synthetic_id"] = 100

    with patch("blends.ctx.ctx.has_feature_flag", side_effect=lambda flag: flag == "StackGraph"):
        build_parameter_node(
            args=mock_args,
            variable="other",
            variable_type="pkg.Foo",
            annotation_class_names={"Foo"},
            value_id=None,
        )

    assert len(_dot_gateway_nodes(mock_args)) == 1
