/*
 * 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.jena.graph;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.mockito.Mockito;
import org.xenei.junit.contract.Contract;
import org.xenei.junit.contract.ContractTest;

import static org.junit.Assert.*;

import org.apache.jena.testing_framework.GraphHelper;
import org.xenei.junit.contract.IProducer;
import org.apache.jena.testing_framework.NodeCreateUtils;

import static org.apache.jena.testing_framework.GraphHelper.*;

/**
 * An abstract test that tests EventManager implementations to ensure they
 * comply with the EventManager contract.
 * 
 * Subclasses of this class must implement the getEventManagerProducer() method
 * to create a new instance of the EventManager for testing.
 */

@Contract(GraphEventManager.class)
public class GraphEventManagerContractTest<T extends GraphEventManager> {

	protected static final Triple[] tripleArray = tripleArray("S P O; Foo R B; X Q Y");

	private IProducer<T> producer;
	
	// the graph that is used as the source of the events.
	private Graph mockGraph;
	// The event manager we are working with.
	private GraphEventManager gem;

	public GraphEventManagerContractTest() {
	}

	@Contract.Inject
	public final void setGraphEventManagerContractTestProducer(
			IProducer<T> producer) {
		this.producer= producer;
	}

	@After
	public final void afterGraphEventManagerContractTest() {
		producer.cleanUp();
	}

	@Before
	public final void beforeGraphEventManagerContractTest() {
		mockGraph = Mockito.mock(Graph.class);
		gem = producer.newInstance();
		L = new RecordingGraphListener();
	}

	/**
	 * Test that when a listener is registered the same EventManager is
	 * returned.
	 */
	@ContractTest
	public void testEventRegister() {
		assertSame(gem, gem.register(new RecordingGraphListener()));
	}

	/**
	 * Test that we can safely unregister a listener that isn't registered.
	 */
	@ContractTest
	public void testEventUnregister() {
		assertSame(gem, gem.unregister(L));
	}

	/**
	 * Handy triple for test purposes.
	 */
	protected Triple SPO = NodeCreateUtils.createTriple("S P O");

	/**
	 * The RecordingListener that reports all the events sent from the
	 * EventManager.
	 */
	protected RecordingGraphListener L;

	/**
	 * Test that adding a triple is reported.
	 */
	@ContractTest
	public void testAddTriple() {
		gem.register(L);
		gem.notifyAddTriple(mockGraph, SPO);
		L.assertHas(new Object[] { "add", mockGraph, SPO });
	}

	/**
	 * Test that deleting a triple is reported.
	 */
	@ContractTest
	public void testDeleteTriple() {
		gem.register(L);
		gem.notifyDeleteTriple(mockGraph, SPO);
		L.assertHas(new Object[] { "delete", mockGraph, SPO });
	}

	/**
	 * Test that when 2 listeners are added both receive events.
	 */
	@ContractTest
	public void testTwoListeners() {
		RecordingGraphListener L1 = new RecordingGraphListener();
		RecordingGraphListener L2 = new RecordingGraphListener();
		gem.register(L1).register(L2);
		gem.notifyAddTriple(mockGraph, SPO);
		L2.assertHas(new Object[] { "add", mockGraph, SPO });
		L1.assertHas(new Object[] { "add", mockGraph, SPO });
	}

	/**
	 * Test that unregistering a listener after registering it results in it not
	 * receiving messages.
	 */
	@ContractTest
	public void testUnregisterWorks() {
		gem.register(L).unregister(L);
		gem.notifyAddTriple(mockGraph, SPO);
		L.assertHas(new Object[] {});
	}

	/**
	 * Test that registering a listener twice results in the listener receiving
	 * the events twice.
	 */
	@ContractTest
	public void testRegisterTwice() {
		gem.register(L).register(L);
		gem.notifyAddTriple(mockGraph, SPO);
		L.assertHas(new Object[] { "add", mockGraph, SPO, "add", mockGraph, SPO });
	}

