/*
 * 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.buildtests.tck;

import java.util.Map;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.test.StateMachineTestPlan;
import org.springframework.statemachine.test.StateMachineTestPlanBuilder;

/**
 * Base tck test class for defining various machine tests
 * which different config implementations can implement to
 * test that same machine behaviour.
 *
 * @author Janne Valkealahti
 *
 */
public abstract class AbstractTckTests {

	protected AnnotationConfigApplicationContext context;

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

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

	@SuppressWarnings("unchecked")
	protected StateMachine<String, String> getStateMachineFromContext() {
		return context.getBean(StateMachine.class);
	}

	@SuppressWarnings("unchecked")
	protected StateMachineFactory<String, String> getStateMachineFactoryFromContext() {
		return context.getBean(StateMachineFactory.class);
	}

	protected void cleanInternal() {
	}

	protected AnnotationConfigApplicationContext buildContext() {
		return null;
	}

	@Test
	public void testSimpleMachine() throws Exception {
		StateMachine<String,String> stateMachine = getSimpleMachine();
		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();
	}

	/**
	 * Return state machine for {@link #testSimpleMachine()}.
	 *
	 * @return StateMachine for SimpleMachine
	 */
	protected abstract StateMachine<String, String> getSimpleMachine() throws Exception;

	@Test
	public void testSimpleSubMachine() throws Exception {
		StateMachine<String,String> stateMachine = getSimpleSubMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S1").and()
					.step().sendEvent("E1").expectStates("S2", "S21").and()
					.step().sendEvent("E2").expectStates("S2", "S22").and()
					.step().sendEvent("E3").expectStates("S3").and()
					.build();
		plan.test();
	}

	/**
	 * Return state machine for {@link #testSimpleSubMachine()}.
	 *
	 * @return StateMachine for SimpleSubMachine
	 */
	protected abstract StateMachine<String, String> getSimpleSubMachine() throws Exception;

	@Test
	public void testShowcaseMachineInitialState() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineA() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("A")
						.expectStateChanged(0)
						.expectStateEntered(0)
						.expectStateExited(0)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineB() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("B")
						.expectStateChanged(2)
						.expectStateEntered(2)
						.expectStateExited(2)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineCHKA() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("H")
						.expectVariable("foo", 1)
						.expectTransition(1)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("K")
						.expectStateEntered(2)
						.expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("A")
						.expectStateChanged(2)
						.expectStateEntered(2)
						.expectStateExited(2)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineC() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineCK() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("K")
						.expectStateEntered(2)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineD() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("D")
						.expectStateEntered(3)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineCD() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("D")
						.expectStateEntered(2)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineI() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("I")
						.expectStateEntered(1)
						.expectStates("S0", "S1", "S12").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineII() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("I")
						.expectStateEntered(1)
						.expectStateEntered(1)
						.expectStateExited(1)
						.expectStates("S0", "S1", "S12").and()
					.step()
						.sendEvent("I")
						.expectStateChanged(2)
						.expectStateEntered(3)
						.expectStateExited(2)
						.expectStates("S0", "S2", "S21", "S212").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineH() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step()
						.expectStates("S0", "S1", "S11")
						.expectVariable("foo", 0).and()
					.step()
						.sendEvent("H")
						.expectTransition(1)
						.expectVariable("foo", 0)
						.expectStates("S0", "S1", "S11").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineCH() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step()
						.expectStates("S0", "S1", "S11")
						.expectVariable("foo", 0).and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("H")
						.expectVariable("foo", 1)
						.expectTransition(1)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineACH() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("A")
						.expectStateChanged(0)
						.expectStateEntered(0)
						.expectStateExited(0)
						.expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("C")
						.expectStateEntered(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.step()
						.sendEvent("H")
						.expectVariable("foo", 1)
						.expectTransition(1)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineE() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("E")
						.expectStateChanged(3)
						.expectStateEntered(4)
						.expectStateExited(3)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineF() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("F")
						.expectStateChanged(2)
						.expectStateEntered(3)
						.expectStateExited(2)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	@Test
	public void testShowcaseMachineG() throws Exception {
		StateMachine<String,String> stateMachine = getShowcaseMachine();
		StateMachineTestPlan<String, String> plan =
				StateMachineTestPlanBuilder.<String, String>builder()
					.stateMachine(stateMachine)
					.step().expectStates("S0", "S1", "S11").and()
					.step()
						.sendEvent("G")
						.expectStateChanged(2)
						.expectStateEntered(3)
						.expectStateExited(2)
						.expectStates("S0", "S2", "S21", "S211").and()
					.build();
		plan.test();
	}

	/**
	 * Return state machine for showcase tests.
	 *
	 * @return StateMachine for ShowcaseMachine
	 */
	protected abstract StateMachine<String, String> getShowcaseMachine() throws Exception;

	@Configuration
	protected static class ShowcaseMachineBeansConfig {

		@Bean
		public FooGuard foo0Guard() {
			return new FooGuard(0);
		}

		@Bean
		public FooGuard foo1Guard() {
			return new FooGuard(1);
		}

		@Bean
		public FooAction fooAction() {
			return new FooAction();
		}
	}

	protected static class FooAction implements Action<String, String> {

		public FooAction() {
		}

		@Override
		public void execute(StateContext<String, String> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			Integer foo = context.getExtendedState().get("foo", Integer.class);
			if (foo == null) {
				variables.put("foo", 0);
			} else if (foo == 0) {
				variables.put("foo", 1);
			} else if (foo == 1) {
				variables.put("foo", 0);
			}
		}
	}

	protected static class FooGuard implements Guard<String, String> {

		private final int match;

		public FooGuard(int match) {
			this.match = match;
		}

		@Override
		public boolean evaluate(StateContext<String, String> context) {
			Object foo = context.getExtendedState().getVariables().get("foo");
			return !(foo == null || !foo.equals(match));
		}
	}
}
