/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.event.Broadcast;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.mock.MockApplication;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.cycle.RequestCycleContext;
import org.apache.wicket.util.tester.WicketTester;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


/**
 * Tests component events
 * 
 * @author igor
 */
public class ComponentEventsTest
{

	private WicketTester tester;
	private TestPage page;
	private TestContainer c1;
	private TestContainer c12;
	private TestContainer c13;
	private TestContainer c134;
	private TestComponent c135;
	private TestComponent c6;
	private TestApplication application;
	private TestSession session;
	private TestRequestCycle cycle;
	private Testable[] all;

	private Object stop;

	/** */
	@BeforeEach
	void setup()
	{
		tester = new WicketTester(new TestApplication());

		application = (TestApplication)tester.getApplication();

		session = (TestSession)tester.getSession();
		cycle = (TestRequestCycle)tester.getRequestCycle();

		page = new TestPage();
		c1 = new TestContainer("c1");
		c12 = new TestContainer("c12");
		c13 = new TestContainer("c13");
		c134 = new TestContainer("c134");
		c135 = new TestComponent("c135");
		c6 = new TestComponent("c6");

		page.add(c1);
		c1.add(c12);
		c1.add(c13);
		c13.add(c134);
		c13.add(c135);
		page.add(c6);

		all = new Testable[] { page, c1, c12, c13, c134, c135, c6, application, session, cycle };

		stop = null;
	}

	/** */
	@AfterEach
	void destroy()
	{
		tester.destroy();
	}

	/** */
	@Test
	void testBreadth_Application()
	{
		page.send(tester.getApplication(), Broadcast.BREADTH, new Payload());
		assertPath(application, session, cycle, page, c1, c12, c13, c134, c135, c6);
	}

	/** */
	@Test
	void testBreadth_Session()
	{
		page.send(tester.getSession(), Broadcast.BREADTH, new Payload());
		assertPath(session, cycle, page, c1, c12, c13, c134, c135, c6);
	}

	/** */
	@Test
	void testBreadth_Cycle()
	{
		page.send(tester.getRequestCycle(), Broadcast.BREADTH, new Payload());
		assertPath(cycle, page, c1, c12, c13, c134, c135, c6);
	}

	/** */
	@Test
	void testBreadth_Page()
	{
		page.send(page, Broadcast.BREADTH, new Payload());
		assertPath(page, c1, c12, c13, c134, c135, c6);
	}

	/** */
	@Test
	void testBreadth_Container()
	{
		page.send(c13, Broadcast.BREADTH, new Payload());
		assertPath(c13, c134, c135);
	}

	/** */
	@Test
	void testBreadth_Component()
	{
		page.send(c6, Broadcast.BREADTH, new Payload());
		assertPath(c6);
	}


	/** */
	@Test
	void testBreadth_Application_Stop()
	{
		stop = application;
		page.send(application, Broadcast.BREADTH, new Payload());
		assertPath(application);
	}

	/** */
	@Test
	void testBreadth_Session_Stop()
	{
		stop = session;
		page.send(application, Broadcast.BREADTH, new Payload());
		assertPath(application, session);
	}

	/** */
	@Test
	void testBreadth_Cycle_Stop()
	{
		stop = cycle;
		page.send(application, Broadcast.BREADTH, new Payload());
		assertPath(application, session, cycle);
	}

	/** */
	@Test
	void testBreadth_Page_Stop()
	{
		stop = page;
		page.send(application, Broadcast.BREADTH, new Payload());
		assertPath(application, session, cycle, page);
	}

	/** */
	@Test
	void testBreadth_Component_Stop()
	{
		stop = c13;
		page.send(application, Broadcast.BREADTH, new Payload());
		assertPath(application, session, cycle, page, c1, c12, c13);
	}


	/** */
	@Test
	void testDepth_Application()
	{
		page.send(application, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page, cycle, session, application);
	}

	/** */
	@Test
	void testDepth_Session()
	{
		page.send(session, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page, cycle, session);
	}

	/** */
	@Test
	void testDepth_Cycle()
	{
		page.send(cycle, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page, cycle);
	}

	/** */
	@Test
	void testDepth_Page()
	{
		page.send(page, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page);
	}

	/** */
	@Test
	void testDepth_Container()
	{
		page.send(c1, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1);
	}

	/** */
	@Test
	void testDepth_Component()
	{
		page.send(c6, Broadcast.DEPTH, new Payload());
		assertPath(c6);
	}


