from typing import Any, Set

from ...error import GraphQLError
from ...language import OperationDefinitionNode, VariableDefinitionNode
from . import ValidationContext, ValidationRule

__all__ = ["NoUndefinedVariablesRule"]


class NoUndefinedVariablesRule(ValidationRule):
    """No undefined variables

    A GraphQL operation is only valid if all variables encountered, both directly and
    via fragment spreads, are defined by that operation.

    See https://spec.graphql.org/draft/#sec-All-Variable-Uses-Defined
    """

    def __init__(self, context: ValidationContext):
        super().__init__(context)
        self.defined_variable_names: Set[str] = set()

    def enter_operation_definition(self, *_args: Any) -> None:
        self.defined_variable_names.clear()

    def leave_operation_definition(
        self, operation: OperationDefinitionNode, *_args: Any
    ) -> None:
        usages = self.context.get_recursive_variable_usages(operation)
        defined_variables = self.defined_variable_names
        for usage in usages:
            node = usage.node
            var_name = node.name.value
            if var_name not in defined_variables:
                self.report_error(
                    GraphQLError(
                        (
                            f"Variable '${var_name}' is not defined"
                            f" by operation '{operation.name.value}'."
                            if operation.name
                            else f"Variable '${var_name}' is not defined."
                        ),
                        [node, operation],
                    )
                )

    def enter_variable_definition(
        self, node: VariableDefinitionNode, *_args: Any
    ) -> None:
        self.defined_variable_names.add(node.variable.name.value)