	/**
	 * Test that registering a listener twice and unregistering it once will
	 * result in the listener receiving each event one time.
	 */
	@ContractTest
	public void testUnregisterOnce() {
		gem.register(L).register(L).unregister(L);
		gem.notifyDeleteTriple(mockGraph, SPO);
		L.assertHas(new Object[] { "delete", mockGraph, SPO });
	}

	/**
	 * Test that adding an array is reported as adding an array.
	 */
	@ContractTest
	public void testAddArray() {
		gem.register(L);
		gem.notifyAddArray(mockGraph, tripleArray);
		L.assertHas(new Object[] { "add[]", mockGraph, tripleArray });
	}

	/**
	 * Test that adding a list is reported as adding a list
	 */
	@ContractTest
	public void testAddList() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyAddList(mockGraph, elems);
		L.assertHas(new Object[] { "addList", mockGraph, elems });
	}

	/**
	 * Test that deleting an array is reported as deleting an array.
	 */
	@ContractTest
	public void testDeleteArray() {
		gem.register(L);
		gem.notifyDeleteArray(mockGraph, tripleArray);
		L.assertHas(new Object[] { "delete[]", mockGraph, tripleArray });
	}

	/**
	 * Test that deleting a list is reported as deleting a list.
	 */
	@ContractTest
	public void testDeleteList() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyDeleteList(mockGraph, elems);
		L.assertHas(new Object[] { "deleteList", mockGraph, elems });
	}

	/**
	 * Test that adding a list as an iterator is reported as an add iterator.
	 */
	@ContractTest
	public void testAddListAsIterator() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyAddIterator(mockGraph, elems);
		L.assertHas(new Object[] { "addIterator", mockGraph, elems });
	}

	/**
	 * Test that adding an iterator is reported as adding an iterator.
	 */
	@ContractTest
	public void testAddIterator() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyAddIterator(mockGraph, elems.iterator());
		L.assertHas(new Object[] { "addIterator", mockGraph, elems });
	}

	/**
	 * Test that deleting an iterator is reported as a deleting an iterator.
	 */
	@ContractTest
	public void testDeleteIterator() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyDeleteIterator(mockGraph, elems.iterator());
		L.assertHas(new Object[] { "deleteIterator", mockGraph, elems });
	}

	/**
	 * Test that deleting a list as an iterator is reported as deleting an
	 * iterator.
	 */
	@ContractTest
	public void testDeleteListAsIterator() {
		gem.register(L);
		List<Triple> elems = Arrays.asList(tripleArray);
		gem.notifyDeleteIterator(mockGraph, elems);
		L.assertHas(new Object[] { "deleteIterator", mockGraph, elems });
	}

	/**
	 * Test that adding a graph is reported as adding a graph.
	 */
	@ContractTest
	public void testAddGraph() {
		gem.register(L);
		Graph other = Mockito.mock(Graph.class);
		gem.notifyAddGraph(mockGraph, other);
		L.assertHas(new Object[] { "addGraph", mockGraph, other });
	}

	/**
	 * Test that deleting a graph is reported as deleting a graph.
	 */
	@ContractTest
	public void testDeleteGraph() {
		gem.register(L);
		Graph other = Mockito.mock(Graph.class);
		gem.notifyDeleteGraph(mockGraph, other);
		L.assertHas(new Object[] { "deleteGraph", mockGraph, other });
	}

	/**
	 * Test that sending a general event is reported as an event and the value
	 * is saved.
	 */
	@ContractTest
	public void testGeneralEvent() {
		gem.register(L);
		Object value = new int[] {};
		gem.notifyEvent(mockGraph, value);
		L.assertHas(new Object[] { "someEvent", mockGraph, value });
	}

	@ContractTest
	public void testListening() {
		assertFalse("Should not be listening", gem.listening());
		gem.register(L);
		assertTrue("Should be listening", gem.listening());
		gem.unregister(L);
		assertFalse("Should not be listening", gem.listening());
	}

	//
	// Foo series of tests to check modifying the manger in mid notification
	//

	private ComeAndGoListener all[];

	abstract private static class ComeAndGoListener implements GraphListener {

		// Was I registered when start() was called, and have not been
		// unregistered.
		boolean inPlay = false;
		// currently registered or not.
		boolean registered = false;
		boolean notified = false;

		void register(GraphEventManager gem) {
			registered = true;
			gem.register(this);
		}

		void unregister(GraphEventManager gem) {
			registered = false;
			inPlay = false;
			gem.unregister(this);
		}

		void start() {
			if (registered)
				inPlay = true;
		}

		void check() {
			if (inPlay && !notified)
				fail("listener that was in-play was not notified of adding triple.");
		}

		@Override
		final public void notifyAddTriple(Graph g, Triple t) {
			notified = true;
			doSomeDamage();
		}

		abstract void doSomeDamage();

		@Override
		public void notifyAddArray(Graph g, Triple[] triples) {
		}

		@Override
		public void notifyAddGraph(Graph g, Graph added) {
		}

		@Override
		public void notifyAddIterator(Graph g, Iterator<Triple> it) {
		}

		@Override
		public void notifyAddList(Graph g, List<Triple> triples) {
		}

		@Override
		public void notifyDeleteArray(Graph g, Triple[] triples) {
		}

		@Override
		public void notifyDeleteGraph(Graph g, Graph removed) {
		}

		@Override
		public void notifyDeleteIterator(Graph g, Iterator<Triple> it) {
		}

		@Override
		public void notifyDeleteList(Graph g, List<Triple> L) {
		}

		@Override
		public void notifyDeleteTriple(Graph g, Triple t) {
		}

		@Override
		public void notifyEvent(Graph source, Object value) {
		}

	}

	/**
	 * ComeAndGoListener implementation that does no damage
	 * 
	 */
	private static final class SimpleListener extends ComeAndGoListener {
		@Override
		void doSomeDamage() {
		}
	}

	/**
	 * Test adding a triple to trigger event.
	 * 
	 * @param registerTo
	 *            Number of listeners to register.
	 * @param allx
	 */
	private void testAddingTriple(int registerTo, ComeAndGoListener... allx) {
		all = allx;
		// register addMe
		for (int i = 0; i < registerTo; i++) {
			all[i].register(gem);
		}

		// start them all
		for (ComeAndGoListener l : all) {
			l.start();
		}
		// send the notification.
		gem.notifyAddTriple(mockGraph, GraphHelper.triple("make a change"));
		// check them
		for (ComeAndGoListener l : all) {
			l.check();
		}
	}

	/**
	 * Test that a listener added during event processing does not receive the
	 * event.
	 */
	@ContractTest
	public void testAddOne() {
		testAddingTriple(2, new ComeAndGoListener() {
			@Override
			void doSomeDamage() {
				all[2].register(gem);
			}
		}, new SimpleListener(), new SimpleListener());
	}

	/**
	 * Test that when a listener that has not yet been handled is removed during
	 * event processing it receive the event.
	 */
	@ContractTest
	public void testDelete2nd() {
		testAddingTriple(3, new ComeAndGoListener() {
			@Override
			void doSomeDamage() {
				all[1].unregister(gem);
			}
		}, new SimpleListener(), new SimpleListener());
	}

	/**
	 * Test that when a listener that has been handled is removed during event
	 * processing it receives the event.
	 */
	@ContractTest
	public void testDelete1st() {
		testAddingTriple(3, new SimpleListener(), new ComeAndGoListener() {
			@Override
			void doSomeDamage() {
				all[0].unregister(gem);
			}
		}, new SimpleListener());
	}

	/**
	 * Test that when a listener that removes itself during event processing it
	 * receives the event.
	 */
	@ContractTest
	public void testDeleteSelf() {
		testAddingTriple(3, new ComeAndGoListener() {
			@Override
			void doSomeDamage() {
				unregister(gem);
			}
		}, new SimpleListener(), new SimpleListener());
	}

	/**
	 * Test that when a listener that removes and adds itself during event
	 * processing it receives the event.
	 */
	@ContractTest
	public void testDeleteAndAddSelf() {
		testAddingTriple(3, new ComeAndGoListener() {
			@Override
			void doSomeDamage() {
				unregister(gem);
				register(gem);
			}
		}, new SimpleListener(), new SimpleListener());
	}

}
