/*
 * Copyright 2015-2020 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
 *
 * https://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.annotation;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.EnumSet;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.ObjectStateMachine;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
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.config.configurers.StateConfigurer.History;

public class MethodAnnotationTests extends AbstractStateMachineTests {

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

	@Test
	@SuppressWarnings("unchecked")
	public void testOnTransition() throws Exception {
		context.register(BeanConfig1.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		Bean1 bean1 = context.getBean(Bean1.class);

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);
		machine.start();

		assertThat(bean1.onTransitionFromS1ToS2Latch.await(1, TimeUnit.SECONDS)).isFalse();
		assertThat(bean1.onTransitionLatch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onTransitionFromS1ToS2Count).isZero();
		assertThat(bean1.onTransitionCount).isEqualTo(1);

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());

		assertThat(bean1.onTransitionFromS1ToS2Latch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onTransitionLatch.await(1, TimeUnit.SECONDS)).isTrue();

		assertThat(bean1.onTransitionFromS1ToS2Count).isEqualTo(1);
		assertThat(bean1.onTransitionCount).isEqualTo(1);
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testOnStateChanged() throws Exception {
		context.register(BeanConfig1.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		Bean1 bean1 = context.getBean(Bean1.class);

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);
		machine.start();

		assertThat(bean1.onStateChangedFromS1ToS2Latch.await(1, TimeUnit.SECONDS)).isFalse();
		assertThat(bean1.onStateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onStateChangedFromS1ToS2Count).isZero();
		assertThat(bean1.onStateChangedCount).isEqualTo(1);

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);

		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());

		assertThat(bean1.onStateChangedFromS1ToS2Latch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onStateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onStateChangedFromS1ToS2Count).isEqualTo(1);
		assertThat(bean1.onStateChangedCount).isEqualTo(1);
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testOnStateMachineStartStop() throws Exception {
		context.register(BeanConfig1.class, Config1.class);
		context.refresh();
		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		Bean1 bean1 = context.getBean(Bean1.class);

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);

		machine.start();
		assertThat(bean1.onStateMachineStartLatch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onStateMachineStartCount).isEqualTo(1);
		assertThat(bean1.onStateMachineStopLatch.await(1, TimeUnit.SECONDS)).isFalse();
		assertThat(bean1.onStateMachineStopCount).isZero();

		bean1.reset(1, 1, 1, 1, 1, 1, 1, 1);
		machine.stop();
		assertThat(bean1.onStateMachineStartLatch.await(1, TimeUnit.SECONDS)).isFalse();
		assertThat(bean1.onStateMachineStartCount).isZero();
		assertThat(bean1.onStateMachineStopLatch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean1.onStateMachineStopCount).isEqualTo(1);
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testOnExtendedStateChanged() throws Exception {
		context.register(BeanConfig5.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		Bean5 bean5 = context.getBean(Bean5.class);
		machine.start();
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("V1", "V1val").build());

		assertThat(bean5.onExtendedStateChanged1Latch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean5.onExtendedStateChanged1Count).isEqualTo(1);

		assertThat(bean5.onExtendedStateChanged2Latch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean5.onExtendedStateChanged2Count).isEqualTo(1);
		assertThat(bean5.onExtendedStateChanged2Value).isEqualTo("V1val");

		assertThat(bean5.onExtendedStateChangedKeyV1Latch.await(1, TimeUnit.SECONDS)).isTrue();
		assertThat(bean5.onExtendedStateChangedKeyV1Count).isEqualTo(1);

		assertThat(bean5.onExtendedStateChangedKeyV2Latch.await(1, TimeUnit.SECONDS)).isFalse();
		assertThat(bean5.onExtendedStateChangedKeyV2Count).isZero();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations2() throws Exception {
		context.register(BeanConfig2.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		machine.start();

		Bean2 bean2 = context.getBean(Bean2.class);

		// this event should cause 'method1' to get called
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1)
				.setHeader("foo", "jee")
				.setHeader("bar1", "jee1")
				.setHeader("bar2", "jee2")
				.setHeader("bar3", "jee3")
				.build());
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).build());

		assertThat(bean2.onMethod1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean2.headers).isNotNull();
		assertThat((String)bean2.headers.get("foo")).isEqualTo("jee");
		assertThat(bean2.extendedState).isNotNull();

		assertThat(bean2.onMethod2Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean2.variable).isNotNull();
		assertThat((String)bean2.variable).isEqualTo("jee");

		assertThat((String)bean2.fooHeader).isEqualTo("jee");
		assertThat((String)bean2.bar1Header).isEqualTo("jee1");
		assertThat((String)bean2.bar2Header).isEqualTo("jee2");
		assertThat(bean2.bar3Header).isEqualTo("jee3");
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations3() throws Exception {
		context.register(BeanConfig3.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		machine.start();

		Bean3 bean3 = context.getBean(Bean3.class);

		// this event should cause 'method1' to get called
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());

		assertThat(bean3.onStateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations4() throws Exception {
		context.register(BeanConfig4.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		assertThat(context.containsBean("fooMachine")).isTrue();
		Bean4 bean4 = context.getBean(Bean4.class);
		machine.start();

		assertThat(bean4.onStateEntryLatch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean4.onStateExitLatch.await(2, TimeUnit.SECONDS)).isFalse();
		assertThat(bean4.onStateEntryCount).isEqualTo(1);
		assertThat(bean4.onStateExitCount).isZero();

		bean4.reset(1, 1);

		// this event should cause 'method1' to get called
		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
		assertThat(bean4.onStateEntryLatch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean4.onStateExitLatch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean4.onStateEntryCount).isEqualTo(1);
		assertThat(bean4.onStateExitCount).isEqualTo(1);
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations5() throws Exception {
		context.register(BeanConfig6.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean6 bean6 = context.getBean(Bean6.class);
		machine.start();

		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E4).build());
		assertThat(bean6.onEventNotAccepted1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean6.onEventNotAccepted2Latch.await(2, TimeUnit.SECONDS)).isFalse();
		assertThat(bean6.onEventNotAccepted3Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean6.onEventNotAccepted4Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean6.onEventNotAccepted4Message).isNotNull();
		assertThat(bean6.onEventNotAccepted4Message.getPayload()).isEqualTo(TestEvents.E4);
		assertThat(bean6.onEventNotAccepted5Latch.await(2, TimeUnit.SECONDS)).isTrue();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations6() throws Exception {
		context.register(BeanConfig7.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean7 bean7 = context.getBean(Bean7.class);
		machine.start();

		machine.sendEvent(MessageBuilder.withPayload(TestEvents.E4).build());

		machine.setStateMachineError(new RuntimeException());

		assertThat(bean7.OnStateMachineError1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean7.OnStateMachineError2Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean7.OnStateMachineError2Exception).isNotNull();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations7() throws Exception {
		context.register(BeanConfig8.class, Config1.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean8 bean8 = context.getBean(Bean8.class);
		machine.start();

		machine.sendEvent(MessageBuilder.withPayload(TestEvents.EF).build());

		assertThat(bean8.OnTransition1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean8.OnTransition2Latch.await(2, TimeUnit.SECONDS)).isTrue();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations8() throws Exception {
		context.register(BeanConfig9.class, Config2.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean9 bean9 = context.getBean(Bean9.class);
		machine.start();

		machine.sendEvent(TestEvents.E1);
		machine.sendEvent(TestEvents.E2);
		machine.sendEvent(TestEvents.E3);
		machine.sendEvent(TestEvents.E4);

		assertThat(machine.getState().getIds()).containsExactly(TestStates.S2, TestStates.S21);
		assertThat(bean9.OnStateEntry1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean9.OnTransition1Latch.await(2, TimeUnit.SECONDS)).isTrue();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations9() throws Exception {
		context.register(BeanConfig10.class, Config3.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean10 bean10 = context.getBean(Bean10.class);
		machine.start();

		machine.sendEvent(TestEvents.E1);
		machine.sendEvent(TestEvents.E2);
		machine.sendEvent(TestEvents.E3);
		machine.sendEvent(TestEvents.E4);

		assertThat(machine.getState().getIds()).containsExactly(TestStates.S2, TestStates.S21);
		assertThat(bean10.OnStateEntry1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean10.OnTransition1Latch.await(2, TimeUnit.SECONDS)).isTrue();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testMethodAnnotations10() throws Exception {
		context.register(BeanConfig11.class, Config4.class);
		context.refresh();

		ObjectStateMachine<TestStates,TestEvents> machine =
				context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
		Bean11 bean11 = context.getBean(Bean11.class);
		machine.start();

		machine.sendEvent(TestEvents.E1);

		assertThat(machine.getState().getIds()).containsExactly(TestStates.S2);
		assertThat(bean11.OnTransition1Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean11.OnTransition2Latch.await(2, TimeUnit.SECONDS)).isTrue();
		assertThat(bean11.count1).isEqualTo(1);
		assertThat(bean11.count2).isEqualTo(1);
	}

	@WithStateMachine
	static class Bean1 {
		CountDownLatch onTransitionFromS1ToS2Latch = new CountDownLatch(1);
		CountDownLatch onTransitionLatch = new CountDownLatch(1);
		CountDownLatch onStateChangedFromS1ToS2Latch = new CountDownLatch(1);
		CountDownLatch onStateChangedLatch = new CountDownLatch(1);
		CountDownLatch onTransitionEndLatch = new CountDownLatch(1);
		CountDownLatch onTransitionStartLatch = new CountDownLatch(1);
		CountDownLatch onStateMachineStartLatch = new CountDownLatch(1);
		CountDownLatch onStateMachineStopLatch = new CountDownLatch(1);
		int onTransitionFromS1ToS2Count = 0;
		int onTransitionCount = 0;
		int onStateChangedFromS1ToS2Count = 0;
		int onStateChangedCount = 0;
		int onTransitionEndCount = 0;
		int onTransitionStartCount = 0;
		int onStateMachineStartCount = 0;
		int onStateMachineStopCount = 0;

		@OnTransition(source = "S1", target = "S2")
		public void onTransitionFromS1ToS2() {
			onTransitionFromS1ToS2Count++;
			onTransitionFromS1ToS2Latch.countDown();
		}

		@OnTransition
		public void onTransition() {
			onTransitionCount++;
			onTransitionLatch.countDown();
		}

		@OnTransitionEnd
		public void onTransitionEnd() {
			onTransitionEndCount++;
			onTransitionEndLatch.countDown();
		}

		@OnTransitionStart
		public void onTransitionStart() {
			onTransitionStartCount++;
			onTransitionStartLatch.countDown();
		}

		@OnStateChanged(source = "S1", target = "S2")
		public void onStateChangedFromS1ToS2() {
			onStateChangedFromS1ToS2Count++;
			onStateChangedFromS1ToS2Latch.countDown();
		}

		@OnStateChanged
		public void onStateChanged() {
			onStateChangedCount++;
			onStateChangedLatch.countDown();
		}

		@OnStateMachineStart
		public void onStateMachineStart() {
			onStateMachineStartLatch.countDown();
			onStateMachineStartCount++;
		}

		@OnStateMachineStart
		public void onStateMachineStartWithParam(StateMachine<TestStates,TestEvents> machine) {
		}

		@OnStateMachineStop
		public void onStateMachineStop() {
			onStateMachineStopLatch.countDown();
			onStateMachineStopCount++;
		}

		@OnStateMachineStop
		public void onStateMachineStopWithParam(StateMachine<TestStates,TestEvents> machine) {
		}

		public void reset(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) {
			onTransitionFromS1ToS2Latch = new CountDownLatch(a1);
			onTransitionLatch = new CountDownLatch(a2);
			onStateChangedFromS1ToS2Latch = new CountDownLatch(a3);
			onStateChangedLatch = new CountDownLatch(a4);
			onTransitionEndLatch = new CountDownLatch(a5);
			onTransitionStartLatch = new CountDownLatch(a6);
			onStateMachineStartLatch = new CountDownLatch(a7);
			onStateMachineStopLatch = new CountDownLatch(a8);
			onTransitionFromS1ToS2Count = 0;
			onTransitionCount = 0;
			onStateChangedFromS1ToS2Count = 0;
			onStateChangedCount = 0;
			onTransitionEndCount = 0;
			onTransitionStartCount = 0;
			onStateMachineStartCount = 0;
			onStateMachineStopCount = 0;
		}

	}

	@WithStateMachine
	static class Bean2 {

		CountDownLatch onMethod1Latch = new CountDownLatch(1);
		CountDownLatch onMethod2Latch = new CountDownLatch(1);
		Map<String, Object> headers;
		ExtendedState extendedState;
		Object variable;
		Object fooHeader;
		Object bar1Header;
		Object bar2Header;
		String bar3Header;

		@OnTransition(source = "S1", target = "S2")
		public void method1(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
			this.headers = headers;
			extendedState.getVariables().put("foo", "jee");
			this.extendedState = extendedState;
			onMethod1Latch.countDown();
		}

		@OnTransition(source = "S2", target = "S3")
		public void method2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
			variable = extendedState.getVariables().get("foo");
			onMethod2Latch.countDown();
		}

		@OnTransition(source = "S1", target = "S2")
		public void method3(@EventHeader(name = "foo") Object header) {
			this.fooHeader = header;
		}

		@OnTransition(source = "S1", target = "S2")
		public void method4(@EventHeader(name = "bar1") Object header1, @EventHeader(value = "bar2") Object header2,
				@EventHeader(value = "bar3") String header3) {
			this.bar1Header = header1;
			this.bar2Header = header2;
			this.bar3Header = header3;
		}
	}

	@WithStateMachine
	static class Bean3 {

		CountDownLatch onStateChangedLatch = new CountDownLatch(1);

		@OnStateChanged
		public void onStateChanged() {
			onStateChangedLatch.countDown();
		}

	}

	@WithStateMachine
	static class Bean4 {

		CountDownLatch onStateEntryLatch = new CountDownLatch(1);
		CountDownLatch onStateExitLatch = new CountDownLatch(1);
		int onStateEntryCount = 0;
		int onStateExitCount = 0;

		@OnStateEntry
		public void onStateEntry() {
			onStateEntryCount++;
			onStateEntryLatch.countDown();
		}

		@OnStateExit
		public void onStateExit() {
			onStateExitCount++;
			onStateExitLatch.countDown();
		}

		public void reset(int a1, int a2) {
			onStateEntryCount = 0;
			onStateExitCount = 0;
			onStateEntryLatch = new CountDownLatch(a1);
			onStateExitLatch = new CountDownLatch(a2);
		}

	}

	@WithStateMachine
	static class Bean5 {

		CountDownLatch onExtendedStateChanged1Latch = new CountDownLatch(1);
		CountDownLatch onExtendedStateChanged2Latch = new CountDownLatch(1);
		CountDownLatch onExtendedStateChangedKeyV1Latch = new CountDownLatch(1);
		CountDownLatch onExtendedStateChangedKeyV2Latch = new CountDownLatch(1);
		int onExtendedStateChanged1Count = 0;
		int onExtendedStateChanged2Count = 0;
		Object onExtendedStateChanged2Value = null;
		int onExtendedStateChangedKeyV1Count = 0;
		int onExtendedStateChangedKeyV2Count = 0;

		@OnExtendedStateChanged
		public void onExtendedStateChanged1() {
			onExtendedStateChanged1Count++;
			onExtendedStateChanged1Latch.countDown();
		}

		@OnExtendedStateChanged
		public void onExtendedStateChanged2(@ExtendedStateVariable("V1") Object value) {
			onExtendedStateChanged2Value = value;
			onExtendedStateChanged2Count++;
			onExtendedStateChanged2Latch.countDown();
		}

		@OnExtendedStateChanged(key = "V1")
		public void onExtendedStateChangedKeyV1() {
			onExtendedStateChangedKeyV1Count++;
			onExtendedStateChangedKeyV1Latch.countDown();
		}

		@OnExtendedStateChanged(key = "V2")
		public void onExtendedStateChangedKeyV2() {
			onExtendedStateChangedKeyV2Count++;
			onExtendedStateChangedKeyV2Latch.countDown();
		}

		public void reset(int a1, int a2, int a3) {
			onExtendedStateChanged1Latch = new CountDownLatch(a1);
			onExtendedStateChanged2Latch = new CountDownLatch(a2);
			onExtendedStateChangedKeyV2Latch = new CountDownLatch(a3);
			onExtendedStateChanged1Count = 0;
			onExtendedStateChanged2Count = 0;
			onExtendedStateChanged2Value = null;
			onExtendedStateChangedKeyV2Count = 0;
		}

	}

	@WithStateMachine
	static class Bean6 {
		CountDownLatch onEventNotAccepted1Latch = new CountDownLatch(1);
		CountDownLatch onEventNotAccepted2Latch = new CountDownLatch(1);
		CountDownLatch onEventNotAccepted3Latch = new CountDownLatch(1);
		CountDownLatch onEventNotAccepted4Latch = new CountDownLatch(1);
		CountDownLatch onEventNotAccepted5Latch = new CountDownLatch(1);
		Message<TestEvents> onEventNotAccepted4Message;

		@OnEventNotAccepted
		public void onEventNotAccepted1() {
			onEventNotAccepted1Latch.countDown();
		}

		@OnEventNotAccepted(event = "E1")
		public void onEventNotAccepted2() {
			onEventNotAccepted2Latch.countDown();
		}

		@OnEventNotAccepted(event = "E4")
		public void onEventNotAccepted3() {
			onEventNotAccepted3Latch.countDown();
		}

		@OnEventNotAccepted()
		public void onEventNotAccepted4(Message<TestEvents> message) {
			onEventNotAccepted4Message = message;
			onEventNotAccepted4Latch.countDown();
		}

		@OnEventNotAccepted(event = {"E1", "E4"})
		public void onEventNotAccepted5() {
			onEventNotAccepted5Latch.countDown();
		}

		void reset() {
			onEventNotAccepted1Latch = new CountDownLatch(1);
			onEventNotAccepted2Latch = new CountDownLatch(1);
			onEventNotAccepted3Latch = new CountDownLatch(1);
			onEventNotAccepted4Latch = new CountDownLatch(1);
			onEventNotAccepted4Message = null;
		}
	}

	@WithStateMachine
	static class Bean7 {
		CountDownLatch OnStateMachineError1Latch = new CountDownLatch(1);
		CountDownLatch OnStateMachineError2Latch = new CountDownLatch(1);
		Exception OnStateMachineError2Exception;

		@OnStateMachineError
		public void OnStateMachineError1() {
			OnStateMachineError1Latch.countDown();
		}

		@OnStateMachineError
		public void OnStateMachineError2(Exception e) {
			OnStateMachineError2Exception = e;
			OnStateMachineError2Latch.countDown();
		}

	}

	@WithStateMachine
	static class Bean8 {
		CountDownLatch OnTransition1Latch = new CountDownLatch(2);
		CountDownLatch OnTransition2Latch = new CountDownLatch(1);

		@OnTransition
		public void OnTransition1() {
			OnTransition1Latch.countDown();
		}

		@OnTransition(target = "SF")
		public void OnTransition2() {
			OnTransition2Latch.countDown();
		}
	}

	@WithStateMachine
	static class Bean9 {
		CountDownLatch OnTransition1Latch = new CountDownLatch(1);
		CountDownLatch OnStateEntry1Latch = new CountDownLatch(2);

		@OnTransition(target = "S21")
		public void OnTransition1() {
			OnTransition1Latch.countDown();
		}

		@OnStateEntry(target = "S21")
		public void OnStateEntry1() {
			OnStateEntry1Latch.countDown();
		}
	}

	@WithStateMachine
	static class Bean10 {
		CountDownLatch OnTransition1Latch = new CountDownLatch(1);
		CountDownLatch OnStateEntry1Latch = new CountDownLatch(2);

		@OnTransition(target = "S21")
		public void OnTransition1() {
			OnTransition1Latch.countDown();
		}

		@OnStateEntry(target = "S21")
		public void OnStateEntry1() {
			OnStateEntry1Latch.countDown();
		}
	}

	@WithStateMachine(id="myMachine")
	static class Bean11 {
		CountDownLatch OnTransition1Latch = new CountDownLatch(1);
		CountDownLatch OnTransition2Latch = new CountDownLatch(1);
		volatile int count1 = 0;
		volatile int count2 = 0;

		@OnTransition(target = "S1")
		public void OnTransition1() {
			count1++;
			OnTransition1Latch.countDown();
		}

		@OnStateEntry(target = "S2")
		public void OnTransition2() {
			count2++;
			OnTransition2Latch.countDown();
		}
	}

	@Configuration
	static class BeanConfig1 {

		@Bean
		public Bean1 bean1() {
			return new Bean1();
		}

	}

	@Configuration
	static class BeanConfig2 {

		@Bean
		public Bean2 bean2() {
			return new Bean2();
		}

	}

	@Configuration
	static class BeanConfig3 {

		@Bean
		public Bean3 bean3() {
			return new Bean3();
		}

	}

	@Configuration
	static class BeanConfig4 {

		@Bean
		public Bean4 bean4() {
			return new Bean4();
		}

	}

	@Configuration
	static class BeanConfig5 {

		@Bean
		public Bean5 bean5() {
			return new Bean5();
		}

	}

	@Configuration
	static class BeanConfig6 {

		@Bean
		public Bean6 bean6() {
			return new Bean6();
		}

	}

	@Configuration
	static class BeanConfig7 {

		@Bean
		public Bean7 bean7() {
			return new Bean7();
		}

	}

	@Configuration
	static class BeanConfig8 {

		@Bean
		public Bean8 bean8() {
			return new Bean8();
		}

	}

	@Configuration
	static class BeanConfig9 {

		@Bean
		public Bean9 bean9() {
			return new Bean9();
		}

	}

	@Configuration
	static class BeanConfig10 {

		@Bean
		public Bean10 bean10() {
			return new Bean10();
		}

	}

	@Configuration
	static class BeanConfig11 {

		@Bean
		public Bean11 bean11() {
			return new Bean11();
		}

	}

	@Configuration
	@EnableStateMachine(name = {StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, "fooMachine"})
	static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {

		@Override
		public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
			states
				.withStates()
					.initial(TestStates.S1)
					.end(TestStates.SF)
					.states(EnumSet.allOf(TestStates.class));
		}

		@Override
		public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
			transitions
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.S2)
					.event(TestEvents.E1)
					.guard(testGuard())
					.action(testAction())
					.action(extendedStateAction())
					.and()
				.withExternal()
					.source(TestStates.S2)
					.target(TestStates.S3)
					.event(TestEvents.E2)
					.and()
				.withExternal()
					.source(TestStates.S3)
					.target(TestStates.S4)
					.event(TestEvents.E3)
					.and()
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.SF)
					.event(TestEvents.EF);
		}

		@Bean
		public TestGuard testGuard() {
			return new TestGuard();
		}

		@Bean
		public TestAction testAction() {
			return new TestAction();
		}

		@Bean
		public Action<TestStates, TestEvents> extendedStateAction() {
			return new Action<TestStates, TestEvents>() {

				@Override
				public void execute(StateContext<TestStates, TestEvents> context) {
					String e1 = context.getMessageHeaders().get("V1", String.class);
					if (e1 != null) {
						context.getExtendedState().getVariables().put("V1", e1);
					}
				}
			};
		}

	}

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

		@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.S2)
						.initial(TestStates.S20)
						.state(TestStates.S20)
						.state(TestStates.S21)
						.history(TestStates.SH, History.SHALLOW);

		}

		@Override
		public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
			transitions
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.S2)
					.event(TestEvents.E1)
					.and()
				.withExternal()
					.source(TestStates.S20)
					.target(TestStates.S21)
					.event(TestEvents.E2)
					.and()
				.withExternal()
					.source(TestStates.S2)
					.target(TestStates.S1)
					.event(TestEvents.E3)
					.and()
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.SH)
					.event(TestEvents.E4);
		}

	}

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

		@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.S2)
						.initial(TestStates.S20)
						.state(TestStates.S20)
						.end(TestStates.S21)
						.history(TestStates.SH, History.SHALLOW);

		}

		@Override
		public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
			transitions
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.S2)
					.event(TestEvents.E1)
					.and()
				.withExternal()
					.source(TestStates.S20)
					.target(TestStates.S21)
					.event(TestEvents.E2)
					.and()
				.withExternal()
					.source(TestStates.S2)
					.target(TestStates.S1)
					.event(TestEvents.E3)
					.and()
				.withExternal()
					.source(TestStates.S1)
					.target(TestStates.SH)
					.event(TestEvents.E4);
		}

	}

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

		@Override
		public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
			config.withConfiguration()
				.machineId("myMachine");
		}

		@Override
		public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
			states.withStates()
				.initial(TestStates.S1)
				.states(EnumSet.allOf(TestStates.class));
		}

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

}
