# -*- coding: utf-8 -*-

import sys
import tempfile
from os.path import getsize
from os import unlink
from functools import partial

from transitions_aio.extensions.nesting import NestedState, HierarchicalMachine
from transitions_aio.extensions import HierarchicalGraphMachine

from unittest import skipIf
from .test_core import TestTransitions, TestCase, TYPE_CHECKING
from .utils import Stuff, DummyModel

try:
    from unittest.mock import MagicMock
except ImportError:
    from mock import MagicMock

try:
    # Just to skip tests if graphviz not installed
    import graphviz as pgv  # @UnresolvedImport
except ImportError:  # pragma: no cover
    pgv = None


if TYPE_CHECKING:
    from typing import List, Dict, Union, Type, Sequence
    from transitions_aio.core import TransitionConfig

default_separator = NestedState.separator


class Dummy:
    pass


test_states = ['A', 'B',
               {'name': 'C', 'children': ['1', '2', {'name': '3', 'children': ['a', 'b', 'c']}]}, 'D', 'E', 'F']


class TestNestedTransitions(TestTransitions):

    def setUp(self):
        self.states = test_states
        self.machine_cls = HierarchicalMachine  # type: Type[HierarchicalMachine]
        self.state_cls = NestedState
        self.stuff = Stuff(self.states, self.machine_cls)

    def test_add_model(self):
        model = Dummy()
        self.stuff.machine.add_model(model, initial='E')

    def test_init_machine_with_hella_arguments(self):
        states = [
            self.state_cls('State1'),
            'State2',
            {
                'name': 'State3',
                'on_enter': 'hello_world'
            }
        ]
        transitions = [
            {'trigger': 'advance',
                'source': 'State2',
                'dest': 'State3'
             }
        ]
        s = Stuff()
        self.stuff.machine_cls(
            model=s, states=states, transitions=transitions, initial='State2')
        s.advance()
        self.assertEqual(s.message, 'Hello World!')

    def test_init_machine_with_nested_states(self):
        State = self.state_cls
        a = State('A')
        b = State('B')
        b_1 = State('1')
        b_2 = State('2')
        b.add_substate(b_1)
        b.add_substates([b_2])
        m = self.stuff.machine_cls(states=[a, b])
        self.assertEqual(m.states['B'].states['1'], b_1)
        m.to("B{0}1".format(State.separator))
        self.assertEqual(m.state, "B{0}1".format(State.separator))

    def test_property_initial(self):
        # Define with list of dictionaries
        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', '3']}, 'D']
        # Define with list of dictionaries
        transitions = [
            {'trigger': 'walk', 'source': 'A', 'dest': 'B'},
            {'trigger': 'run', 'source': 'B', 'dest': 'C'},
            {'trigger': 'sprint', 'source': 'C', 'dest': 'D'}
        ]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        self.assertEqual(m.initial, 'A')
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='C')
        self.assertEqual(m.initial, 'C')
        m = self.stuff.machine_cls(states=states, transitions=transitions)
        self.assertEqual(m.initial, 'initial')

    def test_transition_definitions(self):
        State = self.state_cls
        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', '3']}, 'D']
        # Define with list of dictionaries
        transitions = [
            {'trigger': 'walk', 'source': 'A', 'dest': 'B'},
            {'trigger': 'run', 'source': 'B', 'dest': 'C'},
            {'trigger': 'sprint', 'source': 'C', 'dest': 'D'},
            {'trigger': 'run', 'source': 'C', 'dest': 'C%s1' % State.separator}
        ]  # type: List[Union[List[str], Dict[str, str]]]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        m.walk()
        self.assertEqual(m.state, 'B')
        m.run()
        self.assertEqual(m.state, 'C')
        m.run()
        self.assertEqual(m.state, 'C%s1' % State.separator)
        # Define with list of lists
        transitions = [
            ['walk', 'A', 'B'],
            ['run', 'B', 'C'],
            ['sprint', 'C', 'D']
        ]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        m.to_C()
        m.sprint()
        self.assertEqual(m.state, 'D')

    def test_nested_definitions(self):
        separator = self.state_cls.separator
        state = {
            'name': 'B',
            'children': ['1', '2'],
            'transitions': [['jo', '1', '2']],
            'initial': '2'
        }
        m = self.stuff.machine_cls(initial='A', states=['A', state],
                                   transitions=[['go', 'A', 'B'], ['go', 'B{0}2'.format(separator),
                                                                   'B{0}1'.format(separator)]])
        self.assertTrue(m.is_A())
        m.go()
        self.assertEqual(m.state, 'B{0}2'.format(separator))
        m.go()
        self.assertEqual(m.state, 'B{0}1'.format(separator))
        m.jo()

    def test_transitioning(self):
        s = self.stuff
        s.machine.add_transition('advance', 'A', 'B')
        s.machine.add_transition('advance', 'B', 'C')
        s.machine.add_transition('advance', 'C', 'D')
        s.machine.add_transition('reset', '*', 'A')
        self.assertEqual(len(s.machine.events['reset'].transitions), 6)
        self.assertEqual(len(s.machine.events['reset'].transitions['C']), 1)
        s.advance()
        self.assertEqual(s.state, 'B')
        self.assertFalse(s.is_A())
        self.assertTrue(s.is_B())
        s.advance()
        self.assertEqual(s.state, 'C')

    def test_conditions(self):
        s = self.stuff
        s.machine.add_transition('advance', 'A', 'B', conditions='this_passes')
        s.machine.add_transition('advance', 'B', 'C', unless=['this_fails'])
        s.machine.add_transition('advance', 'C', 'D', unless=['this_fails',
                                                              'this_passes'])
        s.advance()
        self.assertEqual(s.state, 'B')
        s.advance()
        self.assertEqual(s.state, 'C')
        s.advance()
        self.assertEqual(s.state, 'C')

    def test_multiple_add_transitions_from_state(self):
        State = self.state_cls
        s = self.stuff
        s.machine.add_transition(
            'advance', 'A', 'B', conditions=['this_fails'])
        s.machine.add_transition('advance', 'A', 'C')
        s.machine.add_transition('advance', 'C', 'C%s2' % State.separator)
        s.advance()
        self.assertEqual(s.state, 'C')
        s.advance()
        self.assertEqual(s.state, 'C%s2' % State.separator)
        self.assertFalse(s.is_C())
        self.assertTrue(s.is_C(allow_substates=True))

    def test_use_machine_as_model(self):
        states = ['A', 'B', 'C', 'D']
        m = self.stuff.machine_cls(states=states, initial='A')
        m.add_transition('move', 'A', 'B')
        m.add_transition('move_to_C', 'B', 'C')
        m.move()
        self.assertEqual(m.state, 'B')

    def test_add_custom_state(self):
        State = self.state_cls
        s = self.stuff
        s.machine.add_states([{'name': 'E', 'children': ['1', '2']}])
        s.machine.add_state('E%s3' % State.separator)
        s.machine.add_transition('go', '*', 'E%s1' % State.separator)
        s.machine.add_transition('walk', '*', 'E%s3' % State.separator)
        s.machine.add_transition('run', 'E', 'C{0}3{0}a'.format(State.separator))
        s.go()
        self.assertEqual('E{0}1'.format(State.separator), s.state)
        s.walk()
        self.assertEqual('E{0}3'.format(State.separator), s.state)
        s.run()
        self.assertEqual('C{0}3{0}a'.format(State.separator), s.state)

    def test_add_nested_state(self):
        m = self.machine_cls(states=['A'], initial='A')
        m.add_state('B{0}1{0}a'.format(self.state_cls.separator))
        self.assertIn('B', m.states)
        self.assertIn('1', m.states['B'].states)
        self.assertIn('a', m.states['B'].states['1'].states)

        with self.assertRaises(ValueError):
            m.add_state(m.states['A'])

    def test_enter_exit_nested_state(self):
        State = self.state_cls
        mock = MagicMock()

        def callback():
            mock()
        states = ['A', 'B', {'name': 'C', 'on_enter': callback, 'on_exit': callback,
                             'children': [{'name': '1', 'on_enter': callback, 'on_exit': callback}, '2', '3']},
                            {'name': 'D', 'on_enter': callback, 'on_exit': callback}]
        transitions = [['go', 'A', 'C{0}1'.format(State.separator)],
                       ['go', 'C', 'D']]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        m.go()
        self.assertTrue(mock.called)
        self.assertEqual(2, mock.call_count)
        m.go()
        self.assertTrue(m.is_D())
        self.assertEqual(5, mock.call_count)
        m.to_C()
        self.assertEqual(7, mock.call_count)
        m.to('C{0}1'.format(State.separator))
        self.assertEqual(8, mock.call_count)
        m.to('C{0}2'.format(State.separator))
        self.assertEqual(9, mock.call_count)

    def test_ordered_transitions(self):
        State = self.state_cls
        states = [{'name': 'first', 'children': ['second', 'third', {'name': 'fourth', 'children': ['fifth', 'sixth']},
                                                 'seventh']}, 'eighth', 'ninth']
        m = self.stuff.machine_cls(states=states)
        m.add_ordered_transitions()
        self.assertEqual('initial', m.state)
        m.next_state()
        self.assertEqual('first', m.state)
        m.next_state()
        m.next_state()
        self.assertEqual('first{0}third'.format(State.separator), m.state)
        m.next_state()
        m.next_state()
        self.assertEqual('first{0}fourth{0}fifth'.format(State.separator), m.state)
        m.next_state()
        m.next_state()
        self.assertEqual('first{0}seventh'.format(State.separator), m.state)
        m.next_state()
        m.next_state()
        self.assertEqual('ninth', m.state)

        # Include initial state in loop
        m = self.stuff.machine_cls(states=states)
        m.add_ordered_transitions(loop_includes_initial=False)
        m.to_ninth()
        m.next_state()
        self.assertEqual(m.state, 'first')

        # Test user-determined sequence and trigger name
        m = self.stuff.machine_cls(states=states, initial='first')
        m.add_ordered_transitions(['first', 'ninth'], trigger='advance')
        m.advance()
        self.assertEqual(m.state, 'ninth')
        m.advance()
        self.assertEqual(m.state, 'first')

        # Via init argument
        m = self.stuff.machine_cls(states=states, initial='first', ordered_transitions=True)
        m.next_state()
        self.assertEqual(m.state, 'first{0}second'.format(State.separator))

    def test_pickle(self):
        print("separator", self.state_cls.separator)
        import pickle

        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', {'name': '3', 'children': ['a', 'b', 'c']}]},
                  'D', 'E', 'F']
        transitions = [
            {'trigger': 'walk', 'source': 'A', 'dest': 'B'},
            {'trigger': 'run', 'source': 'B', 'dest': 'C'},
            {'trigger': 'sprint', 'source': 'C', 'dest': 'D'}
        ]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        m.walk()
        dump = pickle.dumps(m)
        self.assertIsNotNone(dump)
        m2 = pickle.loads(dump)
        self.assertEqual(m.state, m2.state)
        m2.run()
        m2.to_C_3_a()
        m2.to_C_3_b()

    def test_callbacks_duplicate(self):

        transitions = [
            {'trigger': 'walk', 'source': 'A', 'dest': 'C', 'before': 'before_change',
             'after': 'after_change'},
            {'trigger': 'run', 'source': 'B', 'dest': 'C'}
        ]

        m = self.stuff.machine_cls(states=['A', 'B', 'C'], transitions=transitions,
                                   before_state_change='before_change',
                                   after_state_change='after_change', send_event=True,
                                   initial='A', auto_transitions=True)

        m.before_change = MagicMock()
        m.after_change = MagicMock()

        m.walk()
        self.assertEqual(m.before_change.call_count, 2)
        self.assertEqual(m.after_change.call_count, 2)

    def test_example_one(self):
        State = self.state_cls
        State.separator = '_'
        states = ['standing', 'walking', {'name': 'caffeinated', 'children': ['dithering', 'running']}]
        transitions = [['walk', 'standing', 'walking'],
                       ['stop', 'walking', 'standing'],
                       ['drink', 'caffeinated_dithering', '='],
                       ['drink', 'caffeinated', 'caffeinated_dithering'],
                       ['drink', '*', 'caffeinated'],
                       ['walk', 'caffeinated', 'caffeinated_running'],
                       ['relax', 'caffeinated', 'standing']]
        machine = self.stuff.machine_cls(states=states, transitions=transitions, initial='standing',
                                         ignore_invalid_triggers=True, name='Machine 1')

        machine.walk()   # Walking now
        machine.stop()   # let's stop for a moment
        machine.drink()  # coffee time
        machine.state
        self.assertEqual(machine.state, 'caffeinated')
        machine.drink()  # again!
        self.assertEqual(machine.state, 'caffeinated_dithering')
        machine.drink()  # and again!
        self.assertEqual(machine.state, 'caffeinated_dithering')
        machine.walk()   # we have to go faster
        self.assertEqual(machine.state, 'caffeinated_running')
        machine.stop()   # can't stop moving!
        machine.state
        self.assertEqual(machine.state, 'caffeinated_running')
        machine.relax()  # leave nested state
        machine.state    # phew, what a ride
        self.assertEqual(machine.state, 'standing')
        machine.to_caffeinated_running()  # auto transition fast track
        machine.on_enter_caffeinated_running('callback_method')

    def test_multiple_models(self):
        class Model(object):
            pass
        s1, s2 = Model(), Model()
        m = self.stuff.machine_cls(model=[s1, s2], states=['A', 'B', 'C'], initial='A')
        self.assertEqual(len(m.models), 2)
        m.add_transition('advance', 'A', 'B')
        self.assertNotEqual(s1.advance, s2.advance)
        s1.advance()
        self.assertEqual(s1.state, 'B')
        self.assertEqual(s2.state, 'A')

    def test_excessive_nesting(self):
        states = [{'name': 'A', 'children': []}]  # type: List[Dict[str, Union[str, List[Dict]]]]
        curr = states[0]  # type: Dict
        for i in range(10):
            curr['children'].append({'name': str(i), 'children': []})
            curr = curr['children'][0]
        m = self.stuff.machine_cls(states=states, initial='A')

    def test_intial_state(self):
        separator = self.state_cls.separator
        states = [{'name': 'A', 'children': ['1', '2'], 'initial': '2'},
                  {'name': 'B', 'initial': '2',
                   'children': ['1', {'name': '2', 'initial': 'a',
                                      'children': ['a', 'b']}]}]
        transitions = [['do', 'A', 'B'],
                       ['do', 'B{0}2'.format(separator),
                        'B{0}1'.format(separator)]]
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')
        self.assertEqual(m.state, 'A{0}2'.format(separator))
        m.do()
        self.assertEqual(m.state, 'B{0}2{0}a'.format(separator))
        self.assertTrue(m.is_B(allow_substates=True))
        m.do()
        self.assertEqual(m.state, 'B{0}1'.format(separator))
        m = self.stuff.machine_cls(states=states, transitions=transitions, initial='B{0}2{0}b'.format(separator))
        self.assertTrue('B{0}2{0}b'.format(separator), m.state)

    def test_get_triggers(self):
        seperator = self.state_cls.separator
        states = ['standing', 'walking', {'name': 'caffeinated', 'children': ['dithering', 'running']}]
        transitions = [
            ['walk', 'standing', 'walking'],
            ['go', 'standing', 'walking'],
            ['stop', 'walking', 'standing'],
            {'trigger': 'drink', 'source': '*', 'dest': 'caffeinated{0}dithering'.format(seperator),
             'conditions': 'is_hot', 'unless': 'is_too_hot'},
            ['walk', 'caffeinated{0}dithering'.format(seperator), 'caffeinated{0}running'.format(seperator)],
            ['relax', 'caffeinated', 'standing']
        ]

        machine = self.stuff.machine_cls(states=states, transitions=transitions, auto_transitions=False)
        trans = machine.get_triggers('caffeinated{0}dithering'.format(seperator))
        self.assertEqual(len(trans), 3)
        self.assertTrue('relax' in trans)

    def test_get_nested_transitions(self):
        seperator = self.state_cls.separator
        states = ['A', {'name': 'B', 'children': ['1', '2', {'name': '3', 'children': ['a', 'b'],
                                                             'transitions': [['inner', 'a', 'b'],
                                                                             ['inner', 'b', 'a']]}],
                        'transitions': [['mid', '1', '1'],
                                        ['mid', '2', '3'],
                                        ['mid', '3', '1'],
                                        ['mid2', '2', '3'],
                                        ['mid_loop', '1', '1']]}]
        transitions = [['outer', 'A', 'B'],
                       ['outer', ['A', 'B'], 'C']]
        machine = self.stuff.machine_cls(states=states, transitions=transitions, initial='A', auto_transitions=False)
        self.assertEqual(10, len(machine.get_transitions()))
        self.assertEqual(2, len(machine.get_transitions(source='A')))
        self.assertEqual(2, len(machine.get_transitions('inner')))
        self.assertEqual(3, len(machine.get_transitions('mid')))
        self.assertEqual(3, len(machine.get_transitions(dest='B{0}1'.format(seperator))))
        self.assertEqual(2, len(machine.get_transitions(source='B{0}2'.format(seperator),
                                                        dest='B{0}3'.format(seperator))))
        self.assertEqual(1, len(machine.get_transitions(source='B{0}3{0}a'.format(seperator),
                                                        dest='B{0}3{0}b'.format(seperator))))
        self.assertEqual(1, len(machine.get_transitions(source=machine.states['B'].states['3'].states['b'])))
        # should be B_3_b -> B_3_a, B_3 -> B_1 and B -> C
        self.assertEqual(3, len(machine.get_transitions(source=machine.states['B'].states['3'].states['b'],
                                                        delegate=True)))

    def test_internal_transitions(self):
        s = self.stuff
        s.machine.add_transition('internal', 'A', None, prepare='increase_level')
        s.internal()
        self.assertEqual(s.state, 'A')
        self.assertEqual(s.level, 2)

    def test_transition_with_unknown_state(self):
        s = self.stuff
        with self.assertRaises(ValueError):
            s.machine.add_transition('next', 'A', s.machine.state_cls('X'))

    def test_skip_to_override(self):
        mock = MagicMock()

        class Model:

            def to(self):
                mock()

        model1 = Model()
        model2 = DummyModel()
        machine = self.machine_cls([model1, model2], states=['A', 'B'], initial='A')
        model1.to()
        model2.to('B')
        self.assertTrue(mock.called)
        self.assertTrue(model2.is_B())

    def test_trigger_parent(self):
        parent_mock = MagicMock()
        exit_mock = MagicMock()
        enter_mock = MagicMock()

        class Model:

            def on_exit_A(self):
                parent_mock()

            def on_exit_A_1(self):
                exit_mock()

            def on_enter_A_2(self):
                enter_mock()

        model = Model()
        machine = self.machine_cls(model, states=[{'name': 'A', 'children': ['1', '2']}],
                                   transitions=[['go', 'A', 'A_2'], ['enter', 'A', 'A_1']], initial='A')

        model.enter()
        self.assertFalse(parent_mock.called)
        model.go()
        self.assertTrue(exit_mock.called)
        self.assertTrue(enter_mock.called)
        self.assertFalse(parent_mock.called)

    def test_trigger_parent_model_self(self):
        exit_mock = MagicMock()
        enter_mock = MagicMock()

        class CustomMachine(self.machine_cls):  # type: ignore
            def on_enter_A(self):
                raise AssertionError("on_enter_A must not be called!")

            def on_exit_A(self):
                raise AssertionError("on_exit_A must not be called!")

            def on_exit_A_1(self):
                exit_mock()

            def on_enter_A_2(self):
                enter_mock()

        machine = CustomMachine(states=[{'name': 'A', 'children': ['1', '2']}],
                                transitions=[['go', 'A', 'A_2'], ['enter', 'A', 'A_1']], initial='A')
        machine.enter()
        self.assertFalse(exit_mock.called)
        self.assertFalse(enter_mock.called)
        machine.go()
        self.assertTrue(exit_mock.called)
        self.assertTrue(enter_mock.called)
        machine.go()
        self.assertEqual(2, enter_mock.call_count)

    def test_child_condition_persistence(self):
        # even though the transition is invalid for the parent it is valid for the nested child state
        # no invalid transition exception should be thrown
        machine = self.machine_cls(states=[{'name': 'A', 'children': ['1', '2'], 'initial': '1',
                                            'transitions': [{'trigger': 'go', 'source': '1', 'dest': '2',
                                                             'conditions': self.stuff.this_fails}]}, 'B'],
                                   transitions=[['go', 'B', 'A']], initial='A')
        self.assertFalse(False, machine.go())

    def test_exception_in_state_enter_exit(self):
        # https://github.com/pytransitions/transitions/issues/486
        # NestedState._scope needs to be reset when an error is raised in a state callback
        class Model:
            def on_enter_B_1(self):
                raise RuntimeError("Oh no!")

            def on_exit_C_1(self):
                raise RuntimeError("Oh no!")

        states = ['A',
                  {'name': 'B', 'initial': '1', 'children': ['1', '2']},
                  {'name': 'C', 'initial': '1', 'children': ['1', '2']}]
        model = Model()
        machine = self.machine_cls(model, states=states, initial='A')
        with self.assertRaises(RuntimeError):
            model.to_B()
        self.assertTrue(model.is_B_1())
        machine.set_state('A', model)
        with self.assertRaises(RuntimeError):
            model.to_B()
        with self.assertRaises(RuntimeError):
            model.to_C()
            model.to_A()
        self.assertTrue(model.is_C_1())
        machine.set_state('A', model)
        model.to_C()
        self.assertTrue(model.is_C_1())

    def test_correct_subclassing(self):
        from transitions_aio.core import State

        class WrongStateClass(self.machine_cls):  # type: ignore
            state_cls = State

        class MyNestedState(NestedState):
            pass

        class CorrectStateClass(self.machine_cls):  # type: ignore
            state_cls = MyNestedState

        with self.assertRaises(AssertionError):
            m = WrongStateClass()
        m = CorrectStateClass()

    def test_queued_callbacks(self):
        states = [
            "initial",
            {'name': 'A', 'children': [{'name': '1', 'on_enter': 'go'}, '2'],
             'transitions': [['go', '1', '2']], 'initial': '1'}
        ]
        machine = self.machine_cls(states=states, initial='initial', queued=True)
        machine.to_A()
        self.assertEqual("A{0}2".format(self.state_cls.separator), machine.state)

    def test_nested_transitions(self):
        states = [{
            'name': 'A',
            'states': [
                {'name': 'B',
                 'states': [
                     {'name': 'C',
                      'states': ['1', '2'],
                      'initial': '1'}],
                 'transitions': [['go', 'C_1', 'C_2']],
                 'initial': 'C',
                 }],
            'initial': 'B'
        }]
        machine = self.machine_cls(states=states, initial='A')
        machine.go()

    def test_auto_transitions_from_nested_callback(self):

        def fail():
            self.fail("C should not be exited!")

        states = [
            {'name': 'b', 'children': [
                {'name': 'c', 'on_exit': fail, 'on_enter': 'to_b_c_ca', 'children': ['ca', 'cb']},
                'd'
            ]},
        ]
        machine = self.machine_cls(states=states, queued=True, initial='b')
        machine.to_b_c()

    def test_machine_may_transitions_for_generated_triggers(self):
        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', '3']}, 'D']
        m = self.stuff.machine_cls(states=states, initial='A')
        assert m.may_to_A()
        assert m.may_trigger("to_A")
        m.to_A()
        assert m.may_to_B()
        assert m.may_trigger("to_B")
        m.to_B()
        assert m.may_to_C()
        assert m.may_trigger("to_C")
        m.to_C()
        assert m.may_to_C_1()
        assert m.may_trigger("to_C_1")
        m.to_C_1()
        assert m.may_to_D()
        assert m.may_trigger("to_D")
        m.to_D()

    def test_get_nested_triggers(self):
        transitions = [
            ['goB', 'A', 'B'],
            ['goC', 'B', 'C'],
            ['goA', '*', 'A'],
            ['goF1', ['C{0}1'.format(self.machine_cls.separator), 'C{0}2'.format(self.machine_cls.separator)], 'F'],
            ['goF2', 'C', 'F']
        ]  # type: Sequence[TransitionConfig]
        m = self.machine_cls(states=test_states, transitions=transitions, auto_transitions=False, initial='A')
        self.assertEqual(1, len(m.get_nested_triggers(['C', '1'])))
        with m('C'):
            m.add_transition('goC1', '1', '2')
        self.assertEqual(len(transitions) + 1, len(m.get_nested_triggers()))
        self.assertEqual(2, len(m.get_nested_triggers(['C', '1'])))
        self.assertEqual(2, len(m.get_nested_triggers(['C'])))

    def test_stop_transition_evaluation(self):
        states = ['A', {'name': 'B', 'states': ['C', 'D']}]
        transitions = [['next', 'A', 'B_C'], ['next', 'B_C', 'B_D'], ['next', 'B', 'A']]
        mock = MagicMock()

        def process_error(event_data):
            assert isinstance(event_data.error, ValueError)
            mock()

        m = self.machine_cls(states=states, transitions=transitions, initial='A', send_event=True)
        m.on_enter_B_D(partial(self.stuff.this_raises, ValueError()))
        m.next()
        with self.assertRaises(ValueError):
            m.next()
        assert m.is_B_D()
        assert m.to_B_C()
        m.on_exception = [process_error]
        m.next()
        assert mock.called
        assert m.is_B_D()

    def test_nested_queued_remap(self):
        states = ['A', 'done',
                  {'name': 'B', 'remap': {'done': 'done'},
                   'initial': 'initial',
                   'transitions': [['go', 'initial', 'a']],
                   'states': ['done', 'initial', {'name': 'a', 'remap': {'done': 'done'},
                                                  'initial': 'initial',
                                                  'transitions': [['go', 'initial', 'x']],
                                                  'states': ['done', 'initial',
                                                             {'name': 'x',
                                                              'remap': {'done': 'done'},
                                                              'initial': 'initial',
                                                              'states': ['initial', 'done'],
                                                              'transitions': [['done', 'initial', 'done']]}]}]}]
        m = self.machine_cls(states=states, initial='A', queued=True)
        m.on_enter_B('go')
        m.on_enter_B_a('go')
        m.on_enter_B_a_x_initial('done')
        m.to_B()
        assert m.is_done()

    # https://github.com/pytransitions/transitions/issues/568
    def test_wildcard_src_reflexive_dest(self):
        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', '3']}, 'D']
        machine = self.machine_cls(states=states, transitions=[["reflexive", "*", "="]], initial="A")
        self.assertTrue(machine.is_A())
        machine.reflexive()
        self.assertTrue(machine.is_A())
        state_name = 'C{0}2'.format(self.state_cls.separator)
        machine.set_state(state_name)
        self.assertEqual(state_name, machine.state)
        machine.reflexive()
        self.assertEqual(state_name, machine.state)

    def test_final_state_nested(self):
        final_mock_B = MagicMock()
        final_mock_Y = MagicMock()
        final_mock_Z = MagicMock()
        final_mock_machine = MagicMock()

        mocks = [final_mock_B, final_mock_Y, final_mock_Z, final_mock_machine]

        states = ['A', {'name': 'B', 'parallel': [{'name': 'X', 'final': True},
                                                  {'name': 'Y', 'transitions': [['final_Y', 'yI', 'yII']],
                                                   'initial': 'yI',
                                                   'on_final': final_mock_Y,
                                                   'states':
                                                       ['yI', {'name': 'yII', 'final': True}]
                                                   },
                                                  {'name': 'Z', 'transitions': [['final_Z', 'zI', 'zII']],
                                                   'initial': 'zI',
                                                   'on_final': final_mock_Z,
                                                   'states':
                                                       ['zI', {'name': 'zII', 'final': True}]
                                                   },
                                                  ],
                        "on_final": final_mock_B}]

        machine = self.machine_cls(states=states, on_final=final_mock_machine, initial='A')
        self.assertFalse(any(mock.called for mock in mocks))
        machine.to_B()
        self.assertFalse(any(mock.called for mock in mocks))
        machine.final_Y()
        self.assertTrue(final_mock_Y.called)
        self.assertFalse(final_mock_Z.called)
        self.assertFalse(final_mock_B.called)
        self.assertFalse(final_mock_machine.called)
        machine.final_Z()
        self.assertEqual(1, final_mock_Y.call_count)
        self.assertEqual(1, final_mock_Z.call_count)
        self.assertEqual(1, final_mock_B.call_count)
        self.assertEqual(1, final_mock_machine.call_count)


