import pytest

from blends.ctx import ctx
from blends.models import NId, SyntaxGraph
from blends.symbolic_evaluation.utils import (
    get_backward_paths,
    get_current_class,
    get_forward_paths,
    get_lookup_path,
    get_object_identifiers,
    iter_backward_paths,
    iter_forward_paths,
)


def add_ast_edge(graph: SyntaxGraph, src: NId, dst: NId) -> None:
    graph.add_edge(src, dst, label_ast="AST")


def add_cfg_edge(graph: SyntaxGraph, src: NId, dst: NId) -> None:
    graph.add_edge(src, dst, label_cfg="CFG")


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_backward_paths_single_node() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")

    paths = list(iter_backward_paths(graph, 1))

    assert paths == [[1]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_backward_paths_linear_chain() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    add_cfg_edge(graph, 2, 3)

    paths = list(iter_backward_paths(graph, 3))

    assert paths == [[3, 2, 1]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_backward_paths_branching_parents() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 3)
    add_cfg_edge(graph, 2, 3)

    paths = list(iter_backward_paths(graph, 3))

    assert paths == [[3, 1], [3, 2]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_backward_paths_uses_cfg_parent_from_ast() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="Literal")
    add_ast_edge(graph, 2, 3)
    add_cfg_edge(graph, 1, 2)

    paths = list(get_backward_paths(graph, 3))

    assert paths == [[2, 1]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_backward_paths_respects_recursion_limit() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    add_cfg_edge(graph, 2, 3)
    original_limit = ctx.recursion_limit
    ctx.recursion_limit = 1
    try:
        paths = list(get_backward_paths(graph, 3))
    finally:
        ctx.recursion_limit = original_limit

    assert paths == []


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_backward_paths_handles_stop_iteration() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    original_limit = ctx.recursion_limit
    ctx.recursion_limit = 3
    try:
        paths = list(get_backward_paths(graph, 2))
    finally:
        ctx.recursion_limit = original_limit

    assert paths == [[2, 1]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_forward_paths_single_node() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")

    paths = list(iter_forward_paths(graph, 1))

    assert paths == [[1]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_forward_paths_linear_chain() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    add_cfg_edge(graph, 2, 3)

    paths = list(iter_forward_paths(graph, 1))

    assert paths == [[1, 2, 3]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_iter_forward_paths_branching_children() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    add_cfg_edge(graph, 1, 3)

    paths = list(iter_forward_paths(graph, 1))

    assert paths == [[1, 2], [1, 3]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_forward_paths_uses_cfg_parent_from_ast() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="ExpressionStatement")
    graph.add_node(2, label_type="ExpressionStatement")
    graph.add_node(3, label_type="Literal")
    add_ast_edge(graph, 2, 3)
    add_cfg_edge(graph, 1, 2)

    paths = list(get_forward_paths(graph, 3))

    assert paths == [[2]]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_lookup_path_returns_tail_after_cfg_parent() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="SymbolLookup", symbol="x")
    graph.add_node(2, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    path = [1, 9, 10]

    result = get_lookup_path(graph, path, 1)

    assert result == [9, 10]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_lookup_path_returns_empty_when_parent_last() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="SymbolLookup", symbol="x")
    graph.add_node(2, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    path = [9, 1]

    result = get_lookup_path(graph, path, 1)

    assert result == []


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_lookup_path_raises_when_parent_missing() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="SymbolLookup", symbol="x")
    graph.add_node(2, label_type="ExpressionStatement")
    add_cfg_edge(graph, 1, 2)
    path = [9, 10]

    with pytest.raises(ValueError, match="not in list"):
        get_lookup_path(graph, path, 1)


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_object_identifiers_returns_variable_names() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="VariableDeclaration", variable="client")
    graph.add_node(2, label_type="ObjectCreation", name="HttpClient")
    add_ast_edge(graph, 1, 2)

    result = get_object_identifiers(graph, {"HttpClient"})

    assert result == ["client"]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_object_identifiers_ignores_unlisted_name() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="VariableDeclaration", variable="client")
    graph.add_node(2, label_type="ObjectCreation", name="HttpClient")
    add_ast_edge(graph, 1, 2)

    result = get_object_identifiers(graph, {"OtherClient"})

    assert result == []


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_object_identifiers_ignores_non_variable_declaration_parent() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="Literal")
    graph.add_node(2, label_type="ObjectCreation", name="HttpClient")
    add_ast_edge(graph, 1, 2)

    result = get_object_identifiers(graph, {"HttpClient"})

    assert result == []


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_object_identifiers_ignores_missing_variable_name() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="VariableDeclaration")
    graph.add_node(2, label_type="ObjectCreation", name="HttpClient")
    add_ast_edge(graph, 1, 2)

    result = get_object_identifiers(graph, {"HttpClient"})

    assert result == []


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_current_class_returns_class_parent() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="Class")
    graph.add_node(2, label_type="MethodDeclaration")
    graph.add_node(3, label_type="Literal")
    add_ast_edge(graph, 1, 2)
    add_ast_edge(graph, 2, 3)

    result = get_current_class(graph, 3)

    assert result == 1


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_get_current_class_returns_self_when_no_class_parent() -> None:
    graph = SyntaxGraph()
    graph.add_node(1, label_type="MethodDeclaration")
    graph.add_node(2, label_type="Literal")
    add_ast_edge(graph, 1, 2)

    result = get_current_class(graph, 2)

    assert result == 2  # noqa: PLR2004
