class AutoLink(Graph):
    def __init__(self, goals: Graph, data: Optional[AutoLinkData] = None):
        super(AutoLink, self).__init__(goals)
        self.keywords: Dict[str, int] = {}
        self.back_kw: Dict[int, str] = {}
        if data:
            for goal_id, keyword in data:
                self.keywords[keyword] = goal_id
                self.back_kw[goal_id] = keyword

    def accept_ToggleAutoLink(self, command: ToggleAutoLink) -> None:
        selected_id = self.settings("selection")
        if selected_id in self.goaltree.closed:
            self.error("Autolink cannot be set for closed goals")
            return
        if selected_id == Goals.ROOT_ID:
            self.error("Autolink cannot be set for the root goal")
            return
        keyword = command.keyword.lower().strip()
        if selected_id in self.back_kw:
            self.keywords.pop(self.back_kw[selected_id])
            self.back_kw.pop(selected_id)
            self.events().append(("remove_autolink", selected_id))
        if keyword in self.keywords:
            old_id = self.keywords.pop(keyword)
            self.back_kw.pop(old_id)
            self.events().append(("remove_autolink", old_id))
        if not keyword:
            # empty keyword? exit right now
            return
        self.keywords[keyword] = selected_id
        self.back_kw[selected_id] = keyword
        self.events().append(("add_autolink", selected_id, keyword))

    def accept_ToggleClose(self, command: ToggleClose) -> None:
        selected_id = self.settings("selection")
        if selected_id in self.back_kw:
            self.keywords.pop(self.back_kw[selected_id])
            self.back_kw.pop(selected_id)
            self.events().append(("remove_autolink", selected_id))
        self.goaltree.accept(command)

    def accept_Add(self, command: Add) -> None:
        self._autolink_new_goal(command)

    def accept_Insert(self, command: Insert) -> None:
        self._autolink_new_goal(command)

    def _autolink_new_goal(self, command: Union[Add, Insert]) -> None:
        matching = self._find_matching_goals(command.name)
        ids_before = set(self.goaltree.goals.keys())
        self.goaltree.accept(command)
        ids_after = set(self.goaltree.goals.keys())
        ids_diff = ids_after.difference(ids_before)
        if ids_diff:
            added_id = ids_diff.pop()
            self._make_links(matching, added_id)

    def accept_Rename(self, command: Rename) -> None:
        matching = self._find_matching_goals(command.new_name)
        self.goaltree.accept(command)
        selected_id = command.goal_id or self.settings("selection")
        self._make_links(matching, selected_id)

    def accept_Delete(self, command: Delete) -> None:
        selected_id = command.goal_id or self.settings("selection")
        edges = self.goaltree.q("edge")
        goals_to_check = [selected_id]
        while goals_to_check:
            goal_id = goals_to_check.pop()
            goals_to_check.extend(
                e[0] for e in edges[goal_id]["edge"] if e[1] == EdgeType.PARENT
            )
            if goal_id in self.back_kw:
                added_kw = self.back_kw.pop(goal_id)
                self.keywords.pop(added_kw)
                self.events().append(("remove_autolink", goal_id))
        self.goaltree.accept(command)

    def _find_matching_goals(self, text: str) -> List[int]:
        return [goal_id for kw, goal_id in self.keywords.items() if kw in text.lower()]

    def _make_links(self, matching_goals: List[int], target_goal: int) -> None:
        if not matching_goals:
            return
        self_children: Dict[int, List[int]] = {
            goal_id: [e[0] for e in attrs["edge"]]
            for goal_id, attrs in self.goaltree.q("edge").items()
        }
        for add_to in matching_goals:
            if target_goal not in self_children[add_to]:
                self.goaltree.accept(ToggleLink(add_to, target_goal, EdgeType.BLOCKER))

    @with_key("edge")
    def q(self, keys: str = "name") -> Dict[int, Any]:
        goals = self.goaltree.q(keys)
        new_goals: Dict[int, Any] = dict(goals)
        for keyword, goal_id in self.keywords.items():
            pseudo_id = -(goal_id + 10)
            for real_goal, attrs in goals.items():
                real_edges = attrs.pop("edge")
                new_edges = [
                    e if e[0] != goal_id else (pseudo_id, e[1]) for e in real_edges
                ]
                attrs["edge"] = new_edges
                new_goals[real_goal] = attrs
            pseudo_goal: Dict[str, Any] = {"edge": [(goal_id, EdgeType.PARENT)]}
            if "name" in keys:
                pseudo_goal["name"] = f"Autolink: '{keyword}'"
            if "open" in keys:
                pseudo_goal["open"] = True
            if "select" in keys:
                pseudo_goal["select"] = None
            if "switchable" in keys:
                pseudo_goal["switchable"] = False
            new_goals[pseudo_id] = pseudo_goal
        return new_goals

    @staticmethod
    def export(goals: "AutoLink") -> AutoLinkData:
        return [(goal_id, kw) for goal_id, kw in goals.back_kw.items()] class ToggleAutoLink(Command):
    keyword: str class EdgeType(IntEnum):
    BLOCKER = 1
    PARENT = 2 class ToggleClose(Command):
    """Close an open selected goal. Re-open a closed selected goal"""

    root: int = 0 class Select(Command):
    """Select a goal by its id whether it exist. Do nothing in other case"""

    goal_id: int class Add(Command):
    """Add a new goal to the existing tree"""

    name: str
    add_to: int = 0
    edge_type: EdgeType = EdgeType.PARENT class HoldSelect(Command):
    """Saves current selection into the "previous selection" state""" class Insert(Command):
    """Add an intermediate goal between two selected goals"""

    name: str class Rename(Command):
    """Change a name of the given goal"""

    new_name: str
    goal_id: int = 0 class Graph:
    """Base interface definition"""

    NO_VALUE = "no value"

    def __init__(self, goaltree=None):
        self.goaltree: Graph = goaltree

    def __getattr__(self, item):
        """When method is not found, ask nested goaltree for it"""
        if self.goaltree != self:
            return getattr(self.goaltree, item)
        return None

    def accept(self, command: Command) -> None:
        """React on the given command"""
        method_name = "accept_" + command.__class__.__name__
        if method := getattr(self, method_name):
            method(command)
        elif parent := self.goaltree:
            parent.accept(command)
        else:
            raise NotImplementedError(f"Cannot find method {method_name}")

    def accept_all(self, *commands: Command) -> None:
        """React on the command chain"""
        for command in commands:
            self.accept(command)

    def settings(self, key: str) -> Any:
        """Returns given inner value by the key"""
        if self.goaltree is not None:
            return self.goaltree.settings(key)
        return Graph.NO_VALUE

    def reconfigure_from(self, origin: "Graph") -> None:
        """Synchronize *non-persistent* settings from the given origin"""
        if self.goaltree:
            self.goaltree.reconfigure_from(origin)

    def selections(self) -> Set[int]:
        """Return ids of special 'selection' goals"""
        if self.goaltree:
            return self.goaltree.selections()
        raise NotImplementedError

    def events(self) -> deque:
        """Returns queue of applied actions.
        Note: this queue is modifiable -- you may push new events into it. But this
        behavior may be changed in future."""
        if self.goaltree is not None:
            return self.goaltree.events()
        raise NotImplementedError

    def q(self, keys: str = "name") -> Dict[int, Any]:
        """Run search query against content"""
        raise NotImplementedError

    def error(self, message: str) -> None:
        """Show error message"""
        if self.goaltree is not None:
            self.goaltree.error(message)

    def verify(self) -> bool:
        """Check all inner data for correctness"""
        if self.goaltree is not None:
            return self.goaltree.verify()
        return False class Delete(Command):
    """Remove given or selected goal whether it exists. Do nothing in other case"""

    goal_id: int = 0 class GoalPrototype:
