from collections.abc import Callable
from pathlib import Path

import pytest

from blends import get_graphs_from_path
from blends.models import NId, SyntaxGraph
from blends.query import matching_nodes
from blends.symbolic_evaluation.evaluate import evaluate
from blends.symbolic_evaluation.models import (
    SymbolicEvalArgs,
    SymbolicEvaluation,
)
from blends.symbolic_evaluation.utils import get_backward_paths

TEST_DATA_DIR = Path(__file__).parent.parent / "data" / "test_files" / "symbolic_evaluation"


def make_trigger_evaluator(node_type: str) -> Callable[[SymbolicEvalArgs], SymbolicEvaluation]:
    def evaluator(args: SymbolicEvalArgs) -> SymbolicEvaluation:
        args.triggers.add(node_type)
        return SymbolicEvaluation(args.evaluation[args.n_id], args.triggers)

    return evaluator


def make_danger_evaluator(node_type: str) -> Callable[[SymbolicEvalArgs], SymbolicEvaluation]:
    def evaluator(args: SymbolicEvalArgs) -> SymbolicEvaluation:
        args.triggers.add(node_type)
        args.evaluation[args.n_id] = True
        return SymbolicEvaluation(danger=True, triggers=args.triggers)

    return evaluator


def build_method_evaluators(
    node_types: list[str],
) -> dict[str, Callable[[SymbolicEvalArgs], SymbolicEvaluation]]:
    return {node_type: make_trigger_evaluator(node_type) for node_type in node_types}


def get_syntax_graph(file_path: Path) -> SyntaxGraph:
    graphs = get_graphs_from_path(file_path)
    assert graphs.syntax_graph is not None, f"Failed to parse {file_path}"
    return graphs.syntax_graph


def evaluate_node(
    graph: SyntaxGraph,
    n_id: NId,
    method_evaluators: dict[str, Callable[[SymbolicEvalArgs], SymbolicEvaluation]] | None = None,
) -> SymbolicEvaluation | None:
    for path in get_backward_paths(graph, n_id):
        result = evaluate(graph, path, n_id, None, method_evaluators)
        if result is not None:
            return result
    return None


@pytest.mark.blends_test_group("functional")
class TestSymbolicEvaluationFunctional:
    def test_python_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "python.py")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        node_types = ["method_invocation", "member_access", "symbol_lookup"]
        evaluators = build_method_evaluators(node_types)
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers
        assert "member_access" in result.triggers
        assert "symbol_lookup" in result.triggers

    def test_python_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "python.py")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = {
            "parameter": make_danger_evaluator("parameter"),
            "method_invocation": make_trigger_evaluator("method_invocation"),
        }
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "parameter" in result.triggers

    def test_python_literal_not_dangerous(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "python.py")

        literals = matching_nodes(graph, label_type="Literal")
        assert literals, "No Literal nodes found"

        evaluators = build_method_evaluators(["literal"])
        result = evaluate_node(graph, literals[0], evaluators)

        assert result is not None
        assert result.danger is False
        assert "literal" in result.triggers

    def test_python_binary_operation_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "python.py")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers
        assert "symbol_lookup" in result.triggers

    def test_javascript_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "javascript.js")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_javascript_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "javascript.js")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_java_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "java.java")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_java_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "java.java")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_typescript_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "typescript.ts")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_typescript_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "typescript.ts")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_go_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "go.go")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_go_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "go.go")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_kotlin_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "kotlin.kt")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_kotlin_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "kotlin.kt")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_csharp_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "c_sharp.cs")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_csharp_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "c_sharp.cs")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_php_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "php.php")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_php_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "php.php")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_scala_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "scala.scala")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_scala_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "scala.scala")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_swift_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "swift.swift")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_swift_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "swift.swift")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers

    def test_dart_method_invocation_evaluation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "dart.dart")

        method_invocations = matching_nodes(graph, label_type="MethodInvocation")
        assert method_invocations, "No MethodInvocation nodes found"

        evaluators = build_method_evaluators(["method_invocation", "member_access"])
        result = evaluate_node(graph, method_invocations[0], evaluators)

        assert result is not None
        assert "method_invocation" in result.triggers

    def test_dart_danger_propagation(self) -> None:
        graph = get_syntax_graph(TEST_DATA_DIR / "dart.dart")

        binary_ops = matching_nodes(graph, label_type="BinaryOperation")
        assert binary_ops, "No BinaryOperation nodes found"

        evaluators = {
            "binary_operation": make_trigger_evaluator("binary_operation"),
            "symbol_lookup": make_danger_evaluator("symbol_lookup"),
        }
        result = evaluate_node(graph, binary_ops[0], evaluators)

        assert result is not None
        assert result.danger is True
        assert "binary_operation" in result.triggers
