"""
Module detecting state changes in assert calls
"""

from slither.core.cfg.node import Node
from slither.core.declarations.contract import Contract
from slither.core.declarations.function_contract import FunctionContract
from slither.detectors.abstract_detector import (
    AbstractDetector,
    DetectorClassification,
    DETECTOR_INFO,
)
from slither.slithir.operations.internal_call import InternalCall
from slither.utils.output import Output


def detect_assert_state_change(
    contract: Contract,
) -> list[tuple[FunctionContract, Node]]:
    """
    Detects and returns all nodes with assert calls that change contract state from within the invariant
    :param contract: Contract to detect
    :return: A list of nodes with assert calls that change contract state from within the invariant
    """

    # Create our result set.
    # List of tuples (function, node)
    results = []

    # Loop for each function and modifier.
    for function in contract.functions_declared + list(contract.modifiers_declared):
        for ir_call in function.internal_calls:
            # Detect assert() calls
            if ir_call.function.name == "assert(bool)" and (
                # Detect direct changes to state
                ir_call.node.state_variables_written
                or
                # Detect changes to state via function calls
                any(
                    ir
                    for ir in ir_call.node.irs
                    if isinstance(ir, InternalCall)
                    and ir.function
                    and ir.function.state_variables_written
                )
            ):
                results.append((function, ir_call.node))

    # Return the resulting set of nodes
    return results


class AssertStateChange(AbstractDetector):
    """
    Assert state change
    """

    ARGUMENT = "assert-state-change"
    HELP = "Assert state change"
    IMPACT = DetectorClassification.INFORMATIONAL
    CONFIDENCE = DetectorClassification.HIGH

    WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#assert-state-change"
    WIKI_TITLE = "Assert state change"
    WIKI_DESCRIPTION = """Incorrect use of `assert()`. See Solidity best [practices](https://solidity.readthedocs.io/en/latest/control-structures.html#id4)."""

    # region wiki_exploit_scenario
    WIKI_EXPLOIT_SCENARIO = """
```solidity
contract A {

  uint s_a;

  function bad() public {
    assert((s_a += 1) > 10);
  }
}
```
The assert in `bad()` increments the state variable `s_a` while checking for the condition.
"""
    # endregion wiki_exploit_scenario

    WIKI_RECOMMENDATION = """Use `require` for invariants modifying the state."""

    def _detect(self) -> list[Output]:
        """
        Detect assert calls that change state from within the invariant
        """
        results = []
        for contract in self.contracts:
            assert_state_change = detect_assert_state_change(contract)
            for func, node in assert_state_change:
                info: DETECTOR_INFO = [
                    func,
                    " has an assert() call which possibly changes state.\n",
                ]
                info += ["\t-", node, "\n"]
                info += [
                    "Consider using require() or change the invariant to not modify the state.\n"
                ]
                res = self.generate_result(info)
                results.append(res)
        return results