def _build_goal_prototype(goal_id, name, is_open, children, blockers, select):
def open_(goal_id, name, children=None, blockers=None, select=None):
def clos_(goal_id, name, children=None, blockers=None, select=None):
def build_goaltree(*goal_prototypes, message_fn=None):
from typing import List, Tuple
from siebenapp.autolink import AutoLink, ToggleAutoLink
from siebenapp.domain import (
    EdgeType,
    ToggleClose,
    Select,
    Add,
    HoldSelect,
    Insert,
    Rename,
    Graph,
    Delete,
)
from siebenapp.tests.dsl import build_goaltree, open_, selected, clos_
import pytest
    goals = tree_2_goals
    goals.accept(ToggleAutoLink("me"))
    # Add a goal to the root
    goals.accept_all(Select(1), Add("Link ME please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT), (3, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(3, EdgeType.BLOCKER)]},
        3: {"name": "Link ME please", "edge": []},
    }
    assert _autolink_events(goals) == [
        ("add_autolink", 2, "me"),
    ]


def test_do_not_make_a_link_on_not_old_matching_add(tree_2_goals):
    goals = tree_2_goals
    goals.accept_all(ToggleAutoLink("old"), ToggleAutoLink("new"))
    # Add a goal to the root
    goals.accept_all(Select(1), Add("This is old subgoal"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT), (3, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'new'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": []},
        3: {"name": "This is old subgoal", "edge": []},
    }
    assert _autolink_events(goals) == [
        ("add_autolink", 2, "old"),
        ("remove_autolink", 2),
        ("add_autolink", 2, "new"),
    ]


def test_make_a_link_on_matching_insert(tree_3v_goals):
    goals = tree_3v_goals
    goals.accept(ToggleAutoLink("me"))
    # Add a goal to the root
    goals.accept_all(Select(1), HoldSelect(), Select(3), Insert("Link ME please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT), (4, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(4, EdgeType.BLOCKER)]},
        4: {"name": "Link ME please", "edge": [(3, EdgeType.PARENT)]},
        3: {"name": "Another subgoal", "edge": []},
    }


def test_make_a_link_on_matching_rename(tree_3v_goals):
    goals = tree_3v_goals
    goals.accept(ToggleAutoLink("me"))
    goals.accept_all(Select(3), Rename("Link ME please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT), (3, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(3, EdgeType.BLOCKER)]},
        3: {"name": "Link ME please", "edge": []},
    }


def test_do_not_make_a_link_on_matching_subgoal_add(tree_2_goals):
    goals = tree_2_goals
    goals.accept(ToggleAutoLink("me"))
    # Add a sub goal to the same subgoal
    goals.accept_all(Add("Do NOT link me please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(3, EdgeType.PARENT)]},
        3: {"name": "Do NOT link me please", "edge": []},
    }


def test_do_not_make_a_link_on_matching_subgoal_insert(tree_3i_goals):
    goals = tree_3i_goals
    goals.accept(ToggleAutoLink("me"))
    # Add a sub goal to the same subgoal
    goals.accept_all(HoldSelect(), Select(3), Insert("Do NOT link me please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(4, EdgeType.PARENT)]},
        4: {"name": "Do NOT link me please", "edge": [(3, EdgeType.PARENT)]},
        3: {"name": "Another subgoal", "edge": []},
    }


def test_do_not_make_a_link_on_matching_subgoal_rename(tree_3i_goals):
    goals = tree_3i_goals
    goals.accept(ToggleAutoLink("me"))
    goals.accept_all(Select(3), Rename("Do NOT link me please"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": [(3, EdgeType.PARENT)]},
        3: {"name": "Do NOT link me please", "edge": []},
    }


def test_autolink_on_all_matching_goals(tree_3v_goals):
    goals = tree_3v_goals
    # make 2 autolinks
    goals.accept_all(ToggleAutoLink("me"), Select(3), ToggleAutoLink("plea"))
    assert goals.q("name,edge") == {
        1: {"name": "Root", "edge": [(-12, EdgeType.PARENT), (-13, EdgeType.PARENT)]},
        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},
        -13: {"name": "Autolink: 'plea'", "edge": [(3, EdgeType.PARENT)]},
        2: {"name": "Autolink on me", "edge": []},
        3: {"name": "Another subgoal", "edge": []},
    }
    # add 2-mathing goal
    goals.accept_all(Select(1), Add("Link me to both please"))
    assert goals.q("name,edge") == {
        1: {
            "name": "Root",
            "edge": [
                (-12, EdgeType.PARENT),
                (-13, EdgeType.PARENT),
                (4, EdgeType.PARENT),
            ],
        },

        -12: {"name": "Autolink: 'me'", "edge": [(2, EdgeType.PARENT)]},