	/** */
	@Test
	void testDepth_Session_Stop()
	{
		stop = session;
		page.send(application, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page, cycle, session);
	}

	/** */
	@Test
	void testDepth_Cycle_Stop()
	{
		stop = cycle;
		page.send(application, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page, cycle);
	}

	/** */
	@Test
	void testDepth_Page_Stop()
	{
		stop = page;
		page.send(application, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1, c6, page);
	}

	/** */
	@Test
	void testDepth_Component_Stop()
	{
		stop = c1;
		page.send(application, Broadcast.DEPTH, new Payload());
		assertPath(c12, c134, c135, c13, c1);
	}


	/**
	 * 
	 */
	@Test
	void testBubble_Component()
	{
		c6.send(c135, Broadcast.BUBBLE, new Payload());
		assertPath(c135, c13, c1, page, cycle, session, application);
	}

	/** */
	@Test
	void testBubble_Page()
	{
		c6.send(page, Broadcast.BUBBLE, new Payload());
		assertPath(page, cycle, session, application);
	}

	/** */
	@Test
	void testBubble_Cycle()
	{
		c6.send(cycle, Broadcast.BUBBLE, new Payload());
		assertPath(cycle, session, application);
	}

	/** */
	@Test
	void testBubble_Session()
	{
		c6.send(session, Broadcast.BUBBLE, new Payload());
		assertPath(session, application);
	}


	/** */
	@Test
	void testBubble_Application()
	{
		c6.send(application, Broadcast.BUBBLE, new Payload());
		assertPath(application);
	}

	/** */
	@Test
	void testBubble_Component_Stop()
	{
		stop = c1;
		c6.send(c135, Broadcast.BUBBLE, new Payload());
		assertPath(c135, c13, c1);
	}

	/** */
	@Test
	void testBubble_Component_Page()
	{
		stop = page;
		c6.send(c135, Broadcast.BUBBLE, new Payload());
		assertPath(c135, c13, c1, page);
	}

	/** */
	@Test
	void testBubble_Cycle_Stop()
	{
		stop = cycle;
		c6.send(c135, Broadcast.BUBBLE, new Payload());
		assertPath(c135, c13, c1, page, cycle);
	}

	/** */
	@Test
	void testBubble_Session_Stop()
	{
		stop = session;
		c6.send(c135, Broadcast.BUBBLE, new Payload());
		assertPath(c135, c13, c1, page, cycle, session);
	}

