/*
 * Copyright 2013 Robert von Burg <eitch@eitchnet.ch>
 *
 * 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 li.strolch.runtime.configuration;

import static org.junit.Assert.*;

import java.util.*;

import li.strolch.agent.api.ComponentState;
import li.strolch.agent.api.StrolchComponent;
import li.strolch.agent.impl.ComponentContainerImpl;
import li.strolch.agent.impl.ComponentContainerStateHandler;
import li.strolch.agent.impl.ComponentController;
import li.strolch.agent.impl.ComponentDependencyAnalyzer;
import li.strolch.utils.dbc.DBC.DbcException;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;

@SuppressWarnings("nls")
public class ControllerDependencyTest {

	//
	//  
	//         5
	//         v
	//   2 <-- 11 ---> 10
	//        ^  v     ^
	//        |  9     3
	//        |   ^   /
	//        |    | v
	//       7 --> 8
	//
	//
	//        A
	//       ^ ^
	//      B   C
	//     ^
	//     D 
	//
	//
	//        A1
	//      ^   ^
	//     B1 > C1
	//     ^
	//    D1
	//
	//
	//    +-> A2
	//    |   ^   
	//    |  B2 > C2
	//    |  ^
	//     D2
	//
	//    
	//
	//  a -> b : Upstream dependency for a is b 
	//         : Downstream dependency for b is a
	//

	private ComponentController con2;
	private ComponentController con5;
	private ComponentController con11;
	private ComponentController con10;
	private ComponentController con9;
	private ComponentController con7;
	private ComponentController con8;
	private ComponentController con3;
	private ComponentController conA;
	private ComponentController conB;
	private ComponentController conC;
	private ComponentController conD;
	private ComponentController conA1;
	private ComponentController conB1;
	private ComponentController conC1;
	private ComponentController conD1;
	private ComponentController conA2;
	private ComponentController conB2;
	private ComponentController conC2;
	private ComponentController conD2;

	private StrolchConfiguration strolchConfiguration;
	private Map<String, ComponentController> controllerMap;

	@Before
	public void setupModel() {

		ComponentContainerImpl container = new ComponentContainerImpl(null);
		this.con2 = new ComponentController(new StrolchComponent(container, "2"));
		this.con5 = new ComponentController(new StrolchComponent(container, "5"));
		this.con11 = new ComponentController(new StrolchComponent(container, "11"));
		this.con10 = new ComponentController(new StrolchComponent(container, "10"));
		this.con9 = new ComponentController(new StrolchComponent(container, "9"));
		this.con7 = new ComponentController(new StrolchComponent(container, "7"));
		this.con8 = new ComponentController(new StrolchComponent(container, "8"));
		this.con3 = new ComponentController(new StrolchComponent(container, "3"));

		this.con5.addUpstreamDependency(this.con11);

		this.con11.addUpstreamDependency(this.con2);
		this.con11.addUpstreamDependency(this.con10);
		this.con11.addUpstreamDependency(this.con9);

		this.con7.addUpstreamDependency(this.con11);
		this.con7.addUpstreamDependency(this.con8);

		this.con8.addUpstreamDependency(this.con9);

		this.con3.addUpstreamDependency(this.con8);
		this.con3.addUpstreamDependency(this.con10);

		//

		this.conA = new ComponentController(new StrolchComponent(container, "A"));
		this.conB = new ComponentController(new StrolchComponent(container, "B"));
		this.conC = new ComponentController(new StrolchComponent(container, "C"));
		this.conD = new ComponentController(new StrolchComponent(container, "D"));

		this.conB.addUpstreamDependency(this.conA);
		this.conC.addUpstreamDependency(this.conA);
		this.conD.addUpstreamDependency(this.conB);

		//

		this.conA1 = new ComponentController(new StrolchComponent(container, "A1"));
		this.conB1 = new ComponentController(new StrolchComponent(container, "B1"));
		this.conC1 = new ComponentController(new StrolchComponent(container, "C1"));
		this.conD1 = new ComponentController(new StrolchComponent(container, "D1"));

		this.conB1.addUpstreamDependency(this.conA1);
		this.conB1.addUpstreamDependency(this.conC1);
		this.conC1.addUpstreamDependency(this.conA1);
		this.conD1.addUpstreamDependency(this.conB1);

		//

		this.conA2 = new ComponentController(new StrolchComponent(container, "A2"));
		this.conB2 = new ComponentController(new StrolchComponent(container, "B2"));
		this.conC2 = new ComponentController(new StrolchComponent(container, "C2"));
		this.conD2 = new ComponentController(new StrolchComponent(container, "D2"));

		this.conB2.addUpstreamDependency(this.conA2);
		this.conB2.addUpstreamDependency(this.conC2);
		this.conD2.addUpstreamDependency(this.conB2);
		this.conD2.addUpstreamDependency(this.conA2);

		//

		this.controllerMap = new HashMap<>();

		this.controllerMap.put("2", this.con2);
		this.controllerMap.put("3", this.con3);
		this.controllerMap.put("5", this.con5);
		this.controllerMap.put("7", this.con7);
		this.controllerMap.put("8", this.con8);
		this.controllerMap.put("9", this.con9);
		this.controllerMap.put("10", this.con10);
		this.controllerMap.put("11", this.con11);
		this.controllerMap.put("A", this.conA);
		this.controllerMap.put("B", this.conB);
		this.controllerMap.put("C", this.conC);
		this.controllerMap.put("D", this.conD);
		this.controllerMap.put("A1", this.conA1);
		this.controllerMap.put("B1", this.conB1);
		this.controllerMap.put("C1", this.conC1);
		this.controllerMap.put("D1", this.conD1);
		this.controllerMap.put("A2", this.conA2);
		this.controllerMap.put("B2", this.conB2);
		this.controllerMap.put("C2", this.conC2);
		this.controllerMap.put("D2", this.conD2);

		//

		this.strolchConfiguration = ConfigurationParserTest.parseConfiguration(ControllerDependencyTest.class, "dev");
		for (ComponentController controller : this.controllerMap.values()) {
			ComponentConfiguration componentConfiguration = new ComponentConfiguration(
					this.strolchConfiguration.getRuntimeConfiguration(), controller.getName(), null, null, null, null);
			this.strolchConfiguration.addConfiguration(controller.getName(), componentConfiguration);
		}
	}

	private void assertModel() {

		assertTrue(this.con5.hasUpstreamDependencies());
		assertFalse(this.con5.hasDownstreamDependencies());
		assertEquals(1, this.con5.getUpstreamDependencies().size());
		assertEquals(0, this.con5.getDownstreamDependencies().size());

		assertTrue(this.con11.hasUpstreamDependencies());
		assertTrue(this.con11.hasDownstreamDependencies());
		assertEquals(3, this.con11.getUpstreamDependencies().size());
		assertEquals(2, this.con11.getDownstreamDependencies().size());

		assertFalse(this.con2.hasUpstreamDependencies());
		assertTrue(this.con2.hasDownstreamDependencies());
		assertEquals(0, this.con2.getUpstreamDependencies().size());
		assertEquals(1, this.con2.getDownstreamDependencies().size());

		assertTrue(this.con7.hasUpstreamDependencies());
		assertFalse(this.con7.hasDownstreamDependencies());
		assertEquals(2, this.con7.getUpstreamDependencies().size());
		assertEquals(0, this.con7.getDownstreamDependencies().size());

		assertTrue(this.con8.hasUpstreamDependencies());
		assertTrue(this.con8.hasDownstreamDependencies());
		assertEquals(1, this.con8.getUpstreamDependencies().size());
		assertEquals(2, this.con8.getDownstreamDependencies().size());

		assertTrue(this.con3.hasUpstreamDependencies());
		assertFalse(this.con3.hasDownstreamDependencies());
		assertEquals(2, this.con3.getUpstreamDependencies().size());
		assertEquals(0, this.con3.getDownstreamDependencies().size());

		assertFalse(this.con10.hasUpstreamDependencies());
		assertTrue(this.con10.hasDownstreamDependencies());
		assertEquals(0, this.con10.getUpstreamDependencies().size());
		assertEquals(2, this.con10.getDownstreamDependencies().size());

		assertFalse(this.con9.hasUpstreamDependencies());
		assertTrue(this.con9.hasDownstreamDependencies());
		assertEquals(0, this.con9.getUpstreamDependencies().size());
		assertEquals(2, this.con9.getDownstreamDependencies().size());

		// transitive upstream
		assertTrue(this.con7.hasTransitiveUpstreamDependency(this.con11));
		assertTrue(this.con5.hasTransitiveUpstreamDependency(this.con11));
		assertTrue(this.con11.hasTransitiveUpstreamDependency(this.con2));
		assertTrue(this.con11.hasTransitiveUpstreamDependency(this.con10));
		assertFalse(this.con11.hasTransitiveUpstreamDependency(this.con3));
		assertTrue(this.con8.hasTransitiveUpstreamDependency(this.con9));

		assertTrue(this.con5.hasTransitiveUpstreamDependency(this.con10));
		assertTrue(this.con5.hasTransitiveUpstreamDependency(this.con2));
		assertTrue(this.con5.hasTransitiveUpstreamDependency(this.con9));
		assertTrue(this.con7.hasTransitiveUpstreamDependency(this.con2));
		assertTrue(this.con7.hasTransitiveUpstreamDependency(this.con10));
		assertTrue(this.con7.hasTransitiveUpstreamDependency(this.con9));
		assertTrue(this.con3.hasTransitiveUpstreamDependency(this.con9));
		assertFalse(this.con9.hasTransitiveUpstreamDependency(this.con10));
		assertFalse(this.con9.hasTransitiveUpstreamDependency(this.con11));
		assertFalse(this.con9.hasTransitiveUpstreamDependency(this.con8));
		assertFalse(this.con9.hasTransitiveUpstreamDependency(this.con7));

		// transitive downstream
		assertFalse(this.con5.hasTransitiveDownstreamDependency(this.con8));
		assertFalse(this.con5.hasTransitiveDownstreamDependency(this.con3));
		assertFalse(this.con5.hasTransitiveDownstreamDependency(this.con7));
		assertFalse(this.con9.hasTransitiveDownstreamDependency(this.con10));
		assertTrue(this.con9.hasTransitiveDownstreamDependency(this.con3));
		assertTrue(this.con9.hasTransitiveDownstreamDependency(this.con7));

		//

		assertFalse(this.conA.hasUpstreamDependencies());
		assertTrue(this.conA.hasDownstreamDependencies());
		assertEquals(0, this.conA.getUpstreamDependencies().size());
		assertEquals(2, this.conA.getDownstreamDependencies().size());

		assertTrue(this.conB.hasUpstreamDependencies());
		assertTrue(this.conB.hasDownstreamDependencies());
		assertEquals(1, this.conB.getUpstreamDependencies().size());
		assertEquals(1, this.conB.getDownstreamDependencies().size());

		assertTrue(this.conC.hasUpstreamDependencies());
		assertFalse(this.conC.hasDownstreamDependencies());
		assertEquals(1, this.conC.getUpstreamDependencies().size());
		assertEquals(0, this.conC.getDownstreamDependencies().size());

		assertTrue(this.conD.hasUpstreamDependencies());
		assertFalse(this.conD.hasDownstreamDependencies());
		assertEquals(1, this.conD.getUpstreamDependencies().size());
		assertEquals(0, this.conD.getDownstreamDependencies().size());

		//

		assertFalse(this.conA1.hasUpstreamDependencies());
		assertTrue(this.conA1.hasDownstreamDependencies());
		assertEquals(0, this.conA1.getUpstreamDependencies().size());
		assertEquals(2, this.conA1.getDownstreamDependencies().size());

		assertTrue(this.conB1.hasUpstreamDependencies());
		assertTrue(this.conB1.hasDownstreamDependencies());
		assertEquals(2, this.conB1.getUpstreamDependencies().size());
		assertEquals(1, this.conB1.getDownstreamDependencies().size());

		assertTrue(this.conC1.hasUpstreamDependencies());
		assertTrue(this.conC1.hasDownstreamDependencies());
		assertEquals(1, this.conC1.getUpstreamDependencies().size());
		assertEquals(1, this.conC1.getDownstreamDependencies().size());

		assertTrue(this.conD1.hasUpstreamDependencies());
		assertFalse(this.conD1.hasDownstreamDependencies());
		assertEquals(1, this.conD1.getUpstreamDependencies().size());
		assertEquals(0, this.conD1.getDownstreamDependencies().size());
	}

	@Test
	public void testModel() {
		assertModel();
	}

	@Test
	public void shouldBreakModel1() {
		assertThrows(StrolchConfigurationException.class, () -> {
			assertModel();
			this.con2.addUpstreamDependency(this.con7);
		});
	}

	@Test
	public void shouldBreakModel2() {
		assertThrows(StrolchConfigurationException.class, () -> {
			assertModel();
			this.con3.addUpstreamDependency(this.con3);
		});
	}

	@Test
	public void shouldBreakModel3() {
		assertThrows(StrolchConfigurationException.class, () -> {
			assertModel();
			this.con9.addUpstreamDependency(this.con3);
		});
	}

	@Test
	public void shouldNotBreakModel1() {
		assertModel();
		this.con11.addUpstreamDependency(this.con8);
	}

	@Test
	public void shouldNotBreakModel2() {
		assertModel();
		this.con9.addUpstreamDependency(this.con10);
	}

	@Test
	public void shouldNotBreakModel3() {
		assertModel();
		this.con11.addUpstreamDependency(this.con8);
	}

	@Test
	public void shouldNotBreakModel4() {
		assertModel();
		this.con8.addUpstreamDependency(this.con11);
	}

	@Test
	public void shouldNotBreakModel() {
		assertModel();
		this.con11.addUpstreamDependency(this.con3);
	}

	@Test
	public void shouldCollectUpstreamDependencies1() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conB);
		controllers.add(this.conC);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conA));
	}

	@Test
	public void shouldCollectUpstreamDependencies2() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conD);
		controllers.add(this.conC);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conB));
	}

	@Test
	public void shouldCollectUpstreamDependencies3_() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conB);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conA));
	}

	//
	//        A
	//       ^ ^
	//      B   C
	//     ^
	//     D 
	//

	@Test
	public void shouldCollectUpstreamDependencies3() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conB1);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conC1));
	}

	@Test
	public void shouldNotCollectUpstreamDependencies4() {
		DbcException exception = assertThrows(DbcException.class, () -> {
			assertModel();

			ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
					this.controllerMap);

			Set<ComponentController> controllers = new HashSet<>();
			controllers.add(this.conD1);
			controllers.add(this.conC1);

			dependencyAnalyzer.collectDirectUpstreamDependencies(controllers);
			fail("Shouldn't reach here!");
		});

		MatcherAssert.assertThat(exception.getMessage(),
				Matchers.containsString("Upstream C1 is one of the input controllers!"));
	}

	@Test
	public void shouldCollectUpstreamDependencies5() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conB1);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conC1));
	}

	//
	//        A1
	//      ^   ^
	//     B1 > C1
	//     ^
	//    D1
	//

	@Test
	public void shouldCollectUpstreamDependencies6() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conB2);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(2, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conA2));
		assertTrue(directUpstreamDependencies.contains(this.conC2));
	}

	@Test
	public void shouldCollectUpstreamDependencies7() {
		assertModel();

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);

		Set<ComponentController> controllers = new HashSet<>();
		controllers.add(this.conD2);

		Set<ComponentController> directUpstreamDependencies = dependencyAnalyzer
				.collectDirectUpstreamDependencies(controllers);

		assertEquals(1, directUpstreamDependencies.size());
		assertTrue(directUpstreamDependencies.contains(this.conB2));
	}

	//    +-> A2
	//    |   ^   
	//    |  B2 > C2
	//    |  ^
	//     D2
	//

	@Test
	public void shouldAddDependencies() {

		ComponentContainerImpl container = new ComponentContainerImpl(null);
		StrolchComponent component = new StrolchComponent(container, "1");
		ComponentController controller = new ComponentController(component);

		StrolchComponent upstreamDependencyComp1 = new StrolchComponent(container, "1");
		ComponentController upstreamDependency1 = new ComponentController(upstreamDependencyComp1);

		StrolchComponent upstreamDependencyComp2 = new StrolchComponent(container, "1");
		ComponentController upstreamDependency2 = new ComponentController(upstreamDependencyComp2);

		StrolchComponent transitiveUpstreamDependencyComp2 = new StrolchComponent(container, "1");
		ComponentController transitiveUpstreamDependency2 = new ComponentController(transitiveUpstreamDependencyComp2);

		controller.addUpstreamDependency(upstreamDependency1);
		controller.addUpstreamDependency(upstreamDependency2);
		upstreamDependency1.addUpstreamDependency(transitiveUpstreamDependency2);

		assertTrue(controller.hasUpstreamDependencies());
		assertFalse(controller.hasDownstreamDependencies());
		assertTrue(controller.hasUpstreamDependency(upstreamDependency1));
		assertTrue(controller.hasUpstreamDependency(upstreamDependency2));

		assertTrue(upstreamDependency1.hasDownstreamDependency(controller));
		assertTrue(upstreamDependency2.hasDownstreamDependency(controller));

		assertTrue(upstreamDependency1.hasUpstreamDependencies());
		assertTrue(upstreamDependency1.hasUpstreamDependency(transitiveUpstreamDependency2));
		assertTrue(transitiveUpstreamDependency2.hasDownstreamDependencies());

		assertTrue(controller.hasTransitiveUpstreamDependency(transitiveUpstreamDependency2));
		assertTrue(transitiveUpstreamDependency2.hasTransitiveDownstreamDependency(controller));
	}

	@Test
	public void shouldRunThroughModelStates() {

		ComponentDependencyAnalyzer dependencyAnalyzer = new ComponentDependencyAnalyzer(this.strolchConfiguration,
				this.controllerMap);
		ComponentContainerStateHandler stateHandler = new ComponentContainerStateHandler(dependencyAnalyzer,
				strolchConfiguration);

		for (ComponentController controller : this.controllerMap.values()) {
			assertEquals(ComponentState.UNDEFINED, controller.getState());

			ComponentConfiguration componentConfiguration = this.strolchConfiguration
					.getComponentConfiguration(controller.getName());
			controller.getComponent().setup(componentConfiguration);
		}

		Set<ComponentController> rootUpstreamComponents = dependencyAnalyzer.findRootUpstreamComponents();

		assertState(ComponentState.SETUP, this.controllerMap.values());

		stateHandler.initialize(rootUpstreamComponents);
		assertState(ComponentState.INITIALIZED, this.controllerMap.values());

		stateHandler.start(rootUpstreamComponents);
		assertState(ComponentState.STARTED, this.controllerMap.values());

		Set<ComponentController> rootDownstreamComponents = dependencyAnalyzer.findRootDownstreamComponents();
		stateHandler.stop(rootDownstreamComponents);
		assertState(ComponentState.STOPPED, this.controllerMap.values());

		stateHandler.destroy(rootDownstreamComponents);
		assertState(ComponentState.DESTROYED, this.controllerMap.values());
	}

	private void assertState(ComponentState expectedState, Collection<ComponentController> collection) {
		for (ComponentController controller : collection) {
			assertEquals(expectedState, controller.getState());
		}
	}
}
