package eu.dnetlib.xml.database.exist;

import java.io.IOException;
import java.util.*;

import eu.dnetlib.xml.database.XMLDatabase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.xmldb.api.base.XMLDBException;

import static org.junit.Assert.*;

/**
 * Test the exist wrapper.
 *
 * @author marko
 *
 */
public class ExistDatabaseTest { // NOPMD by marko on 11/24/08 5:01 PM
	/**
	 * number of values in stress test profile.
	 */
	private static final int STRESS_VALUES = 100;

	/**
	 * number of times the values are repeated in the stress test.
	 */
	private static final int STRESS_PROF_LEN = 100;

	/**
	 * number of profile writes in stress test.
	 */
	private static final int STRESS_TIMES = 2000000;

	/**
	 * number of collections for stress test.
	 */
	private static final int STRESS_COLLS = 40;

	/**
	 * number of profiles in stress test.
	 */
	private static final int STRESS_PROFILES = 1000;

	/**
	 * parallel tuning.
	 */
	private static final int VALUE_SCATTER = 7;

	/**
	 * parallel tuning.
	 */
	private static final int QUERY_SCATTER = 5;

	/**
	 * test trigger name.
	 */
	private static final String TEST_TRIGGER = "testTrigger";

	/**
	 * number of collections for parallel test.
	 */
	private static final int PAR_COLLS = 20;

	/**
	 * iterations per thread.
	 */
	private static final int PAR_ITERATIONS = 100;

	/**
	 * parallel job executions.
	 */
	private static final int PAR_TIMES = 100;

	/**
	 * logger.
	 */
	public static final Log log = LogFactory.getLog(ExistDatabaseTest.class); // NOPMD by marko on 11/24/08 5:01 PM

	/**
	 * test xml string.
	 */
	private static final String HELLO_XML = "<hello/>";

	/**
	 * test xml content.
	 */
	private static final String HELLO_XML_CONTENT = "<hello>content</hello>";

	/**
	 * test file name.
	 */
	private static final String EXAMPLE = "example";

	/**
	 * trigger test file name.
	 */
	private static final String TRIGGER_TEST_FILE = "shouldTrigger";

	/**
	 * root collection prefix.
	 */
	private static final String DB_ROOT = "/db";

	/**
	 * test collection name.
	 */
	private static final String DB_TEST = "/db/testCollection";

	/**
	 * test other collection name.
	 */
	private static final String DB_OTHER = "/db/otherCollection";

	/**
	 * test sub collection.
	 */
	private static final String DB_SUB = DB_TEST + "/sub";

	/**
	 * xml database under test.
	 */
	private transient XMLDatabase database;

	/**
	 * eXist database under test, viewed only in setUp and tearDown.
	 */
	private transient ExistDatabase edb;

	/**
	 * prepares the database.
	 *
	 * @throws Exception
	 *             exist error
	 */
	@Before
	public void setUp() throws Exception {
		edb = new TemporaryExistDatabase();
		edb.start();
		database = edb;
	}

	/**
	 * shuts down the database.
	 *
	 * @throws IOException
	 *             happens
	 */
	@After
	public void tearDown() throws IOException {
		if (edb != null)
			edb.stop();
	}

