/*
 * Copyright 2016 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.data;

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

import java.util.Iterator;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineModelConfigurer;
import org.springframework.statemachine.config.model.StateMachineModelFactory;
import org.springframework.statemachine.data.support.StateMachineJackson2RepositoryPopulatorFactoryBean;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.test.StateMachineTestPlan;
import org.springframework.statemachine.test.StateMachineTestPlanBuilder;
import org.springframework.util.ObjectUtils;

/**
 * Base repository config tests.
 *
 * @author Janne Valkealahti
 */
public abstract class AbstractRepositoryTests {

	protected AnnotationConfigApplicationContext context;

	@Before
	public void setup() {
		cleanInternal();
		context = buildContext();
	}

	@After
	public void clean() {
		if (context != null) {
			context.close();
		}
		context = null;
	}

	protected void cleanInternal() {
	}

	protected Class<?>[] getRegisteredClasses() {
		return new Class<?>[0];
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine2() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config2.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("E1").expectStates("S2").and()
					.step().sendEvent("E2").expectStates("S3").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine3() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config3.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("E1").expectStates("S2", "S20").and()
					.step().sendEvent("E2").expectStates("S2", "S21").and()
					.step().sendEvent("E3").expectStates("S1").and()
					.step().sendEvent("E4").expectStates("S2", "S21").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine4() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config4.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("E1").expectStates("S2", "S20").and()
					.step().sendEvent("E2").expectStates("S2", "S21").and()
					.step().sendEvent("E3").expectStates("S1").and()
					.step().sendEvent("E4").expectStates("S2", "S21").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine5() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config5.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S10", "S20").and()
					.step().sendEvent("E1").expectStates("S11", "S21").and()
					.step().sendEvent("E2").expectStates("S10", "S21").and()
					.step().sendEvent("E3").expectStates("S10", "S20").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine6First() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config6.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s30").build()).expectStates("S30").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine6Then1() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config6.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s31").build()).expectStates("S31").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine6Then2() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config6.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s32").build()).expectStates("S32").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine6Last() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config6.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").build()).expectStates("S33").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine8First() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config8.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("junction", "s30").build()).expectStates("S30").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine8Then1() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config8.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("junction", "s31").build()).expectStates("S31").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine8Then2() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config8.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("junction", "s32").build()).expectStates("S32").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine8Last() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config8.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent(MessageBuilder.withPayload("E1").build()).expectStates("S33").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine9() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config9.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("ENTRY1").expectStates("S2", "S22").and()
					.step().sendEvent("EXIT1").expectStates("S4").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine10() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config10.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent("E1").expectStates("S2", "S21", "S31").and()
					.build();
		plan.test();

		State<String, String> endState = null;
		Iterator<State<String, String>> iterator = stateMachine.getStates().iterator();
		while (iterator.hasNext()) {
			State<String, String> next = iterator.next();
			if (next.getId().equals("SF")) {
				endState = next;
				break;
			}
		}
		assertThat(endState, notNullValue());
		assertThat(endState.getPseudoState(), notNullValue());
		assertThat(endState.getPseudoState().getKind(), is(PseudoStateKind.END));
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine11() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config11.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("SI").and()
					.step().sendEvent("E1").expectStates("S2", "S20", "S30").and()
					.step().sendEvent("E2").expectStates("S2", "S21", "S30").and()
					.step().sendEvent("E3").expectStates("S4").and()
					.step().sendEvent("E4").expectStates("SI").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine12() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config12.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("READY").and()
					.step().sendEvent("E3").expectStates("S3").and()
					.step().sendEvent("E1").expectStates("S3").and()
					.step().sendEvent("E6").expectStates("S1").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine13() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config13.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("E1").expectStates("S2", "S21").and()
					.step().sendEvent("E3").expectStates("S2", "S22").and()
					.step().sendEvent("E2").expectStates("S3").and()
					.build();
		plan.test();
	}

	@SuppressWarnings("unchecked")
	@Test
	public void testMachine14() throws Exception {
		context.register(getRegisteredClasses());
		context.register(Config14.class, FactoryConfig.class);
		context.refresh();
		StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
		StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();

		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").expectVariable("foo", 0).and()
					.step().sendEvent("E1").expectStates("S2").and()
					.build();
		plan.test();
	}

	@Configuration
	public static class Config2 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data2.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config3 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data3.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config4 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data4.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config5 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data5.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config6 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data6.json")});
			return factoryBean;
		}

		@Bean
		public Guard<String, String> s30Guard() {
			return new ChoiceGuard("s30");
		}

		@Bean
		public Guard<String, String> s31Guard() {
			return new ChoiceGuard("s31");
		}

		@Bean
		public Guard<String, String> s32Guard() {
			return new ChoiceGuard("s32");
		}

	}

	@Configuration
	public static class Config7 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data7.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config8 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data8.json")});
			return factoryBean;
		}

		@Bean
		public Guard<String, String> s30Guard() {
			return new JunctionGuard("s30");
		}

		@Bean
		public Guard<String, String> s31Guard() {
			return new JunctionGuard("s31");
		}

		@Bean
		public Guard<String, String> s32Guard() {
			return new JunctionGuard("s32");
		}

	}

	@Configuration
	public static class Config9 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data9.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config10 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data10.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config11 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data11.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config12 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data12.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config13 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data13.json")});
			return factoryBean;
		}
	}

	@Configuration
	public static class Config14 {

		@Bean
		public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
			StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
			factoryBean.setResources(new Resource[]{new ClassPathResource("data14.json")});
			return factoryBean;
		}
	}

	@Configuration
	@EnableStateMachineFactory
	public static class FactoryConfig extends StateMachineConfigurerAdapter<String, String> {

		@Autowired
		private StateRepository<? extends RepositoryState> stateRepository;

		@Autowired
		private TransitionRepository<? extends RepositoryTransition> transitionRepository;

		@Override
		public void configure(StateMachineModelConfigurer<String, String> model) throws Exception {
			model
				.withModel()
					.factory(modelFactory());
		}

		@Bean
		public StateMachineModelFactory<String, String> modelFactory() {
			return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
		}
	}

	private static class ChoiceGuard implements Guard<String, String> {

		private final String match;

		public ChoiceGuard(String match) {
			this.match = match;
		}

		@Override
		public boolean evaluate(StateContext<String, String> context) {
			return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("choice", String.class));
		}
	}

	private static class JunctionGuard implements Guard<String, String> {

		private final String match;

		public JunctionGuard(String match) {
			this.match = match;
		}

		@Override
		public boolean evaluate(StateContext<String, String> context) {
			return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("junction", String.class));
		}
	}

	/**
	 * Builds the context.
	 *
	 * @return the annotation config application context
	 */
	protected AnnotationConfigApplicationContext buildContext() {
		return null;
	}
}