	@Test
	void testBehaviorBreadth()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c, Broadcast.BREADTH, new Payload());
		assertEquals(0, c.sequence);
		assertEquals(1, b1.sequence);
		assertEquals(2, b2.sequence);
		assertEquals(-1, application.sequence);
		assertEquals(-1, session.sequence);
		assertEquals(-1, cycle.sequence);
		assertEquals(-1, page.sequence);
	}

	@Test
	void testBehaviorExact()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c, Broadcast.EXACT, new Payload());
		assertEquals(0, c.sequence);
		assertEquals(1, b1.sequence);
		assertEquals(2, b2.sequence);
		assertEquals(-1, application.sequence);
		assertEquals(-1, session.sequence);
		assertEquals(-1, cycle.sequence);
		assertEquals(-1, page.sequence);
	}

	@Test
	void testPageExact()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		c.add(b1);

		page.add(c);
		TestBehavior b2 = new TestBehavior();
		page.add(b2);

		c.send(page, Broadcast.EXACT, new Payload());
		assertEquals(-1, c.sequence);
		assertEquals(-1, b1.sequence);
		assertEquals(1, b2.sequence);
		assertEquals(-1, application.sequence);
		assertEquals(-1, session.sequence);
		assertEquals(-1, cycle.sequence);
		assertEquals(0, page.sequence);
	}

	@Test
	void testApplicationExact()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c.getApplication(), Broadcast.EXACT, new Payload());
		assertEquals(-1, c.sequence);
		assertEquals(-1, b1.sequence);
		assertEquals(-1, b2.sequence);
		assertEquals(0, application.sequence);
		assertEquals(-1, session.sequence);
		assertEquals(-1, cycle.sequence);
		assertEquals(-1, page.sequence);
	}

	@Test
	void testSessionExact()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c.getSession(), Broadcast.EXACT, new Payload());
		assertEquals(-1, c.sequence);
		assertEquals(-1, b1.sequence);
		assertEquals(-1, b2.sequence);
		assertEquals(-1, application.sequence);
		assertEquals(0, session.sequence);
		assertEquals(-1, cycle.sequence);
		assertEquals(-1, page.sequence);
	}

	@Test
	void testRequestCycleExact()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior();
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c.getRequestCycle(), Broadcast.EXACT, new Payload());
		assertEquals(-1, c.sequence);
		assertEquals(-1, b1.sequence);
		assertEquals(-1, b2.sequence);
		assertEquals(-1, application.sequence);
		assertEquals(-1, session.sequence);
		assertEquals(0, cycle.sequence);
		assertEquals(-1, page.sequence);
	}

	@Test
	void testBehavior_stop()
	{
		TestComponent c = new TestComponent("c");
		TestBehavior b1 = new TestBehavior()
		{
			@Override
			public void onEvent(Component component, IEvent<?> event)
			{
				super.onEvent(component, event);
				event.stop();
			}
		};
		TestBehavior b2 = new TestBehavior();
		c.add(b1, b2);

		c.send(c, Broadcast.BREADTH, new Payload());
		assertEquals(0, c.sequence);
		assertEquals(1, b1.sequence);
		assertEquals(-1, b2.sequence);
	}


	private void assertPath(Testable... testables)
	{
		List<Testable> remaining = new ArrayList<Testable>(Arrays.asList(all));

		for (int i = 0; i < testables.length; i++)
		{
			assertEquals(i, testables[i].getSequence(), "checking path element " + i);
			remaining.remove(testables[i]);
		}

		for (Testable testable : remaining)
		{
			String name = testable.getClass().getSimpleName();
			if (testable instanceof Component && !(testable instanceof Page))
			{
				name += "#" + ((Component)testable).getId();
			}
			assertEquals(-1, testable.getSequence(),
				name + " should not have been visited, but was.");
		}
	}

	private static interface Testable
	{
		int getSequence();
	}

	private static class Payload
	{
		private int counter;

		int next()
		{
			return counter++;
		}
	}

	private class TestApplication extends MockApplication implements Testable
	{
		int sequence = -1;

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			if (stop == this)
			{
				event.stop();
			}
		}

		@Override
		public Session newSession(Request request, Response response)
		{
			return new TestSession(request);
		}

		@Override
		protected void init()
		{
			super.init();
			setRequestCycleProvider(TestRequestCycle::new);
		}

		@Override
		public int getSequence()
		{
			return sequence;
		}
	}

	private class TestSession extends WebSession implements Testable
	{
		private static final long serialVersionUID = 1L;

		int sequence = -1;

		TestSession(Request request)
		{
			super(request);
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			if (stop == this)
			{
				event.stop();
			}
		}
	}

	private class TestRequestCycle extends RequestCycle implements Testable
	{
		int sequence = -1;

		TestRequestCycle(RequestCycleContext context)
		{
			super(context);
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			if (stop == this)
			{
				event.stop();
			}
		}
	}

	private class TestPage extends WebPage implements Testable
	{
		private static final long serialVersionUID = 1L;

		int sequence = -1;

		TestPage()
		{
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			// System.out.println(getId());
			if (stop == this)
			{
				event.stop();
			}
		}

	}

	private class TestContainer extends WebMarkupContainer implements Testable
	{
		private static final long serialVersionUID = 1L;

		int sequence = -1;

		TestContainer(String id)
		{
			super(id);
		}

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			// System.out.println(getId());
			if (stop == this)
			{
				event.stop();
			}
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}
	}

	private class TestComponent extends WebComponent implements Testable
	{
		private static final long serialVersionUID = 1L;

		int sequence = -1;

		TestComponent(String id)
		{
			super(id);
		}

		@Override
		public void onEvent(IEvent<?> event)
		{
			super.onEvent(event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			// System.out.println(getId());
			if (stop == this)
			{
				event.stop();
			}
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}
	}

	private class TestBehavior extends Behavior implements Testable
	{
		private static final long serialVersionUID = 1L;

		int sequence = -1;
		Component component;

		@Override
		public void onEvent(Component component, IEvent<?> event)
		{
			super.onEvent(component, event);
			Payload payload = (Payload)event.getPayload();
			sequence = payload.next();
			this.component = component;
			// System.out.println(getId());
			if (stop == this)
			{
				event.stop();
			}
		}


		@Override
		public int getSequence()
		{
			return sequence;
		}
	}

}