	/**
	 * test create.
	 *
	 * @throws XMLDBException
	 *             happens
	 */
	@Test
	public void create() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, HELLO_XML);
		assertEquals("database resource created", true, true);
	}

	/**
	 * correct behavior is to throw exception on empty file name, because otherwise the file is invisible but the data
	 * is inserted in the xml db.
	 *
	 * @throws XMLDBException
	 *             expected
	 */
	@Test(expected = XMLDBException.class)
	public void createEmptyName() throws XMLDBException {
		database.create("", DB_ROOT, "<shouldnt_exist/>");

		// code to repeat the bug, shouldn't be executed because of the expected exception
		final Iterator<String> res = database.xquery("collection('')/shouldnt_exist");

		assertTrue("shouldn't exist, but it exists",  res.hasNext());
		log.info(database.list(DB_ROOT));
		assertEquals("file should be listed, with empty file name", 1, database.list(DB_ROOT).size());
		assertEquals("file should be listed, with empty file name", "", database.list(DB_ROOT).get(0));

		assertEquals("the file can be retrieved", "<shouldnt_exist/>", database.read("", DB_ROOT));
	}

	/**
	 * test read.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void read() throws XMLDBException {
		create();
		final String res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("simple reading", HELLO_XML, res);
	}

	/**
	 * test read of non existing file.
	 *
	 * @throws XMLDBException
	 *             shouldn't throw exception
	 */
	@Test
	public void readError() throws XMLDBException {
		final String res = database.read("nonExisting", DB_ROOT);
		assertEquals("expecting null on unexisting", null, res);
	}

	/**
	 * test remove.
	 *
	 * @throws XMLDBException
	 *             shouldn't throw exception
	 */
	@Test
	public void remove() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, HELLO_XML);
		String res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("reading", HELLO_XML, res);

		database.remove(EXAMPLE, DB_ROOT);
		res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("now it doesn't exist", null, res);
	}

	/**
	 * delete unexisting file.
	 *
	 * @throws XMLDBException
	 *             could happen
	 */
	@Test
	public void doubleDelete() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, HELLO_XML);
		String res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("reading", HELLO_XML, res);

		database.remove(EXAMPLE, DB_ROOT);
		res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("now it doesn't exist", null, res);

		assertFalse("already exists", database.remove(EXAMPLE, DB_ROOT));
	}

	/**
	 * update an xml file.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void update() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, HELLO_XML);
		String res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("reading", HELLO_XML, res);

		database.update(EXAMPLE, DB_ROOT, "<world/>");
		res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("now it doesn't exist", "<world/>", res);
	}

	/**
	 * update an unexisting file.
	 *
	 * @throws XMLDBException
	 *             expected
	 */
	@Test(expected = XMLDBException.class)
	public void updateError() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, HELLO_XML);
		database.remove(EXAMPLE, DB_ROOT);
		final String res = database.read(EXAMPLE, DB_ROOT);
		assertEquals("check non existing", null, res);

		database.update(EXAMPLE, DB_ROOT, "<world/>"); // throws
	}

	/**
	 * test xquery.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test()
	public void query() throws XMLDBException {
		database.create(EXAMPLE, DB_ROOT, "<queryTest><one><sub/></one><two/></queryTest>");
		final Iterator<String> res = database.xquery("collection('/db')//queryTest/one");
		assertTrue("finds only one result", res.hasNext());

		final Iterator<String> res2 = database.xquery("collection('/db')//queryTest/two");
		assertTrue("finds only one result",res2.hasNext());
		assertEquals("check the correct result", "<two/>", res2.next());
	}

	/**
	 * create a collection.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test()
	public void createCollection() throws XMLDBException {
		database.createCollection(DB_TEST);
		database.create(EXAMPLE, DB_TEST, "<col/>");

		final String res = database.read(EXAMPLE, DB_TEST);
		assertEquals("check another collection", "<col/>", res);
	}

	/**
	 * check the existence of a collection.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test()
	public void checkCollection() throws XMLDBException {
		createCollection();

		assertTrue("check root", database.collectionExists(DB_ROOT));
		assertTrue("check test collection", database.collectionExists(DB_TEST));
		assertFalse("check non existing", database.collectionExists("/db/testNonExistingCollection"));
	}

	/**
	 * shows that a spurious collection create is legal.
	 *
	 * @throws XMLDBException
	 *             shouldn't throw
	 */
	@Test()
	public void createCollectionDuplicate() throws XMLDBException {
		database.createCollection(DB_TEST);
		database.createCollection(DB_TEST);
	}

	/**
	 * remove a collection.
	 *
	 * @throws XMLDBException
	 *             shouldn't throw
	 */
	@Test()
	public void removeCollection() throws XMLDBException {
		database.createCollection(DB_TEST);
		assertTrue("check before remove", database.collectionExists(DB_TEST));
		database.removeCollection(DB_TEST);
		assertFalse("check after remove", database.collectionExists(DB_TEST));
	}

	/**
	 * check useless contract with spring.
	 */
	@Test
	public void testIsRunning() {
		assertFalse("contract with spring", edb.isRunning());
	}

	/**
	 * Test a scenario where exist fails to create a database.
	 *
	 * @throws IOException
	 *             happens
	 * @throws XMLDBException
	 *             happens
	 */
	@Test(expected = IllegalStateException.class)
	public void checkWrongConfigFile() throws IOException, XMLDBException {
		edb.stop();
		edb = new ExistDatabase();
		try {
			edb.setConfigFile("/tmp/unexistingfile");
			edb.start();
			database = edb;

			create();
		} finally {
			edb = null; // NOPMD - just for junit
		}
	}

	/**
	 * get child collections.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void testListChildCollections() throws XMLDBException {
		database.createCollection(DB_TEST);
		assertTrue("check that collection exists", database.collectionExists(DB_TEST));

		final String[] expectedNames = new String[] { "child1", "child2", "child3" };
		for (String name : expectedNames)
			database.createCollection(DB_TEST + "/" + name);

		final List<String> res = database.listChildCollections(DB_TEST);
		// the db doesn't return then in the same order
		Collections.sort(res);
		assertArrayEquals("check list", expectedNames, res.toArray());
	}

	/**
	 * check list resources.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void testList() throws XMLDBException {
		database.createCollection(DB_TEST);
		assertTrue("collection should exist", database.collectionExists(DB_TEST));

		final String[] expectedNames = new String[] { "name1", "name2", "name3" };
		for (String name : expectedNames)
			database.create(name, DB_TEST, HELLO_XML);

		final List<String> res = database.list(DB_TEST);
		// the db doesn't return then in the same order
		Collections.sort(res);
		assertArrayEquals("check list", expectedNames, res.toArray());
	}

	/**
	 * test low-level set trigger.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void testSetExistTrigger() throws XMLDBException {
		final Map<String, String> params = new HashMap<String, String>();
		params.put("listenerBean", "pippo");

		database.createCollection(DB_TEST);
		edb.setExistTrigger(TestExistTrigger.class, DB_TEST, Arrays.asList(new String[] { "store", "update", "delete" }), params);

		// assertNotNull("check that the conf is stored", database.read(ExistDatabase.COLLECTION_XCONF, DB_ROOT + "/system/config" + DB_TEST));

		database.create(TRIGGER_TEST_FILE, DB_TEST, HELLO_XML_CONTENT);
		assertEquals("check the the write happened", HELLO_XML_CONTENT, database.read(TRIGGER_TEST_FILE, DB_TEST));

		database.update(TRIGGER_TEST_FILE, DB_TEST, "<hello>new content</hello>");

		database.xquery("for $x in collection('')/hello where $x/text() = 'new content' return update value $x/text() with 'xupdate'");
		assertEquals("check the the xupdate happened", "<hello>xupdate</hello>", database.read(TRIGGER_TEST_FILE, DB_TEST));
	}

	/**
	 * Test high level addTrigger.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void testRegisterTrigger() throws XMLDBException {
		final TestTrigger trigger = new TestTrigger();
		trigger.setName(TEST_TRIGGER);

		database.createCollection(DB_TEST);
		database.registerTrigger(trigger, DB_TEST);

		assertFalse("check that the tester state is correct", trigger.isCreated());

		database.create(TRIGGER_TEST_FILE, DB_TEST, HELLO_XML_CONTENT);
		assertEquals("check the the write happened", HELLO_XML_CONTENT, database.read(TRIGGER_TEST_FILE, DB_TEST));
		assertFalse("check that the trigger is not invoked for the update event", trigger.isUpdated());
		assertFalse("check that the trigger is not invoked for the deletion event", trigger.isDeleted());
		assertTrue("check that the trigger is invoked create", trigger.isCreated());
		assertEquals("check file name for create", TRIGGER_TEST_FILE, trigger.getLastFile());
		assertEquals("check collection name for create", DB_TEST, trigger.getLastCollection());

		trigger.reset();
		assertFalse("check that the tester state is correct", trigger.isUpdated());
		database.update(TRIGGER_TEST_FILE, DB_TEST, "<hello>new content</hello>");
		assertFalse("check that the trigger is not invoked for the creation event", trigger.isCreated());
		assertFalse("check that the trigger is not invoked for the deletion event", trigger.isDeleted());
		assertTrue("check that the trigger is invoked for update", trigger.isUpdated());
		assertEquals("check file name for update", TRIGGER_TEST_FILE, trigger.getLastFile());
		assertEquals("check collection name for update", DB_TEST, trigger.getLastCollection());

		trigger.reset();
		database.xquery("for $x in collection('')/hello where $x/text() = 'new content' return update value $x/text() with 'xupdate'");
		assertTrue("check that the trigger is invoked for xupdate", trigger.isUpdated());
		assertEquals("check file name for xupdate", TRIGGER_TEST_FILE, trigger.getLastFile());
		assertEquals("check collection name for xupdate", DB_TEST, trigger.getLastCollection());

		trigger.reset();
		assertFalse("check that the tester state is correct", trigger.isDeleted());
		database.remove(TRIGGER_TEST_FILE, DB_TEST);
		assertFalse("check that the trigger is not invoked for the creation event", trigger.isCreated());
		assertFalse("check that the trigger is not invoked for the update event", trigger.isUpdated());
		assertNull("check that the file is removed", database.read(TRIGGER_TEST_FILE, DB_TEST));
		assertTrue("check that the trigger is invoked for delete", trigger.isDeleted());
		assertEquals("check file name for delete", TRIGGER_TEST_FILE, trigger.getLastFile());
		assertEquals("check collection name for delete", DB_TEST, trigger.getLastCollection());
	}

	/**
	 * a trigger configuration file should not be listed as a xml file resource.
	 *
	 * @throws XMLDBException
	 *             shouldn't happen
	 */
	@Test
	public void testListWithTriggerConf() throws XMLDBException {
		database.createCollection(DB_TEST);
		assertTrue("collection should exist", database.collectionExists(DB_TEST));

		final TestTrigger trigger = new TestTrigger();
		trigger.setName(TEST_TRIGGER);

		database.registerTrigger(trigger, DB_TEST);

		final String[] expectedNames = new String[] { "name1", "name2", "name3" };
		for (String name : expectedNames)
			database.create(name, DB_TEST, HELLO_XML);

		assertTrue("check that trigger was invoked", trigger.isCreated());

		final List<String> res = database.list(DB_TEST);
		// the db doesn't return then in the same order
		Collections.sort(res);
		assertArrayEquals("check list", expectedNames, res.toArray());
	}

	/**
	 * test trigger with several collections.
	 *
	 * @throws XMLDBException
	 *             shoudn't happen
	 */
	@Test
	public void testTriggerDifferentCollections() throws XMLDBException {
		final TestTrigger trigger = new TestTrigger();
		trigger.setName(TEST_TRIGGER);

		database.createCollection(DB_TEST);
		database.createCollection(DB_OTHER);
		database.createCollection(DB_SUB);

		database.registerTrigger(trigger, DB_TEST);
		database.create(TRIGGER_TEST_FILE, DB_TEST, HELLO_XML);
		assertTrue("trigger was registered for this collection", trigger.isCreated());

		trigger.reset();
		database.create(TRIGGER_TEST_FILE, DB_OTHER, HELLO_XML);
		assertFalse("trigger was not registered for this collection", trigger.isCreated());

		trigger.reset();
		database.registerTrigger(trigger, DB_OTHER);
		database.remove(TRIGGER_TEST_FILE, DB_OTHER);
		assertTrue("trigger is now registered for this collection", trigger.isDeleted());

		trigger.reset();
		database.create(TRIGGER_TEST_FILE, DB_SUB, HELLO_XML);
		assertTrue("trigger is automatically registered for the sub collection", trigger.isCreated());

	}

	/**
	 * fails because eXist wants that all collection paths begin with /db.
	 *
	 * @throws XMLDBException
	 *             expected
	 */
	@Test(expected = XMLDBException.class)
	public void testGetCollection() throws XMLDBException {
		edb.getCollection("/something");
		assertNotNull("dummy", edb);
	}

	/**
	 * simple parallel job.
	 *
	 * @author marko
	 *
	 */
	class SimpleParallelJob extends Thread { // NOPMD
		/**
		 * some argument.
		 */
		private final transient int name;
		/**
		 * record eventual exceptions.
		 */
		private transient Throwable throwable = null;

		/**
		 * pass some argument to the job.
		 *
		 * @param name
		 *            some argument
		 */
		SimpleParallelJob(final int name) {
			super();
			this.name = name;
		}

		/**
		 * {@inheritDoc}
		 *
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			try {
				parallelJob(name);
			} catch (XMLDBException e) {
				log.fatal("parallel job failing", e);
				throwable = e;
			} catch (Throwable e) { // NOPMD
				log.fatal("other exception", e);
				throwable = e;
			}
		}

		public Throwable getThrowable() {
			return throwable;
		}
	}

	/**
	 * stress test the eXist db.
	 *
	 * @throws Throwable
	 *             could
	 */
	@Test
	public void testParallel() throws Throwable {
		final TestTrigger trigger = new TestTrigger();
		trigger.setName(TEST_TRIGGER);

		database.registerTrigger(trigger, DB_ROOT);

		final List<SimpleParallelJob> threads = new ArrayList<SimpleParallelJob>(); // NOPMD
		for (int i = 0; i < PAR_TIMES; i++)
			threads.add(new SimpleParallelJob(i)); // NOPMD
		for (Thread thread : threads) { // NOPMD
			thread.start();
		}
		for (Thread thread : threads) { // NOPMD
			thread.join();
		}

		for (SimpleParallelJob thread : threads) { // NOPMD
			if (thread.getThrowable() != null)
				throw thread.getThrowable();
		}

		assertNotNull("dummy", threads);
	}

	/**
	 * Stress test for eXist WAL logs.
	 *
	 * @throws XMLDBException
	 *             could happen
	 */
	@Test
	@Ignore
	public void testManueleStress() throws XMLDBException {
		final StringBuffer childTemplate = new StringBuffer(200); // NOPMD

		for (int i = 0; i < STRESS_VALUES; i++) {
			childTemplate.append("<key>" + i + "-VAL</key><value>ITER</value>");
		}

		final StringBuffer hugeTemplate = new StringBuffer(200);
		hugeTemplate.append("<root>");
		for (int i = 0; i < STRESS_PROF_LEN; i++) {
			final String child = childTemplate.toString().replace("VAL", Integer.toString(i));
			hugeTemplate.append(child);
		}
		hugeTemplate.append("</root>");

		for (int i = 0; i < STRESS_TIMES; i++) {
			final String huge = hugeTemplate.toString().replace("ITER", Integer.toString(i));
			database.create("stressTest-" + (i % STRESS_PROFILES), DB_ROOT + "/" + (i % STRESS_COLLS), huge);
		}

		assertNotNull("dummy", hugeTemplate);
	}

	/**
	 * one parallel job execution.
	 *
	 * @param arg
	 *            some argument
	 * @throws XMLDBException
	 *             could happen.
	 */
	protected void parallelJob(final int arg) throws XMLDBException {
		final String name = Integer.toString(arg);
		final String coll = DB_ROOT + "/" + Integer.toString(arg % PAR_COLLS);
		database.create(name, coll, "<a" + name + "/>");

		for (int i = 0; i < PAR_ITERATIONS; i++) {
			database.update(name, coll, "<a" + name + " value=\"" + i % VALUE_SCATTER + "\"/>");
			final Iterator<String> res = database.xquery("collection('')//*[@value='" + i % QUERY_SCATTER + "']");
			while (res.hasNext())
				res.next();
		}
	}

}