class TestSeparatorsBase(TestCase):

    separator = default_separator

    def setUp(self):

        class CustomNestedState(NestedState):
            separator = self.separator

        class CustomHierarchicalMachine(HierarchicalMachine):
            state_cls = CustomNestedState

        self.states = test_states
        self.machine_cls = CustomHierarchicalMachine
        self.state_cls = CustomNestedState
        self.stuff = Stuff(self.states, self.machine_cls)

    def test_add_nested_state(self):
        m = self.machine_cls(states=['A'], initial='A')
        m.add_state('B{0}1{0}a'.format(self.state_cls.separator))
        m.add_state('B{0}2{0}b'.format(self.state_cls.separator))
        self.assertIn('B', m.states)
        self.assertIn('1', m.states['B'].states)
        self.assertIn('a', m.states['B'].states['1'].states)

        with self.assertRaises(ValueError):
            m.add_state(m.states['A'])

    def test_enter_exit_nested(self):
        separator = self.state_cls.separator
        s = self.stuff
        s.machine.add_transition('advance', 'A', 'C{0}3'.format(separator))
        s.machine.add_transition('reverse', 'C', 'A')
        s.machine.add_transition('lower', ['C{0}1'.format(separator),
                                           'C{0}3'.format(separator)], 'C{0}3{0}a'.format(separator))
        s.machine.add_transition('rise', 'C{0}3'.format(separator), 'C{0}1'.format(separator))
        s.machine.add_transition('fast', 'A', 'C{0}3{0}a'.format(separator))

        for state_name in s.machine.get_nested_state_names():
            state = s.machine.get_state(state_name)
            state.on_enter.append('increase_level')
            state.on_exit.append('decrease_level')

        s.advance()
        self.assertEqual('C{0}3'.format(separator), s.state)
        self.assertEqual(2, s.level)
        self.assertEqual(3, s.transitions)  # exit A; enter C,3
        s.lower()
        self.assertEqual(s.state, 'C{0}3{0}a'.format(separator))
        self.assertEqual(3, s.level)
        self.assertEqual(4, s.transitions)  # enter a
        s.rise()
        self.assertEqual('C%s1' % separator, s.state)
        self.assertEqual(2, s.level)
        self.assertEqual(7, s.transitions)  # exit a, 3; enter 1
        s.reverse()
        self.assertEqual('A', s.state)
        self.assertEqual(1, s.level)
        self.assertEqual(10, s.transitions)  # exit 1, C; enter A
        s.fast()
        self.assertEqual(s.state, 'C{0}3{0}a'.format(separator))
        self.assertEqual(s.level, 3)
        self.assertEqual(s.transitions, 14)  # exit A; enter C, 3, a
        s.to_A()
        self.assertEqual(s.state, 'A')
        self.assertEqual(s.level, 1)
        self.assertEqual(s.transitions, 18)  # exit a, 3, C; enter A
        s.to_A()
        self.assertEqual(s.state, 'A')
        self.assertEqual(s.level, 1)
        self.assertEqual(s.transitions, 20)  # exit A; enter A
        if separator == '_':
            s.to_C_3_a()
        else:
            print("separator", separator)
            s.to_C.s3.a()
        self.assertEqual(s.state, 'C{0}3{0}a'.format(separator))
        self.assertEqual(s.level, 3)
        self.assertEqual(s.transitions, 24)  # exit A; enter C, 3, a

    def test_state_change_listeners(self):
        State = self.state_cls
        s = self.stuff
        s.machine.add_transition('advance', 'A', 'C%s1' % State.separator)
        s.machine.add_transition('reverse', 'C', 'A')
        s.machine.add_transition('lower', 'C%s1' % State.separator, 'C{0}3{0}a'.format(State.separator))
        s.machine.add_transition('rise', 'C%s3' % State.separator, 'C%s1' % State.separator)
        s.machine.add_transition('fast', 'A', 'C{0}3{0}a'.format(State.separator))
        s.machine.on_enter_C('hello_world')
        s.machine.on_exit_C('goodbye')
        s.machine.on_enter('C{0}3{0}a'.format(State.separator), 'greet')
        s.machine.on_exit('C%s3' % State.separator, 'meet')
        s.advance()
        self.assertEqual(s.state, 'C%s1' % State.separator)
        self.assertEqual(s.message, 'Hello World!')
        s.lower()
        self.assertEqual(s.state, 'C{0}3{0}a'.format(State.separator))
        self.assertEqual(s.message, 'Hi')
        s.rise()
        self.assertEqual(s.state, 'C%s1' % State.separator)
        self.assertTrue(s.message is not None and s.message.startswith('Nice to'))
        s.reverse()
        self.assertEqual(s.state, 'A')
        self.assertTrue(s.message is not None and s.message.startswith('So long'))
        s.fast()
        self.assertEqual(s.state, 'C{0}3{0}a'.format(State.separator))
        self.assertEqual(s.message, 'Hi')
        s.to_A()
        self.assertEqual(s.state, 'A')
        self.assertTrue(s.message is not None and s.message.startswith('So long'))

    def test_nested_auto_transitions(self):
        State = self.state_cls
        s = self.stuff
        s.to_C()
        self.assertEqual(s.state, 'C')
        state = 'C{0}3{0}a'.format(State.separator)
        s.to(state)
        self.assertEqual(s.state, state)
        # backwards compatibility check (can be removed in 0.7)
        self.assertEqual(s.state, state)
        for state_name in s.machine.get_nested_state_names():
            event_name = 'to_{0}'.format(state_name)
            num_base_states = len(s.machine.states)
            self.assertTrue(event_name in s.machine.events)
            self.assertEqual(len(s.machine.events[event_name].transitions), num_base_states)

    @skipIf(pgv is None, 'NestedGraph diagram test requires graphviz')
    def test_ordered_with_graph(self):
        class CustomHierarchicalGraphMachine(HierarchicalGraphMachine):
            state_cls = self.state_cls

        states = ['A', 'B', {'name': 'C', 'children': ['1', '2',
                                                       {'name': '3', 'children': ['a', 'b', 'c']}]}, 'D', 'E', 'F']
        machine = CustomHierarchicalGraphMachine(states=states, initial='A', auto_transitions=False,
                                                 ignore_invalid_triggers=True, graph_engine="graphviz")
        machine.add_ordered_transitions(trigger='next_state')
        machine.next_state()
        self.assertEqual(machine.state, 'B')
        target = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
        machine.get_graph().draw(target.name, prog='dot')
        self.assertTrue(getsize(target.name) > 0)
        target.close()
        unlink(target.name)

    def test_example_two(self):
        separator = self.state_cls.separator
        states = ['A', 'B',
                  {'name': 'C', 'children': ['1', '2',
                                             {'name': '3', 'children': ['a', 'b', 'c']}]
                   }]

        transitions = [
            ['reset', 'C', 'A'],
            ['reset', 'C%s2' % separator, 'C']  # overwriting parent reset
        ]

        # we rely on auto transitions
        machine = self.stuff.machine_cls(states=states, transitions=transitions, initial='A')

        machine.to_B()  # exit state A, enter state B
        machine.to_C()  # exit B, enter C
        if separator == '_':
            machine.to_C_3_a()
            self.assertTrue(machine.is_C(allow_substates=True))
            self.assertFalse(machine.is_C())
            self.assertTrue(machine.is_C_3(allow_substates=True))
            self.assertFalse(machine.is_C_3())
            self.assertTrue(machine.is_C_3_a())
        else:
            machine.to_C.s3.a()  # enter C↦a; enter C↦3↦a;
            self.assertTrue(machine.is_C(allow_substates=True))
            self.assertFalse(machine.is_C())
            self.assertTrue(machine.is_C.s3(allow_substates=True))
            self.assertFalse(machine.is_C.s3())
            self.assertTrue(machine.is_C.s3.a())
        self.assertEqual(machine.state, 'C{0}3{0}a'.format(separator))
        machine.to('C{0}2'.format(separator))  # exit C↦3↦a, exit C↦3, enter C↦2
        self.assertEqual(machine.state, 'C{0}2'.format(separator))
        machine.reset()  # exit C↦2; reset C has been overwritten by C↦3
        self.assertEqual('C', machine.state)
        machine.reset()  # exit C, enter A
        self.assertEqual('A', machine.state)

    def test_machine_may_transitions(self):
        states = ['A', 'B', {'name': 'C', 'children': ['1', '2', '3']}, 'D']
        transitions = [
            {'trigger': 'walk', 'source': 'A', 'dest': 'B'},
            {'trigger': 'run', 'source': 'B', 'dest': 'C'},
            {'trigger': 'wait', 'source': 'B', 'dest': None},
            {'trigger': 'run_fast', 'source': 'C', 'dest': 'C{0}1'.format(self.separator)},
            {'trigger': 'sprint', 'source': 'C', 'dest': 'D'}
        ]
        m = self.stuff.machine_cls(
            states=states, transitions=transitions, initial='A', auto_transitions=False
        )
        assert m.may_walk()
        assert m.may_trigger("walk")
        assert not m.may_run()
        assert not m.may_trigger("run")
        assert not m.may_run_fast()
        assert not m.may_trigger("run_fast")
        assert not m.may_sprint()
        assert not m.may_trigger("sprint")
        assert not m.may_wait()
        assert not m.may_trigger("wait")

        m.walk()
        assert not m.may_walk()
        assert not m.may_trigger("walk")
        assert m.may_run()
        assert m.may_trigger("run")
        assert not m.may_run_fast()
        assert not m.may_trigger("run_fast")
        assert m.may_wait()
        assert m.may_trigger("wait")

        m.run()
        assert m.may_run_fast()
        assert m.may_trigger("run_fast")
        assert m.may_sprint()
        assert m.may_trigger("sprint")

        m.run_fast()

    def test_remove_nested_transition(self):
        separator = self.state_cls.separator
        state = {
            'name': 'B',
            'children': ['1', '2'],
            'transitions': [['jo', '1', '2']],
            'initial': '2'
        }
        m = self.stuff.machine_cls(initial='A', states=['A', state],
                                   transitions=[['go', 'A', 'B'], ['go', 'B{0}2'.format(separator),
                                                                   'B{0}1'.format(separator)]])
        m.remove_transition("go", "A", "B")
        assert m.get_transitions("go")
        m.remove_transition("go")
        assert not m.get_transitions("go")
        assert m.get_transitions("jo")
        m.remove_transition("jo")
        assert not m.get_transitions("jo")

    def test_add_nested_instances(self):
        if self.separator != '_':
            idle_state = NestedState(name='idle')
            task_machine = self.machine_cls(states=[idle_state], initial=idle_state.name)
            task_state = NestedState('mock_task')
            parent_state = NestedState(name='seq', initial=task_state.name)
            parent_state.add_substate(task_state)
            # add_state breaks in versions greater than 0.9.0
            task_machine.add_state(parent_state)
            task_machine.add_transition('t0', 'idle', parent_state.name)
            task_machine.t0()
            assert not task_machine.is_seq()
            assert task_machine.is_seq(allow_substates=True)
            assert task_machine.is_seq.mock_task()


class TestSeparatorsSlash(TestSeparatorsBase):
    separator = '/'


class TestSeparatorsDot(TestSeparatorsBase):
    separator = '.'


@skipIf(sys.version_info[0] < 3, "Unicode separators are only supported for Python 3")
class TestSeparatorUnicode(TestSeparatorsBase):
    separator = u'↦'
