class ToggleAutoLink(Command):
    keyword: str class EdgeType(IntEnum):
    BLOCKER = 1
    PARENT = 2 class HoldSelect(Command):
    """Saves current selection into the "previous selection" state""" class ToggleClose(Command):
    """Close an open selected goal. Re-open a closed selected goal"""

    root: int = 0 class Delete(Command):
    """Remove given or selected goal whether it exists. Do nothing in other case"""

    goal_id: int = 0 class ToggleLink(Command):
    """Create or remove a link between two given goals, if possible"""

    lower: int = 0
    upper: int = 0
    edge_type: EdgeType = EdgeType.BLOCKER class Add(Command):
    """Add a new goal to the existing tree"""

    name: str
    add_to: int = 0
    edge_type: EdgeType = EdgeType.PARENT class Select(Command):
    """Select a goal by its id whether it exist. Do nothing in other case"""

    goal_id: int 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 Command:
    pass class FilterBy(Command):
    """Activate or deactivate filtering"""

    pattern: str class Goals(Graph):
    ROOT_ID = 1

    def __init__(self, name: str, message_fn: Callable[[str], None] = None) -> None:
        super().__init__()
        self.goals: Dict[int, Optional[str]] = {}
        self.edges: Dict[Tuple[int, int], EdgeType] = {}
        self.edges_forward: Dict[int, Dict[int, EdgeType]] = defaultdict(
            lambda: defaultdict(lambda: EdgeType.BLOCKER)
        )
        self.edges_backward: Dict[int, Dict[int, EdgeType]] = defaultdict(
            lambda: defaultdict(lambda: EdgeType.BLOCKER)
        )
        self.closed: Set[int] = set()
        self.selection = Goals.ROOT_ID
        self.previous_selection = Goals.ROOT_ID
        self._events: deque = deque()
        self.message_fn = message_fn
        self._add_no_link(name)

    def error(self, message: str) -> None:
        if self.message_fn:
            self.message_fn(message)

    def _has_link(self, lower: int, upper: int) -> bool:
        return upper in self.edges_forward[lower]

    def _forward_edges(self, goal: int) -> List[Edge]:
        return [Edge(goal, k, v) for k, v in self.edges_forward[goal].items()]

    def _back_edges(self, goal: int) -> List[Edge]:
        return [Edge(k, goal, v) for k, v in self.edges_backward[goal].items()]

    def _parent(self, goal: int) -> int:
        parents = {e for e in self._back_edges(goal) if e.type == EdgeType.PARENT}
        return parents.pop().source if parents else Goals.ROOT_ID

    def settings(self, key: str) -> Any:
        return {
            "selection": self.selection,
            "previous_selection": self.previous_selection,
        }.get(key, 0)

    def selections(self) -> Set[int]:
        return set(
            k
            for k in [
                self.settings("selection"),
                self.settings("previous_selection"),
            ]
            if k is not None
        )

    def events(self) -> deque:
        return self._events

    def accept_Add(self, command: Add) -> bool:
        add_to = command.add_to or self.selection
        if add_to in self.closed:
            self.error("A new subgoal cannot be added to the closed one")
            return False
        next_id = self._add_no_link(command.name)
        self.accept_ToggleLink(ToggleLink(add_to, next_id, command.edge_type))
        return True

    def _add_no_link(self, name: str) -> int:
        next_id = max(list(self.goals.keys()) + [0]) + 1
        self.goals[next_id] = name
        self._events.append(("add", next_id, name, True))
        return next_id

    def accept_Select(self, command: Select):
        goal_id = command.goal_id
        if goal_id in self.goals and self.goals[goal_id] is not None:
            self.selection = goal_id
            self._events.append(("select", goal_id))

    def accept_HoldSelect(self, command: HoldSelect):
        self.previous_selection = self.selection
        self._events.append(("hold_select", self.selection))

    def q(self, keys: str = "name") -> Dict[int, Any]:
        def sel(x: int) -> Optional[str]:
            if x == self.selection:
                return "select"
            if x == self.previous_selection:
                return "prev"
            return None

        keys_list = keys.split(",")
        result: Dict[int, Any] = dict()
        for key, name in ((k, n) for k, n in self.goals.items() if n is not None):
            value = {
                "edge": sorted([(e.target, e.type) for e in self._forward_edges(key)]),
                "name": name,
                "open": key not in self.closed,
                "select": sel(key),
                "switchable": self._switchable(key),
            }
            result[key] = {k: v for k, v in value.items() if k in keys_list}
        return result

    def _switchable(self, key: int) -> bool:
        if key in self.closed:
            if back_edges := self._back_edges(key):
                return any(e.source not in self.closed for e in back_edges)
            return True
        return all(x.target in self.closed for x in self._forward_edges(key))

    def accept_Insert(self, command: Insert):
        if (lower := self.previous_selection) == (upper := self.selection):
            self.error("A new goal can be inserted only between two different goals")
            return
        edge_type = self.edges_forward[lower].get(upper, EdgeType.BLOCKER)
        if self.accept_Add(Add(command.name, lower, edge_type)):
            key = len(self.goals)
            self.accept_ToggleLink(ToggleLink(key, upper, edge_type))
            if self._has_link(lower, upper):
                self.accept_ToggleLink(ToggleLink(lower, upper))

    def accept_Rename(self, command: Rename):
        goal_id = command.goal_id or self.selection
        self.goals[goal_id] = command.new_name
        self._events.append(("rename", command.new_name, goal_id))

    def accept_ToggleClose(self, command: ToggleClose) -> None:
        if self.selection in self.closed:
            if self._may_be_reopened():
                self.closed.remove(self.selection)
                self._events.append(("toggle_close", True, self.selection))
            else:
                self.error(
                    "This goal can't be reopened because other subgoals block it"
                )
        else:
            if self._may_be_closed():
                self.closed.add(self.selection)
                self._events.append(("toggle_close", False, self.selection))
                self.accept_Select(
                    Select(self._first_open_and_switchable(command.root))
                )
                self.accept(HoldSelect())
            else:
                self.error("This goal can't be closed because it have open subgoals")

    def _may_be_closed(self) -> bool:
        return all(g.target in self.closed for g in self._forward_edges(self.selection))

    def _may_be_reopened(self) -> bool:
        blocked_by_selected = [e.source for e in self._back_edges(self.selection)]
        return all(g not in self.closed for g in blocked_by_selected)

    def _first_open_and_switchable(self, root: int) -> int:
        root = max(root, Goals.ROOT_ID)
        front = [root]
        candidates: List[int] = []
        while front:
            next_goal = front.pop()
            subgoals = [
                e.target
                for e in self._forward_edges(next_goal)
                if e.target not in self.closed and e.type == EdgeType.PARENT
            ]
            front.extend(subgoals)
            candidates.extend(g for g in subgoals if self._switchable(g))
        return min(candidates) if candidates else root

    def accept_Delete(self, command: Delete) -> None:
        if (goal_id := command.goal_id or self.selection) == Goals.ROOT_ID:
            self.error("Root goal can't be deleted")
            return
        parent = self._parent(goal_id)
        self._delete_subtree(goal_id)
        self.accept_Select(Select(parent))
        self.accept(HoldSelect())

    def _delete_subtree(self, goal_id: int) -> None:
        parent = self._parent(goal_id)
        self.goals[goal_id] = None
        self.closed.add(goal_id)
        forward_edges = self._forward_edges(goal_id)
        next_to_remove = {e for e in forward_edges if e.type == EdgeType.PARENT}
        blockers = {e for e in forward_edges if e.type == EdgeType.BLOCKER}
        for back_edge in self._back_edges(goal_id):
            self.edges_forward[back_edge.source].pop(goal_id)
        self.edges_forward[goal_id].clear()
        for forward_edge in forward_edges:
            self.edges_backward[forward_edge.target].pop(goal_id)
        self.edges_backward[goal_id].clear()
        self.edges = {k: v for k, v in self.edges.items() if goal_id not in k}
        for old_blocker in blockers:
            if not self._back_edges(old_blocker.target):
                self._create_new_link(parent, old_blocker.target, EdgeType.BLOCKER)
        for next_goal in next_to_remove:
            if not self._back_edges(next_goal.target):
                self._delete_subtree(next_goal.target)
        self._events.append(("delete", goal_id))

    def accept_ToggleLink(self, command: ToggleLink):
        if (lower := command.lower or self.previous_selection) == (
            upper := command.upper or self.selection
        ):
            self.error("Goal can't be linked to itself")
            return
        if self._has_link(lower, upper):
            if (
                current_edge_type := self.edges_forward[lower][upper]
            ) != command.edge_type:
                self._replace_link(lower, upper, command.edge_type)
                self._transform_old_parents_into_blocked(lower, upper)
            else:
                self._remove_existing_link(lower, upper, current_edge_type)
        else:
            self._create_new_link(lower, upper, command.edge_type)

    def _replace_link(self, lower: int, upper: int, edge_type: EdgeType) -> None:
        old_edge_type = self.edges_forward[lower][upper]
        self.edges[(lower, upper)] = edge_type
        self.edges_forward[lower][upper] = edge_type
        self.edges_backward[upper][lower] = edge_type
        self._events.append(("link", lower, upper, edge_type))
        self._events.append(("unlink", lower, upper, old_edge_type))

    def _remove_existing_link(
        self, lower: int, upper: int, edge_type: int = None
    ) -> None:
        if len(self._back_edges(upper)) > 1:
            self.edges.pop((lower, upper))
            self.edges_forward[lower].pop(upper)
            self.edges_backward[upper].pop(lower)
            self._events.append(("unlink", lower, upper, edge_type))
        else:
            self.error("Can't remove the last link")

    def _create_new_link(self, lower: int, upper: int, edge_type: EdgeType) -> None:
        if lower in self.closed and upper not in self.closed:
            self.error("An open goal can't block already closed one")
            return
        if self._has_circular_dependency(lower, upper):
            self.error("Circular dependencies between goals are not allowed")
            return
        if edge_type == EdgeType.PARENT:
            self._transform_old_parents_into_blocked(lower, upper)
        self.edges[lower, upper] = edge_type
        self.edges_forward[lower][upper] = edge_type
        self.edges_backward[upper][lower] = edge_type
        self._events.append(("link", lower, upper, edge_type))

    def _transform_old_parents_into_blocked(self, lower, upper):
        old_parents = [
            e.source
            for e in self._back_edges(upper)
            if e.type == EdgeType.PARENT and e.source != lower
        ]
        for p in old_parents:
            self._replace_link(p, upper, EdgeType.BLOCKER)

    def _has_circular_dependency(self, lower: int, upper: int) -> bool:
        front: Set[int] = {upper}
        visited: Set[int] = set()
        total: Set[int] = set()
        while front:
            g = front.pop()
            visited.add(g)
            for e in self._forward_edges(g):
                total.add(e.target)
                if e not in visited:
                    front.add(e.target)
        return lower in total

    def verify(self) -> bool:
        assert all(
            g.target in self.closed for p in self.closed for g in self._forward_edges(p)
        ), "Open goals could not be blocked by closed ones"

        queue: List[int] = [Goals.ROOT_ID]
        visited: Set[int] = set()
        while queue:
            goal = queue.pop()
            queue.extend(
                g.target
                for g in self._forward_edges(goal)
                if g.target not in visited and self.goals[g.target] is not None
            )
            visited.add(goal)
        assert visited == {
            x for x in self.goals if self.goals[x] is not None
        }, "All subgoals must be accessible from the root goal"

        deleted_nodes = [g for g, v in self.goals.items() if v is None]
        assert all(
            not self._forward_edges(n) for n in deleted_nodes
        ), "Deleted goals must have no dependencies"

        fwd_edges = set(
            (g1, g2, et) for g1, e in self.edges_forward.items() for g2, et in e.items()
        )
        bwd_edges = set(
            (g1, g2, et)
            for g2, e in self.edges_backward.items()
            for g1, et in e.items()
        )
        assert (
            fwd_edges == bwd_edges
        ), "Forward and backward edges must always match each other"

        parent_edges = [k for k, v in self.edges.items() if v == EdgeType.PARENT]
        edges_with_parent = {child for parent, child in parent_edges}
        assert len(parent_edges) == len(
            edges_with_parent
        ), "Each goal must have at most 1 parent"

        return True

    @staticmethod
    def build(goals, edges, settings, message_fn=None):
        # type: (GoalsData, EdgesData, OptionsData, Callable[[str], None]) -> Goals
        result = Goals("", message_fn)
        result._events.clear()
        goals_dict = {g[0]: g[1] for g in goals}
        result.goals = {
            i: goals_dict.get(i) for i in range(1, max(goals_dict.keys()) + 1)
        }

        result.closed = {g[0] for g in goals if not g[2]}.union(
            {k for k, v in result.goals.items() if v is None}
        )

        for parent, child, link_type in edges:
            result.edges[parent, child] = EdgeType(link_type)
            result.edges_forward[parent][child] = EdgeType(link_type)
            result.edges_backward[child][parent] = EdgeType(link_type)
        selection_dict = dict(settings)
        result.selection = selection_dict.get("selection", result.selection)
        result.previous_selection = selection_dict.get(
            "previous_selection", result.previous_selection
        )
        result.verify()
        return result

    @staticmethod
    def export(goals):
        # type: (Goals) -> Tuple[GoalsData, EdgesData, OptionsData]
        nodes = [
            (g_id, g_name, g_id not in goals.closed)
            for g_id, g_name in goals.goals.items()
        ]

        edges = [(k[0], k[1], v) for k, v in goals.edges.items()]
        settings = [
            ("selection", goals.selection),
            ("previous_selection", goals.previous_selection),
        ]
        return nodes, edges, settings def all_layers(
    graph: Goals,
    zoom_data: Optional[ZoomData] = None,
    autolink_data: Optional[AutoLinkData] = None,
) -> Graph:
    """Wrap given Goals instance with all default logic layers"""
    return view_layers(persistent_layers(graph, zoom_data, autolink_data)) class ToggleOpenView(Command):
    """Switch between "only open goals" and "all goals" views""" class ToggleSwitchableView(Command):
    """Switch between "only switchable goals" and "all goals" views""" def save_updates(goals: Graph, connection: sqlite3.Connection) -> None:
    actions = {
        "add": ["insert into goals values (?,?,?)"],
        "toggle_close": ["update goals set open=? where goal_id=?"],
        "rename": ["update goals set name=? where goal_id=?"],
        "link": ["insert into edges values (?,?,?)"],
        "unlink": ["delete from edges where parent=? and child=? and reltype=?"],
        "select": [
            'delete from settings where name="selection"',
            'insert into settings values ("selection", ?)',
        ],
        "hold_select": [
            'delete from settings where name="previous_selection"',
            'insert into settings values ("previous_selection", ?)',
        ],
        "delete": [
            "delete from goals where goal_id=?",
            "delete from edges where child=?",
            "delete from edges where parent=?",
        ],
        "zoom": ["insert into zoom values (?, ?)"],
        "unzoom": ["delete from zoom where goal=?"],
        "add_autolink": ["insert into autolink values (?, ?)"],
        "remove_autolink": ["delete from autolink where goal=?"],
    }
    cur = connection.cursor()
    while goals.events():
        event = goals.events().popleft()
        if event[0] in actions:
            for query in actions[event[0]]:
                if "?" in query:
                    cur.execute(query, event[1:])
                else:
                    cur.execute(query)
    connection.commit() def save_connection(goals: Graph, connection) -> None:
    run_migrations(connection)
    root_goals: Goals = get_root(goals)
    goals_export, edges_export, select_export = Goals.export(root_goals)
    zoom_goals = goals
    while not isinstance(zoom_goals, Zoom):
        zoom_goals = zoom_goals.goaltree
    zoom_export = Zoom.export(zoom_goals)
    autolink_goals = goals
    while not isinstance(autolink_goals, AutoLink):
        autolink_goals = autolink_goals.goaltree
    autolink_export = AutoLink.export(autolink_goals)
    cur = connection.cursor()
    cur.executemany("insert into goals values (?,?,?)", goals_export)
    cur.executemany("insert into edges values (?,?,?)", edges_export)
    cur.executemany("insert into settings values (?,?)", select_export)
    cur.executemany("insert into zoom values (?, ?)", zoom_export)
    cur.executemany("insert into autolink values(?, ?)", autolink_export)
    root_goals._events.clear()
    connection.commit() class ToggleZoom(Command):
    """Hide or show all goals blocked by the current one"""
