import pytest

from blends.models import StackGraph
from blends.stack.attributes import (
    STACK_GRAPH_IS_DEFINITION,
    STACK_GRAPH_IS_EXPORTED,
    STACK_GRAPH_IS_REFERENCE,
    STACK_GRAPH_KIND,
    STACK_GRAPH_PRECEDENCE,
    STACK_GRAPH_SCOPE,
    STACK_GRAPH_SYMBOL,
)
from blends.stack.node_kinds import (
    StackGraphNodeKind,
)
from blends.stack.validation import (
    validate_stack_graph_graph,
    validate_stack_graph_node,
)


def _g(**attrs: object) -> StackGraph:
    graph = StackGraph()
    graph.add_node(1, **attrs)
    return graph


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_push_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_pop_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "bar",
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 5,
            STACK_GRAPH_IS_EXPORTED: False,
            STACK_GRAPH_IS_DEFINITION: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_push_scoped_symbol_node() -> None:
    graph = StackGraph()
    graph.add_node(
        1,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "baz",
            STACK_GRAPH_SCOPE: 100,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
            STACK_GRAPH_IS_REFERENCE: True,
        },
    )
    graph.add_node(100)

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_scope_node_exported() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_scope_node_not_exported() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_root_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.ROOT.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_drop_scopes_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.DROP_SCOPES.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_jump_to_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.JUMP_TO.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_valid_pop_scoped_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "qux",
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_PRECEDENCE: 0,
            STACK_GRAPH_IS_EXPORTED: False,
            STACK_GRAPH_IS_DEFINITION: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_missing_symbol_on_push_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Missing symbol" in errors[0]
    assert "1" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_empty_symbol_on_push_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "",
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Missing symbol" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_missing_scope_on_push_scoped_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_SCOPE: None,
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Missing scope" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_missing_is_exported_on_scope_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_IS_EXPORTED: None,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Missing is_exported" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_symbol_on_scope_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.SCOPE.value,
            STACK_GRAPH_SYMBOL: "unexpected",
            STACK_GRAPH_IS_EXPORTED: False,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected symbol" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_scope_on_push_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_SCOPE: "scope_1",
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected scope" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_is_exported_on_push_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_IS_EXPORTED: True,
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected is_exported" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_missing_symbol_on_pop_scoped_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_IS_DEFINITION: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Missing symbol" in errors[0]
    assert "1" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_symbol_on_drop_scopes_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.DROP_SCOPES.value,
            STACK_GRAPH_SYMBOL: "unexpected",
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected symbol" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_symbol_on_jump_to_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.JUMP_TO.value,
            STACK_GRAPH_SYMBOL: "unexpected",
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected symbol" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_scope_on_drop_scopes_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.DROP_SCOPES.value,
            STACK_GRAPH_SCOPE: "scope_1",
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected scope" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_scope_on_jump_to_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.JUMP_TO.value,
            STACK_GRAPH_SCOPE: "scope_1",
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected scope" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_is_exported_on_drop_scopes_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.DROP_SCOPES.value,
            STACK_GRAPH_IS_EXPORTED: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected is_exported" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_is_exported_on_jump_to_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.JUMP_TO.value,
            STACK_GRAPH_IS_EXPORTED: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected is_exported" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unexpected_is_exported_on_pop_scoped_symbol_node() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_IS_EXPORTED: True,
            STACK_GRAPH_IS_DEFINITION: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unexpected is_exported" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_invalid_precedence() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_PRECEDENCE: "not_an_int",
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Invalid precedence" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_unknown_kind() -> None:
    graph = _g(**{STACK_GRAPH_KIND: "UnknownNodeKind"})

    errors = validate_stack_graph_node(graph, 1)

    assert len(errors) == 1
    assert "Unknown stack graph kind" in errors[0]
    assert "UnknownNodeKind" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_returns_error_list() -> None:
    graph = _g(
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_SCOPE: "unexpected",
            STACK_GRAPH_IS_EXPORTED: True,
            STACK_GRAPH_IS_REFERENCE: True,
        }
    )

    errors = validate_stack_graph_node(graph, 1)

    assert isinstance(errors, list)
    assert len(errors) == 3  # noqa: PLR2004
    assert all(isinstance(error, str) for error in errors)


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_ignores_nodes_without_kind() -> None:
    graph = StackGraph()
    graph.add_node(1, label_type="SomeNode")

    errors = validate_stack_graph_node(graph, 1)

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_stack_graph_graph_all_nodes() -> None:
    graph = StackGraph()
    graph.add_node(
        1,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_IS_REFERENCE: True,
        },
    )
    graph.add_node(
        2,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SYMBOL.value,
            STACK_GRAPH_SYMBOL: None,
            STACK_GRAPH_IS_DEFINITION: True,
        },
    )
    graph.add_node(3)

    errors = validate_stack_graph_graph(graph)

    assert len(errors) == 1
    assert "Missing symbol" in errors[0]
    assert "2" in errors[0]


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_stack_graph_graph_specific_nodes() -> None:
    graph = StackGraph()
    graph.add_node(
        1,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "foo",
            STACK_GRAPH_IS_REFERENCE: True,
        },
    )
    graph.add_node(
        2,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SYMBOL.value,
            STACK_GRAPH_SYMBOL: None,
        },
    )

    errors = validate_stack_graph_graph(graph, node_ids=[1])

    assert errors == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_validate_stack_graph_graph_with_new_node_kinds() -> None:
    graph = StackGraph()
    graph.add_node(1, **{STACK_GRAPH_KIND: StackGraphNodeKind.DROP_SCOPES.value})
    graph.add_node(2, **{STACK_GRAPH_KIND: StackGraphNodeKind.JUMP_TO.value})
    graph.add_node(
        3,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.POP_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "bar",
            STACK_GRAPH_IS_DEFINITION: True,
        },
    )
    graph.add_node(
        4,
        **{
            STACK_GRAPH_KIND: StackGraphNodeKind.PUSH_SCOPED_SYMBOL.value,
            STACK_GRAPH_SYMBOL: "baz",
            STACK_GRAPH_SCOPE: 100,
            STACK_GRAPH_IS_REFERENCE: True,
        },
    )
    graph.add_node(100)

    errors = validate_stack_graph_graph(graph)

    assert errors == []
