package info.ebenoit.ebul.cmp;


import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;



/** Tests for {@link ComponentState} */
public class TestComponentState
{

	/**
	 * A "fake" {@link ComponentRegistry} whose initialisation, activation and failure state can be modified manually
	 */
	private static class FakeRegistry
			extends ComponentRegistry
	{
		private boolean initialised;
		private boolean active;
		private boolean failed;


		@Override
		public boolean isInitialised( )
		{
			return this.initialised;
		}


		@Override
		public boolean isActive( )
		{
			return this.active;
		}


		@Override
		public boolean hasFailed( )
		{
			return this.failed;
		}
	}

	/** Class used to test parametric component handling */
	private static class PCmpTest
			implements ParametricComponent
	{
		private final String componentName;


		public PCmpTest( final String componentName )
		{
			super( );
			this.componentName = componentName;
		}


		@Override
		public String getComponentName( )
		{
			return this.componentName;
		}

	}

	/** Class used to test methods that affect the component's lifecycle */
	private static class LCATest
	{
		private LifecycleStage stage;
	}

	/** Class used to test {@link ComponentState#restart()} */
	private static class RestartTest
	{
		private boolean stopped;
		private boolean started;


		private static ComponentState get( final ComponentRegistry reg , final boolean active )
		{
			return TestComponentState.makeState( RestartTest.getDef( ) , reg , true , active );
		}


		private static NewComponentInfo< RestartTest > getDef( )
		{
			return new NewComponentInfo< >( new RestartTest( ) ) //
					.setLifecycleAction( LifecycleStage.START , o -> {
						o.started = true;
					} ) //
					.setLifecycleAction( LifecycleStage.STOP , o -> {
						o.stopped = true;
					} );
		}
	}


	private static ComponentState makeState( final NewComponentInfo< ? > ci , final ComponentRegistry reg ,
			final boolean initialised , final boolean active )
	{
		final ComponentState cs = new ComponentState( reg , ci );
		cs.setInitialised( initialised );
		cs.setActive( active );
		return cs;
	}

	private FakeRegistry reg;


	@Before
	public void setup( )
	{
		this.reg = new FakeRegistry( );
	}