import os
import sqlite3
from collections import Counter
from contextlib import closing
from typing import Set
from hypothesis import settings, assume, note, event
from hypothesis.stateful import (
    RuleBasedStateMachine,
    rule,
    initialize,
    invariant,
    precondition,
)
from hypothesis.strategies import data, integers, booleans, text, sampled_from
from siebenapp.autolink import ToggleAutoLink
from siebenapp.domain import (
    EdgeType,
    HoldSelect,
    ToggleClose,
    Delete,
    ToggleLink,
    Add,
    Select,
    Insert,
    Rename,
    Command,
)
from siebenapp.filter_view import FilterBy
from siebenapp.goaltree import Goals
from siebenapp.layers import all_layers
from siebenapp.open_view import ToggleOpenView
from siebenapp.switchable_view import ToggleSwitchableView
from siebenapp.system import save_updates, save_connection
from siebenapp.zoom import ToggleZoom
        super().__init__()
        self.goaltree = all_layers(Goals("Root"))
        self.database = sqlite3.connect(":memory:")

    @initialize()
    def open_db_connection(self):
        save_connection(self.goaltree, self.database)
        self.db_is_ready = True

    def teardown(self):
        self.database.close()

    def _accept_all(self, *commands: Command) -> None:
        for command in commands:
            note(str(command))
        self.goaltree.accept_all(*commands)

    def _accept(self, command: Command) -> None:
        note(str(command))
        self.goaltree.accept(command)

    #
    # Modifiers
    #

    @rule()
    def add_goal(self):
        event("add")
        self._accept(Add("a"))

    @rule()
    def delete_goal(self):
        event("delete")
        self._accept(Delete())

    @rule(d=data())
    def select_random_goal(self, d):
        event("select")
        max_key = max(self.goaltree.q().keys())
        assume(max_key >= 1)
        event("valid select 1")
        random_goal = d.draw(integers(min_value=1, max_value=max_key))
        assume(random_goal in self.goaltree.q())
        event("valid select 2")
        self._accept(Select(random_goal))
        # Any valid goal must be selectable
        assert self.goaltree.q("select")[random_goal]["select"] == "select"

    @rule()
    def hold_selection(self):
        event("hold")
        self._accept(HoldSelect())

    @rule(d=data())
    # Ignore trivial trees (without any subgoal)
    @precondition(lambda self: len(self.goaltree.q()) > 1)
    def insert(self, d):
        event("insert")
        candidates = sorted(
            list(
                goal_id
                for goal_id, attrs in self.goaltree.q("select").items()
                if attrs["select"] != "select"
            )
        )
        random_goal = d.draw(sampled_from(candidates))
        self._accept_all(HoldSelect(), Select(random_goal), Insert("i"))

    @rule(b=booleans(), d=data())
    # Ignore trivial trees (without any subgoal)
    @precondition(lambda self: len(self.goaltree.q()) > 1)
    def toggle_link(self, b, d):
        event("toggle link")
        goal_keys = sorted(list(k for k in self.goaltree.q().keys() if k > 0))
        assume(len(goal_keys) > 1)
        selection = d.draw(sampled_from(goal_keys))
        goal_keys.remove(selection)
        prev_selection = d.draw(sampled_from(goal_keys))
        event("valid toggle link")
        edge_type = EdgeType.PARENT if b else EdgeType.BLOCKER
        self._accept(
            ToggleLink(lower=prev_selection, upper=selection, edge_type=edge_type)
        )

    @rule(c=sampled_from(" abcit"))
    # Ignore trivial trees (without any subgoal)
    @precondition(lambda self: len(self.goaltree.q()) > 1)
    def add_autolink(self, c):
        event("autolink")
        event("valid autolink")
        self._accept(ToggleAutoLink(c))

    @rule()
    def close_or_open(self):
        event("close/open")
        self._accept(ToggleClose())

    @rule(t=text())
    def rename(self, t):
        event("rename")
        self._accept(Rename(t))

    @rule()
    def zoom(self):
        event("zoom")
        self._accept(ToggleZoom())

    @rule()
    def switchable_view(self):
        event("switchable_view")
        self._accept(ToggleSwitchableView())

    @rule()
    def open_view(self):
        event("open_view")
        self._accept(ToggleOpenView())

    @rule()
    def filter_by_text(self):
        event("filter x")

        self._accept(FilterBy("x"))