import pytest

from blends.ctx import ctx
from blends.models import NId, SyntaxGraph
from blends.symbolic_evaluation import evaluate
from blends.symbolic_evaluation.dispatcher import generic
from blends.symbolic_evaluation.models import (
    MissingSymbolicEvalError,
    SymbolicEvalArgs,
    SymbolicEvaluation,
)
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: str, dst: str) -> None:
    graph.add_edge(src, dst, label_ast="AST")


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


def base_args(graph: SyntaxGraph, n_id: NId, path: list[str]) -> SymbolicEvalArgs:
    return SymbolicEvalArgs(
        evaluation={},
        graph=graph,
        path=path,
        n_id=n_id,
        triggers=set(),
        graph_db=None,
        method_evaluators=None,
    )


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_generic_missing_evaluator_raises() -> None:
    graph = SyntaxGraph()
    graph.add_node("1", label_type="UnknownType")
    args = base_args(graph, "1", ["1"])

    with pytest.raises(MissingSymbolicEvalError):
        generic(args)


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

    result = evaluate(graph, ["1"], "1")

    assert result is None


@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_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_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"


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_symbolic_eval_args_fork_n_id_preserves_state() -> None:
    graph = SyntaxGraph()
    evaluation = {"1": True}
    triggers = {"source"}
    args = SymbolicEvalArgs(
        evaluation=evaluation,
        graph=graph,
        path=["1", "2"],
        n_id="1",
        triggers=triggers,
        graph_db=None,
        method_evaluators=None,
    )

    forked = args.fork_n_id("2")

    assert forked.n_id == "2"
    assert forked.path is args.path
    assert forked.graph is args.graph
    assert forked.evaluation is args.evaluation
    assert forked.triggers is args.triggers


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_symbolic_eval_args_fork_n_id_overrides_graph_and_path() -> None:
    graph = SyntaxGraph()
    next_graph = SyntaxGraph()
    args = SymbolicEvalArgs(
        evaluation={},
        graph=graph,
        path=["1"],
        n_id="1",
        triggers=set(),
        graph_db=None,
        method_evaluators=None,
    )

    forked = args.fork_n_id("2", path=["2"], graph=next_graph)

    assert forked.n_id == "2"
    assert forked.path == ["2"]


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_symbolic_eval_args_fork_n_id_empty_path_uses_original() -> None:
    graph = SyntaxGraph()
    args = SymbolicEvalArgs(
        evaluation={},
        graph=graph,
        path=["1"],
        n_id="1",
        triggers=set(),
        graph_db=None,
        method_evaluators=None,
    )

    forked = args.fork_n_id("2", path=[])

    assert forked.path is args.path


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_symbolic_eval_args_fork_updates_selected_fields() -> None:
    graph = SyntaxGraph()
    args = SymbolicEvalArgs(
        evaluation={"1": False},
        graph=graph,
        path=["1"],
        n_id="1",
        triggers={"t1"},
        graph_db=None,
        method_evaluators=None,
    )

    forked = args.fork(n_id="2")

    assert forked.n_id == "2"
    assert forked.graph is args.graph
    assert forked.path is args.path
    assert forked.evaluation is args.evaluation
    assert forked.triggers is args.triggers


@pytest.mark.blends_test_group("symbolic_evaluation_unittesting")
def test_symbolic_evaluation_fields() -> None:
    evaluation = SymbolicEvaluation(danger=True, triggers={"a"})

    assert evaluation.danger is True
    assert evaluation.triggers == {"a"}