	/** Test: initialising a {@link ComponentState} using an existing object */
	@Test
	public void testInitialiseWithObject( )
	{
		final Object object = new Object( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertSame( object , cs.getComponent( ) );
		Assert.assertEquals( "Object" , cs.getName( ) );
	}


	/** Test: initialising a {@link ComponentState} using a component supplier */
	@Test
	public void testInitialiseWithSupplier( )
	{
		final Object object = new Object( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( ( ) -> object );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertSame( object , cs.getComponent( ) );
		Assert.assertEquals( "Object" , cs.getName( ) );
	}


	/**
	 * Test: initialising a {@link ComponentState} using a component supplier that throws an exception throws a
	 * {@link ComponentCreationException}
	 */
	@Test( expected = ComponentCreationException.class )
	public void testInitialiseWithFailingSupplier( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( ( ) -> {
			throw new Exception( "failing" );
		} );
		new ComponentState( this.reg , ci );
	}


	/** Test: initialising a {@link ComponentState} with a supplied name */
	@Test
	public void testInitialiseWithName( )
	{
		final Object object = new Object( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ).setName( "Test" );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertEquals( "Test" , cs.getName( ) );
	}


	/** Test: initialising a {@link ComponentState} with {@link ParametricComponent} */
	@Test
	public void testInitialiseWithParametricComponent( )
	{
		final Object object = new PCmpTest( "ParametricName" );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertEquals( "ParametricName" , cs.getName( ) );
	}


	/** Test: initialising a {@link ComponentState} with {@link ParametricComponent} overrides any configured name */
	@Test
	public void testInitialiseWithParametricComponentAndName( )
	{
		final Object object = new PCmpTest( "ParametricName" );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ).setName( "Test" );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertEquals( "ParametricName" , cs.getName( ) );
	}


	/** Test: {@link ComponentState} copies autostart flag */
	@Test
	public void testInitialiseAutostart( )
	{
		final Object object = new Object( );

		NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
		ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertFalse( cs.hasAutostart( ) );

		ci = new NewComponentInfo< Object >( object ).setAutostart( true );
		cs = new ComponentState( this.reg , ci );
		Assert.assertTrue( cs.hasAutostart( ) );
	}


	/** Test: {@link ComponentState} copies lifecycle actions and can execute them */
	@Test
	public void testInitialiseLCA( )
			throws Throwable
	{
		final LCATest lca = new LCATest( );
		final NewComponentInfo< LCATest > ci = new NewComponentInfo< LCATest >( lca );
		for ( final LifecycleStage stage : LifecycleStage.values( ) ) {
			ci.setLifecycleAction( stage , ( o ) -> {
				o.stage = stage;
			} );
		}
		final ComponentState cs = new ComponentState( this.reg , ci );

		for ( final LifecycleStage stage : LifecycleStage.values( ) ) {
			cs.runLifecycleAction( stage );
			Assert.assertSame( lca.stage , stage );
		}
	}


	/** Test: {@link ComponentState#toString()} on anonymous components */
	@Test
	public void testToStringAnon( )
	{
		final Object object = new PCmpTest( null );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertEquals( "anonymous component of type " + PCmpTest.class.getCanonicalName( ) , cs.toString( ) );
	}


	/** Test: {@link ComponentState#toString()} on named components */
	@Test
	public void testToStringNamed( )
	{
		final Object object = new Object( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ).setName( "Test" );
		final ComponentState cs = new ComponentState( this.reg , ci );
		Assert.assertEquals( "component 'Test' of type " + Object.class.getCanonicalName( ) , cs.toString( ) );
	}


	/** Test: {@link ComponentState#addDependency(ComponentState)} adds the specified dependency */
	@Test
	public void testAddDependency( )
	{
		final Object object = new Object( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
		final ComponentState cs1 = new ComponentState( this.reg , ci );
		final ComponentState cs2 = new ComponentState( this.reg , ci );

		cs1.addDependency( cs2 );

		Assert.assertEquals( 1 , cs1.getDependencies( ).size( ) );
		Assert.assertTrue( cs1.getDependencies( ).contains( cs2 ) );
		Assert.assertEquals( 1 , cs2.getReverseDependencies( ).size( ) );
		Assert.assertTrue( cs2.getReverseDependencies( ).contains( cs1 ) );
	}


	/** Test: {@link ComponentState#start()} on uninitialised components throws {@link IllegalStateException} */
	@Test( expected = IllegalStateException.class )
	public void testStartUninitialised( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = new ComponentState( this.reg , ci );
		this.reg.active = true;
		cs.start( );
	}


	/** Test: {@link ComponentState#start()} with a failed registry throws {@link IllegalStateException} */
	@Test( expected = IllegalStateException.class )
	public void testStartFailedRegistry( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = new ComponentState( this.reg , ci );
		cs.setInitialised( true );
		this.reg.failed = true;
		cs.start( );
	}


	/** Test: {@link ComponentState#start()} with an inactive registry throws {@link IllegalStateException} */
	@Test( expected = IllegalStateException.class )
	public void testStartInactiveRegistry( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = new ComponentState( this.reg , ci );
		cs.setInitialised( true );
		cs.start( );
	}


	/** Test: {@link ComponentState#start()} with no lifecycle action runs without a hitch */
	@Test
	public void testStartNoAction( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = new ComponentState( this.reg , ci );
		cs.setInitialised( true );
		this.reg.active = true;
		cs.start( );
		Assert.assertTrue( cs.isActive( ) );
	}


	/** Test: {@link ComponentState#start()} runs the startup action */
	@Test
	public void testStartStartupAction( )
	{
		final LCATest lcaTest = new LCATest( );
		final NewComponentInfo< LCATest > ci = new NewComponentInfo< LCATest >( lcaTest ) //
				.setLifecycleAction( LifecycleStage.START , ( o ) -> {
					o.stage = LifecycleStage.START;
				} );
		final ComponentState cs = new ComponentState( this.reg , ci );
		cs.setInitialised( true );
		this.reg.active = true;
		cs.start( );
		Assert.assertSame( LifecycleStage.START , lcaTest.stage );
		Assert.assertTrue( cs.isActive( ) );
	}


	/** Test: {@link ComponentState#start()} on an active component does nothing */
	@Test
	public void testStartStartupAlreadyActive( )
	{
		final LCATest lcaTest = new LCATest( );
		final NewComponentInfo< LCATest > ci = new NewComponentInfo< LCATest >( lcaTest ) //
				.setLifecycleAction( LifecycleStage.START , ( o ) -> {
					o.stage = LifecycleStage.START;
				} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		cs.start( );
		Assert.assertNull( lcaTest.stage );
	}


	/** Test: {@link ComponentState#start()} re-throws {@link ComponentStartupException} */
	@Test
	public void testStartStartupActionExceptionPassthrough( )
	{
		final ComponentStartupException failure = new ComponentStartupException( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) ) //
				.setLifecycleAction( LifecycleStage.START , ( o ) -> {
					throw failure;
				} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , false );
		this.reg.active = true;
		try {
			cs.start( );
		} catch ( final ComponentStartupException e ) {
			Assert.assertSame( failure , e );
			return;
		}
		Assert.fail( "no ComponentStartupException" );
	}


	/** Test: {@link ComponentState#start()} transmits other exceptions using a {@link ComponentStartupException} */
	@Test
	public void testStartStartupActionExceptionTransmit( )
	{
		final Exception failure = new Exception( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) ) //
				.setLifecycleAction( LifecycleStage.START , ( o ) -> {
					throw failure;
				} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , false );
		this.reg.active = true;
		try {
			cs.start( );
		} catch ( final ComponentStartupException e ) {
			Assert.assertSame( failure , e.getCause( ) );
			return;
		}
		Assert.fail( "no ComponentStartupException" );
	}


	/** Test: {@link ComponentState#start()} attempts to start a component's dependencies */
	@Test
	public void testStartDependencies( )
	{
		final LCATest lcaTest = new LCATest( );
		final NewComponentInfo< LCATest > ci1 = new NewComponentInfo< LCATest >( lcaTest ) //
				.setLifecycleAction( LifecycleStage.START , ( o ) -> {
					o.stage = LifecycleStage.START;
				} );
		final ComponentState cs1 = TestComponentState.makeState( ci1 , this.reg , true , false );

		final NewComponentInfo< Object > ci2 = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs2 = TestComponentState.makeState( ci2 , this.reg , true , false );
		cs2.addDependency( cs1 );

		this.reg.active = true;
		cs2.start( );
		Assert.assertSame( LifecycleStage.START , lcaTest.stage );
	}


	/** Test: {@link ComponentState#stop()} with a failed registry throws {@link IllegalStateException} */
	@Test( expected = IllegalStateException.class )
	public void testStopFailedRegistry( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.failed = true;
		cs.stop( );
	}


	/** Test: {@link ComponentState#stop()} with no configured action works */
	@Test
	public void testStopNoAction( )
	{
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		Assert.assertNull( cs.stop( ) );
		Assert.assertFalse( cs.isActive( ) );
	}


	/** Test: {@link ComponentState#stop()} executes the configured stop action */
	@Test
	public void testStopAction( )
	{
		final LCATest lcaTest = new LCATest( );
		final NewComponentInfo< LCATest > ci = new NewComponentInfo< LCATest >( lcaTest ) //
				.setLifecycleAction( LifecycleStage.STOP , ( o ) -> {
					o.stage = LifecycleStage.STOP;
				} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		Assert.assertNull( cs.stop( ) );
		Assert.assertFalse( cs.isActive( ) );
		Assert.assertSame( LifecycleStage.STOP , lcaTest.stage );
	}


	/** Test: {@link ComponentState#stop()} returns an exception thrown by the configured stop action */
	@Test
	public void testStopActionFailure( )
	{
		final Exception failure = new Exception( );
		final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( new Object( ) ) //
				.setLifecycleAction( LifecycleStage.STOP , ( o ) -> {
					throw failure;
				} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		Assert.assertSame( failure , cs.stop( ) );
		Assert.assertTrue( cs.isActive( ) );
	}


	/** Test: {@link ComponentState#stop()} stops reverse dependencies */
	@Test
	public void testStopActionStopReverseDeps( )
	{
		final NewComponentInfo< Object > ci1 = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs1 = TestComponentState.makeState( ci1 , this.reg , true , true );

		final NewComponentInfo< Object > ci2 = new NewComponentInfo< Object >( new Object( ) );
		final ComponentState cs2 = TestComponentState.makeState( ci2 , this.reg , true , true );
		cs2.addDependency( cs1 );

		this.reg.active = true;
		Assert.assertNull( cs1.stop( ) );
		Assert.assertFalse( cs2.isActive( ) );
	}


	/** Test: {@link ComponentState#restart()} with a failed registry throws {@link IllegalStateException} */
	@Test( expected = IllegalStateException.class )
	public void testRestartFailedRegistry( )
	{
		final NewComponentInfo< RestartTest > ci = new NewComponentInfo< >( new RestartTest( ) );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.failed = true;
		cs.restart( );
	}


	/** Test: {@link ComponentState#restart()} on a stopped component does nothing */
	@Test
	public void testRestartInactive( )
	{
		final ComponentState cs = RestartTest.get( this.reg , false );
		final RestartTest rt = (RestartTest) cs.getComponent( );
		this.reg.active = true;
		cs.restart( );
		Assert.assertFalse( rt.stopped );
		Assert.assertFalse( rt.started );
	}


	/** Test: {@link ComponentState#restart()} on an active component restarts it */
	@Test
	public void testRestartActive( )
	{
		final ComponentState cs = RestartTest.get( this.reg , true );
		final RestartTest rt = (RestartTest) cs.getComponent( );
		this.reg.active = true;
		cs.restart( );
		Assert.assertTrue( rt.stopped );
		Assert.assertTrue( rt.started );
	}


	/** Test: {@link ComponentState#restart()} throws a {@link ComponentRestartException} when stop fails */
	@Test
	public void testRestartWhenStopFails( )
	{
		final Exception failure = new Exception( );
		final NewComponentInfo< RestartTest > ci = RestartTest.getDef( );
		ci.setLifecycleAction( LifecycleStage.STOP , ( o ) -> {
			throw failure;
		} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		try {
			cs.restart( );
		} catch ( final ComponentRestartException e ) {
			Assert.assertSame( failure , e.getCause( ) );
			Assert.assertTrue( cs.isActive( ) );
			return;
		}
		Assert.fail( "no ComponentRestartException" );
	}


	/** Test: {@link ComponentState#restart()} throws a {@link ComponentRestartException} when start fails */
	@Test
	public void testRestartWhenStartFails( )
	{
		final ComponentStartupException failure = new ComponentStartupException( );
		final NewComponentInfo< RestartTest > ci = RestartTest.getDef( );
		ci.setLifecycleAction( LifecycleStage.START , ( o ) -> {
			throw failure;
		} );
		final ComponentState cs = TestComponentState.makeState( ci , this.reg , true , true );
		this.reg.active = true;
		try {
			cs.restart( );
		} catch ( final ComponentRestartException e ) {
			Assert.assertSame( failure , e.getCause( ) );
			Assert.assertFalse( cs.isActive( ) );
			return;
		}
		Assert.fail( "no ComponentRestartException" );
	}


	/** Test: {@link ComponentState#restart()} restarts active reverse dependencies */
	@Test
	public void testRestartActiveReverseDeps( )
	{
		final ComponentState cs1 = TestComponentState.makeState( RestartTest.getDef( ) , this.reg , true , true );
		final ComponentState cs2 = TestComponentState.makeState( RestartTest.getDef( ) , this.reg , true , true );
		cs2.addDependency( cs1 );

		this.reg.active = true;
		cs1.restart( );

		final RestartTest rt = (RestartTest) cs2.getComponent( );
		Assert.assertTrue( rt.stopped );
		Assert.assertTrue( rt.started );
	}


	/** Test: {@link ComponentState#restart()} does not start inactive reverse dependencies */
	@Test
	public void testRestartInactiveReverseDeps( )
	{
		final ComponentState cs1 = TestComponentState.makeState( RestartTest.getDef( ) , this.reg , true , true );
		final ComponentState cs2 = TestComponentState.makeState( RestartTest.getDef( ) , this.reg , true , false );
		cs2.addDependency( cs1 );

		this.reg.active = true;
		cs1.restart( );

		final RestartTest rt = (RestartTest) cs2.getComponent( );
		Assert.assertFalse( rt.stopped );
		Assert.assertFalse( rt.started );
	}

}