/*
 * Copyright 2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.statemachine;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.state.DefaultPseudoState;
import org.springframework.statemachine.state.EnumState;
import org.springframework.statemachine.state.PseudoState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.state.StateMachineState;
import org.springframework.statemachine.transition.DefaultExternalTransition;
import org.springframework.statemachine.transition.DefaultLocalTransition;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.trigger.EventTrigger;

public class SubStateMachineTests extends AbstractStateMachineTests {

	@Override
	protected AnnotationConfigApplicationContext buildContext() {
		return new AnnotationConfigApplicationContext();
	}

	@Test
	public void testExternalTransition() throws Exception {
		/**
		 *          +-------------------------------------------+
		 *  *-init->|                    S1                     |
		 *          +-------------------------------------------+
		 *          |  entry/                                   |
		 *          |  exit/                                    |
		 *          |         +--------------------------+      |
		 *          |     *-->|           S11            |      |
		 *          |         +--------------------------+      |
		 *          |         | entry/                   |      |
		 *          |         | exit/                    |      |
		 *          |         |        +-----------+     |      |
		 *          |         |    *-->|   S111    |     |      |
		 *          |         |        +-----------+     |      |
		 *          |         |        | entry/    |     |      |
		 *          |<----E1-----------| exit/     |     |      |
		 *          |         |        |           |     |      |
		 *          |         |        +-----------+     |      |
		 *          |         |                          |      |
		 *          |         +--------------------------+      |
		 *          |                                           |
		 *          +-------------------------------------------+
		 */
		PseudoState<TestStates,TestEvents> pseudoState = new DefaultPseudoState<TestStates,TestEvents>(PseudoStateKind.INITIAL);

		TestEntryAction entryActionS111 = new TestEntryAction("S111");
		TestExitAction exitActionS111 = new TestExitAction("S111");
		Collection<Action<TestStates, TestEvents>> entryActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS111.add(entryActionS111);
		Collection<Action<TestStates, TestEvents>> exitActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS111.add(exitActionS111);
		State<TestStates,TestEvents> stateS111 = new EnumState<TestStates,TestEvents>(TestStates.S111, null, entryActionsS111, exitActionsS111, pseudoState);

		// submachine 11
		Collection<State<TestStates,TestEvents>> substates111 = new ArrayList<State<TestStates,TestEvents>>();
		substates111.add(stateS111);
		Collection<Transition<TestStates,TestEvents>> subtransitions111 = new ArrayList<Transition<TestStates,TestEvents>>();
		ObjectStateMachine<TestStates, TestEvents> submachine11 = new ObjectStateMachine<TestStates, TestEvents>(substates111, subtransitions111, stateS111);

		// submachine 1
		TestEntryAction entryActionS11 = new TestEntryAction("S11");
		TestExitAction exitActionS11 = new TestExitAction("S11");
		Collection<Action<TestStates, TestEvents>> entryActionsS11 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS11.add(entryActionS11);
		Collection<Action<TestStates, TestEvents>> exitActionsS11 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS11.add(exitActionS11);
		StateMachineState<TestStates,TestEvents> stateS11 = new StateMachineState<TestStates,TestEvents>(TestStates.S11, submachine11, null, entryActionsS11, exitActionsS11, pseudoState);

		Collection<State<TestStates,TestEvents>> substates11 = new ArrayList<State<TestStates,TestEvents>>();
		substates11.add(stateS11);
		Collection<Transition<TestStates,TestEvents>> subtransitions11 = new ArrayList<Transition<TestStates,TestEvents>>();
		ObjectStateMachine<TestStates, TestEvents> submachine1 = new ObjectStateMachine<TestStates, TestEvents>(substates11, subtransitions11, stateS11);

		// machine
		TestEntryAction entryActionS1 = new TestEntryAction("S1");
		TestExitAction exitActionS1 = new TestExitAction("S1");
		Collection<Action<TestStates, TestEvents>> entryActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS1.add(entryActionS1);
		Collection<Action<TestStates, TestEvents>> exitActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS1.add(exitActionS1);

		StateMachineState<TestStates,TestEvents> stateS1 = new StateMachineState<TestStates,TestEvents>(TestStates.S1, submachine1, null, entryActionsS1, exitActionsS1, pseudoState);
		Collection<State<TestStates,TestEvents>> states = new ArrayList<State<TestStates,TestEvents>>();
		states.add(stateS1);
		Collection<Transition<TestStates,TestEvents>> transitions = new ArrayList<Transition<TestStates,TestEvents>>();
		DefaultExternalTransition<TestStates,TestEvents> transitionFromS111ToS1 =
				new DefaultExternalTransition<TestStates,TestEvents>(stateS111, stateS1, null, TestEvents.E1, null, new EventTrigger<TestStates,TestEvents>(TestEvents.E1));
		transitions.add(transitionFromS111ToS1);
		ObjectStateMachine<TestStates, TestEvents> machine = new ObjectStateMachine<TestStates, TestEvents>(states, transitions, stateS1);


		SyncTaskExecutor taskExecutor = new SyncTaskExecutor();
		BeanFactory beanFactory = new DefaultListableBeanFactory();
		machine.setTaskExecutor(taskExecutor);
		machine.setBeanFactory(beanFactory);
		machine.afterPropertiesSet();
		submachine1.setTaskExecutor(taskExecutor);
		submachine1.setBeanFactory(beanFactory);
		submachine1.afterPropertiesSet();
		submachine11.setTaskExecutor(taskExecutor);
		submachine11.setBeanFactory(beanFactory);
		submachine11.afterPropertiesSet();
		machine.start();

		machine.sendEvent(TestEvents.E1);

		assertThat(entryActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));

		assertThat(entryActionS111.stateContexts.size(), is(2));
		assertThat(exitActionS111.stateContexts.size(), is(1));
		assertThat(entryActionS11.stateContexts.size(), is(2));
		assertThat(exitActionS11.stateContexts.size(), is(1));
		assertThat(entryActionS1.stateContexts.size(), is(1));
		assertThat(exitActionS1.stateContexts.size(), is(1));
	}

	@Test
	public void testExternalTransition2() throws Exception {

		/**
		 *          +---------------------------------------------------+
		 *  *-init->|                         S1                        |
		 *          +---------------------------------------------------+
		 *          |  entry/                                           |
		 *          |  exit/                                            |
		 *          |            +-----------+          +-----------+   |
		 *          |        *-->|   S111    |          |   S112    |   |
		 *          |            +-----------+          +-----------+   |
		 *          |            | entry/    |          | entry/    |   |
		 *          |            | exit/     |----E1--->| exit/     |   |
		 *          |            |           |          |           |   |
		 *          |            |           |<---E2----|           |   |
		 *          |            |           |          |           |   |
		 *          |            +-----------+          +-----------+   |
		 *          |                                                   |
		 *          +---------------------------------------------------+
		 */


		PseudoState<TestStates,TestEvents> pseudoState = new DefaultPseudoState<TestStates,TestEvents>(PseudoStateKind.INITIAL);

		TestEntryAction entryActionS111 = new TestEntryAction("S111");
		TestExitAction exitActionS111 = new TestExitAction("S111");
		Collection<Action<TestStates, TestEvents>> entryActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS111.add(entryActionS111);
		Collection<Action<TestStates, TestEvents>> exitActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS111.add(exitActionS111);
		State<TestStates,TestEvents> stateS111 = new EnumState<TestStates,TestEvents>(TestStates.S111, null, entryActionsS111, exitActionsS111, pseudoState);

		TestEntryAction entryActionS112 = new TestEntryAction("S112");
		TestExitAction exitActionS112 = new TestExitAction("S112");
		Collection<Action<TestStates, TestEvents>> entryActionsS112 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS112.add(entryActionS112);
		Collection<Action<TestStates, TestEvents>> exitActionsS112 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS112.add(exitActionS112);
		State<TestStates,TestEvents> stateS112 = new EnumState<TestStates,TestEvents>(TestStates.S112, null, entryActionsS112, exitActionsS112, null);

		// submachine 1
		Collection<State<TestStates,TestEvents>> substates11 = new ArrayList<State<TestStates,TestEvents>>();
		substates11.add(stateS111);
		substates11.add(stateS112);
		Collection<Transition<TestStates,TestEvents>> subtransitions11 = new ArrayList<Transition<TestStates,TestEvents>>();
		ObjectStateMachine<TestStates, TestEvents> submachine11 = new ObjectStateMachine<TestStates, TestEvents>(substates11, subtransitions11, stateS111);

		// machine
		TestEntryAction entryActionS1 = new TestEntryAction("S1");
		TestExitAction exitActionS1 = new TestExitAction("S1");
		Collection<Action<TestStates, TestEvents>> entryActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS1.add(entryActionS1);
		Collection<Action<TestStates, TestEvents>> exitActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS1.add(exitActionS1);

		StateMachineState<TestStates,TestEvents> stateS1 = new StateMachineState<TestStates,TestEvents>(TestStates.S1, submachine11, null, entryActionsS1, exitActionsS1, pseudoState);
		Collection<State<TestStates,TestEvents>> states = new ArrayList<State<TestStates,TestEvents>>();
		states.add(stateS1);
		Collection<Transition<TestStates,TestEvents>> transitions = new ArrayList<Transition<TestStates,TestEvents>>();
		DefaultExternalTransition<TestStates,TestEvents> transitionFromS111ToS112 =
				new DefaultExternalTransition<TestStates,TestEvents>(stateS111, stateS112, null, TestEvents.E1, null, new EventTrigger<TestStates,TestEvents>(TestEvents.E1));
		transitions.add(transitionFromS111ToS112);
		ObjectStateMachine<TestStates, TestEvents> machine = new ObjectStateMachine<TestStates, TestEvents>(states, transitions, stateS1);


		SyncTaskExecutor taskExecutor = new SyncTaskExecutor();
		BeanFactory beanFactory = new DefaultListableBeanFactory();
		machine.setTaskExecutor(taskExecutor);
		machine.setBeanFactory(beanFactory);
		machine.afterPropertiesSet();
		submachine11.setTaskExecutor(taskExecutor);
		submachine11.setBeanFactory(beanFactory);
		submachine11.afterPropertiesSet();
		machine.start();

		machine.sendEvent(TestEvents.E1);

		assertThat(entryActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS112.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS112.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false));
		assertThat(entryActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false));

		assertThat(entryActionS111.stateContexts.size(), is(1));
		assertThat(exitActionS111.stateContexts.size(), is(1));
		assertThat(entryActionS112.stateContexts.size(), is(1));
		assertThat(exitActionS112.stateContexts.size(), is(0));
		assertThat(entryActionS1.stateContexts.size(), is(1));
		assertThat(exitActionS1.stateContexts.size(), is(0));
	}


	@Test
	public void testLocalTransition() throws Exception {
		/**
		 *          +-------------------------------------------+
		 *  *-init->|                    S1                     |
		 *          +-------------------------------------------+
		 *          |  entry/                                   |
		 *          |  exit/                                    |
		 *          |         +--------------------------+      |
		 *          |     *-->|           S11            |      |
		 *          |         +--------------------------+      |
		 *          |         | entry/                   |      |
		 *          |         | exit/                    |      |
		 *          |         |        +-----------+     |      |
		 *          |         |    *-->|   S111    |     |      |
		 *          |         |        +-----------+     |      |
		 *          |         |        | entry/    |     |      |
		 *          |<----E1-----------| exit/     |     |      |
		 *          |         |        |           |     |      |
		 *          |         |        +-----------+     |      |
		 *          |         |                          |      |
		 *          |         +--------------------------+      |
		 *          |                                           |
		 *          +-------------------------------------------+
		 */
		PseudoState<TestStates,TestEvents> pseudoState = new DefaultPseudoState<TestStates,TestEvents>(PseudoStateKind.INITIAL);

		TestEntryAction entryActionS111 = new TestEntryAction("S111");
		TestExitAction exitActionS111 = new TestExitAction("S111");
		Collection<Action<TestStates, TestEvents>> entryActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS111.add(entryActionS111);
		Collection<Action<TestStates, TestEvents>> exitActionsS111 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS111.add(exitActionS111);
		State<TestStates,TestEvents> stateS111 = new EnumState<TestStates,TestEvents>(TestStates.S111, null, entryActionsS111, exitActionsS111, pseudoState);

		// submachine 11
		Collection<State<TestStates,TestEvents>> substates111 = new ArrayList<State<TestStates,TestEvents>>();
		substates111.add(stateS111);
		Collection<Transition<TestStates,TestEvents>> subtransitions111 = new ArrayList<Transition<TestStates,TestEvents>>();
		ObjectStateMachine<TestStates, TestEvents> submachine11 = new ObjectStateMachine<TestStates, TestEvents>(substates111, subtransitions111, stateS111);

		// submachine 1
		TestEntryAction entryActionS11 = new TestEntryAction("S11");
		TestExitAction exitActionS11 = new TestExitAction("S11");
		Collection<Action<TestStates, TestEvents>> entryActionsS11 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS11.add(entryActionS11);
		Collection<Action<TestStates, TestEvents>> exitActionsS11 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS11.add(exitActionS11);
		StateMachineState<TestStates,TestEvents> stateS11 = new StateMachineState<TestStates,TestEvents>(TestStates.S11, submachine11, null, entryActionsS11, exitActionsS11, pseudoState);

		Collection<State<TestStates,TestEvents>> substates11 = new ArrayList<State<TestStates,TestEvents>>();
		substates11.add(stateS11);
		Collection<Transition<TestStates,TestEvents>> subtransitions11 = new ArrayList<Transition<TestStates,TestEvents>>();
		ObjectStateMachine<TestStates, TestEvents> submachine1 = new ObjectStateMachine<TestStates, TestEvents>(substates11, subtransitions11, stateS11);

		// machine
		TestEntryAction entryActionS1 = new TestEntryAction("S1");
		TestExitAction exitActionS1 = new TestExitAction("S1");
		Collection<Action<TestStates, TestEvents>> entryActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		entryActionsS1.add(entryActionS1);
		Collection<Action<TestStates, TestEvents>> exitActionsS1 = new ArrayList<Action<TestStates, TestEvents>>();
		exitActionsS1.add(exitActionS1);

		StateMachineState<TestStates,TestEvents> stateS1 = new StateMachineState<TestStates,TestEvents>(TestStates.S1, submachine1, null, entryActionsS1, exitActionsS1, pseudoState);
		Collection<State<TestStates,TestEvents>> states = new ArrayList<State<TestStates,TestEvents>>();
		states.add(stateS1);
		Collection<Transition<TestStates,TestEvents>> transitions = new ArrayList<Transition<TestStates,TestEvents>>();
		DefaultLocalTransition<TestStates,TestEvents> transitionFromS11ToS1 =
				new DefaultLocalTransition<TestStates,TestEvents>(stateS111, stateS1, null, TestEvents.E1, null, new EventTrigger<TestStates,TestEvents>(TestEvents.E1));
		transitions.add(transitionFromS11ToS1);
		ObjectStateMachine<TestStates, TestEvents> machine = new ObjectStateMachine<TestStates, TestEvents>(states, transitions, stateS1);


		SyncTaskExecutor taskExecutor = new SyncTaskExecutor();
		BeanFactory beanFactory = new DefaultListableBeanFactory();
		machine.setTaskExecutor(taskExecutor);
		machine.setBeanFactory(beanFactory);
		machine.afterPropertiesSet();
		submachine1.setTaskExecutor(taskExecutor);
		submachine1.setBeanFactory(beanFactory);
		submachine1.afterPropertiesSet();
		submachine11.setTaskExecutor(taskExecutor);
		submachine11.setBeanFactory(beanFactory);
		submachine11.afterPropertiesSet();
		machine.start();

		machine.sendEvent(TestEvents.E1);

		assertThat(entryActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false));

		assertThat(entryActionS111.stateContexts.size(), is(2));
		assertThat(exitActionS111.stateContexts.size(), is(1));
		assertThat(entryActionS11.stateContexts.size(), is(1));
		assertThat(exitActionS11.stateContexts.size(), is(1));
		assertThat(entryActionS1.stateContexts.size(), is(1));
		assertThat(exitActionS1.stateContexts.size(), is(0));
	}

	@Test
	public void testExternalTransition3() throws Exception {
		context.register(BaseConfig.class, Config1.class);
		context.refresh();
		assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
		@SuppressWarnings("unchecked")
		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);

		machine.start();

		machine.sendEvent(TestEvents.E1);

		TestEntryAction entryActionS111 = context.getBean("entryActionS111", TestEntryAction.class);
		TestExitAction exitActionS111 = context.getBean("exitActionS111", TestExitAction.class);
		TestEntryAction entryActionS11 = context.getBean("entryActionS11", TestEntryAction.class);
		TestExitAction exitActionS11 = context.getBean("exitActionS11", TestExitAction.class);
		TestEntryAction entryActionS1 = context.getBean("entryActionS1", TestEntryAction.class);
		TestExitAction exitActionS1 = context.getBean("exitActionS1", TestExitAction.class);

		assertThat(entryActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS111.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS11.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(entryActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
		assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));

		assertThat(entryActionS111.stateContexts.size(), is(2));
		assertThat(exitActionS111.stateContexts.size(), is(1));
		assertThat(entryActionS11.stateContexts.size(), is(2));
		assertThat(exitActionS11.stateContexts.size(), is(1));
		assertThat(entryActionS1.stateContexts.size(), is(1));
		assertThat(exitActionS1.stateContexts.size(), is(1));

	}

	@Test
	public void testMixedStates() throws Exception {
		context.register(BaseConfig.class, Config2.class);
		context.refresh();
		assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
		@SuppressWarnings("unchecked")
		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		machine.start();
		assertThat(machine, notNullValue());

		assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S10));
	}

	@Test
	public void testStateChangeWithinMachine() {
		context.register(BaseConfig.class, Config3.class);
		context.refresh();
		assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
		@SuppressWarnings("unchecked")
		ObjectStateMachine<TestStates2,TestEvents2> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		machine.start();
		assertThat(machine, notNullValue());
		assertThat(machine.getState().getIds(), contains(TestStates2.IDLE, TestStates2.CLOSED));
		machine.sendEvent(TestEvents2.EJECT);
		assertThat(machine.getState().getIds(), contains(TestStates2.IDLE, TestStates2.OPEN));
	}

	@Configuration
	@EnableStateMachine
	public static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {

		@Override
		public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
			states
				.withStates()
					.initial(TestStates.S1)
					.state(TestStates.S1, Arrays.asList(entryActionS1()), Arrays.asList(exitActionS1()))
					.and()
					.withStates()
						.parent(TestStates.S1)
						.initial(TestStates.S11)
						.state(TestStates.S11, Arrays.asList(entryActionS11()), Arrays.asList(exitActionS11()))
						.and()
						.withStates()
							.parent(TestStates.S11)
							.initial(TestStates.S111)
							.state(TestStates.S111, Arrays.asList(entryActionS111()), Arrays.asList(exitActionS111()));
		}

		@Override
		public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
			transitions
				.withExternal()
					.source(TestStates.S111)
					.target(TestStates.S1)
					.event(TestEvents.E1);
		}

		@Bean(name = "entryActionS111")
		public Action<TestStates, TestEvents> entryActionS111() {
			return new TestEntryAction("S111");
		}

		@Bean(name = "exitActionS111")
		public Action<TestStates, TestEvents> exitActionS111() {
			return new TestExitAction("S111");
		}

		@Bean(name = "entryActionS11")
		public Action<TestStates, TestEvents> entryActionS11() {
			return new TestEntryAction("S11");
		}

		@Bean(name = "exitActionS11")
		public Action<TestStates, TestEvents> exitActionS11() {
			return new TestExitAction("S11");
		}

		@Bean(name = "entryActionS1")
		public Action<TestStates, TestEvents> entryActionS1() {
			return new TestEntryAction("S1");
		}

		@Bean(name = "exitActionS1")
		public Action<TestStates, TestEvents> exitActionS1() {
			return new TestExitAction("S1");
		}

	}

	@Configuration
	@EnableStateMachine
	static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {

		@Override
		public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
			config
				.withVerifier()
					.enabled(false);
		}

		@Override
		public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
			states
				.withStates()
					.initial(TestStates.S1)
					.state(TestStates.S1)
					.state(TestStates.S2)
					.and()
					.withStates()
						.parent(TestStates.S1)
						.initial(TestStates.S10)
						.state(TestStates.S10);
		}

	}

	@Configuration
	@EnableStateMachine
	static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates2, TestEvents2> {

		@Override
		public void configure(StateMachineStateConfigurer<TestStates2, TestEvents2> states) throws Exception {
			states
				.withStates()
					.initial(TestStates2.IDLE)
					.state(TestStates2.IDLE)
					.and()
					.withStates()
						.parent(TestStates2.IDLE)
						.initial(TestStates2.CLOSED)
						.state(TestStates2.CLOSED)
						.state(TestStates2.OPEN)
						.and()
				.withStates()
					.state(TestStates2.BUSY)
					.and()
					.withStates()
						.parent(TestStates2.BUSY)
						.initial(TestStates2.PLAYING)
						.state(TestStates2.PLAYING)
						.state(TestStates2.PAUSED);

		}

		@Override
		public void configure(StateMachineTransitionConfigurer<TestStates2, TestEvents2> transitions) throws Exception {
			transitions
				.withExternal()
					.source(TestStates2.CLOSED)
					.target(TestStates2.OPEN)
					.event(TestEvents2.EJECT)
					.and()
				.withExternal()
					.source(TestStates2.OPEN)
					.target(TestStates2.CLOSED)
					.event(TestEvents2.EJECT);
		}

	}

}
