package eu.dnetlib.enabling.is.registry;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.xml.sax.SAXException;

import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryException;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryService;
import eu.dnetlib.enabling.is.store.rmi.ISStoreException;
import eu.dnetlib.enabling.tools.OpaqueResource;
import eu.dnetlib.enabling.tools.ServiceLocator;
import eu.dnetlib.enabling.tools.StringOpaqueResource;
import eu.dnetlib.test.AbstractIntegrationContainerTest;

/**
 * IS registry integration test.
 *
 *
 * @author marko
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
public class ISRegistryServiceClientTest extends AbstractIntegrationContainerTest {

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(ISRegistryServiceClientTest.class);

	/**
	 * number of parallel tests.
	 */
	private static final int PAR_TIMES = 10;

	/**
	 * number of repetitions parallel job body.
	 */
	private static final int PARALLEL_REPEAT = 1;

	/**
	 * lookup locator.
	 */
	@Resource(name = "lookUpLocator")
	private transient ServiceLocator<ISLookUpService> lookupLocator;

	/**
	 * registry locator.
	 */
	@Resource(name = "registryLocator")
	private transient ServiceLocator<ISRegistryService> registryLocator;

	/**
	 * profile source from test-profile.xml .
	 */
	private transient String profileSource;

	/**
	 * service resolver.
	 *
	 * @throws IOException
	 *             shouldn't happen
	 */
	// @Resource
	// private transient ServiceResolver serviceResolver;
	@Before
	public void setUp() throws IOException {
		final StringWriter writer = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("test-profile.xml"), writer);
		profileSource = writer.getBuffer().toString();
	}

	/**
	 * test register profile.
	 *
	 * @throws IOException
	 *             should'nt happen
	 * @throws ISRegistryException
	 *             shouldn't happen
	 * @throws ISStoreException
	 *             shouldn't happen
	 * @throws ISLookUpException
	 *             shouldn't happen
	 * @throws ParserConfigurationException
	 *             shouldn't happen
	 * @throws SAXException
	 *             shouldn't happen
	 * @throws XPathExpressionException
	 *             shouldn't happen
	 */
	@Test
	public void testRegistry() throws IOException, ISRegistryException, ISLookUpException, ISStoreException, XPathExpressionException, SAXException,
			ParserConfigurationException {
		log.debug("testing registry");

		final String newId = registryLocator.getService().registerProfile(profileSource);

		final String resourceProfile = lookupLocator.getService().getResourceProfile(newId);
		final OpaqueResource resource = new StringOpaqueResource(resourceProfile);

		assertNotNull("check stored", resourceProfile);
		assertEquals("check id", newId, resource.getResourceId());
	}

	/**
	 * test that a profile inserted for validation is really stored in the pending collection.
	 *
	 * @throws ISRegistryException
	 *             shouldn't happen
	 * @throws XPathExpressionException
	 *             shouldn't happen
	 * @throws SAXException
	 *             shouldn't happen
	 * @throws IOException
	 *             shouldn't happen
	 * @throws ParserConfigurationException
	 *             shouldn't happen
	 * @throws ISLookUpException
	 *             shouldn't happen
	 * @throws ISStoreException
	 *             shouldn't happen
	 */
	@Test
	public void testInsertProfileForValidation() throws ISRegistryException, XPathExpressionException, SAXException, IOException,
			ParserConfigurationException, ISLookUpException, ISStoreException {
		final OpaqueResource resource = new StringOpaqueResource(profileSource);
		final String kind = resource.getResourceKind();
		final String type = resource.getResourceType();

		final String newId = registryLocator.getService().insertProfileForValidation(type, profileSource);

		final List<String> normalMatch = lookupLocator.getService().quickSearchProfile(buildQuery(newId, kind, type));
		assertNull("check normal", normalMatch);

		final List<String> pendingMatch = lookupLocator.getService().quickSearchProfile(buildQuery(newId, "PendingRepositoryResources", type));
		assertNotNull("check pending", pendingMatch);
		assertEquals("check pending size", 1, pendingMatch.size());
	}

	/**
	 * test that a profile inserted for validation is really stored in the pending collection.
	 *
	 * @throws ISRegistryException
	 *             shouldn't happen
	 * @throws XPathExpressionException
	 *             shouldn't happen
	 * @throws SAXException
	 *             shouldn't happen
	 * @throws IOException
	 *             shouldn't happen
	 * @throws ParserConfigurationException
	 *             shouldn't happen
	 * @throws ISLookUpException
	 *             shouldn't happen
	 * @throws ISStoreException
	 *             shouldn't happen
	 */
	@Test
	public void testInsertProfileForValidationDateProblem() throws ISRegistryException, XPathExpressionException, SAXException, IOException,
			ParserConfigurationException, ISLookUpException, ISStoreException {
		StringWriter dateProblemSource = new StringWriter();

		IOUtils.copy(getClass().getResourceAsStream("date-problem-profile.xml"), dateProblemSource);

		final String type = "RepositoryServiceResourceType";

		final String newId = registryLocator.getService().insertProfileForValidation(type, dateProblemSource.toString());

		assertNotNull("registered identifier", newId);
	}

	/**
	 * test that a validated profile is no more accessible in the pending collection but is visible the normal
	 * collection.
	 *
	 * @throws ISRegistryException
	 *             shouldn't happen
	 * @throws XPathExpressionException
	 *             shouldn't happen
	 * @throws SAXException
	 *             shouldn't happen
	 * @throws IOException
	 *             shouldn't happen
	 * @throws ParserConfigurationException
	 *             shouldn't happen
	 * @throws ISLookUpException
	 *             shouldn't happen
	 * @throws ISStoreException
	 *             shouldn't happen
	 */
	@Test
	public void testValidateProfile() throws XPathExpressionException, SAXException, IOException, ParserConfigurationException, ISRegistryException,
			ISLookUpException, ISStoreException {
		final OpaqueResource resource = new StringOpaqueResource(profileSource);
		final String kind = resource.getResourceKind();
		final String type = resource.getResourceType();

		final String oldId = registryLocator.getService().insertProfileForValidation(type, profileSource);
		final String newId = registryLocator.getService().validateProfile(oldId);

		final List<String> normalMatch = lookupLocator.getService().quickSearchProfile(buildQuery(newId, kind, type));
		assertNotNull("check normal", normalMatch);
		assertEquals("check normal size", 1, normalMatch.size());

		final List<String> pendingMatch = lookupLocator.getService().quickSearchProfile(buildQuery(newId, "PendingRepositoryResources", type));
		assertNull("check pending", pendingMatch);

	}

	/**
	 * test delete profile.
	 *
	 * @throws ISRegistryException
	 *             shouldn't happen
	 * @throws ISLookUpException
	 *             shouldn't happen
	 * @throws ISStoreException
	 *             shouldn't happen
	 * @throws XPathExpressionException
	 *             shouldn't happen
	 * @throws SAXException
	 *             shouldn't happen
	 * @throws IOException
	 *             shouldn't happen
	 * @throws ParserConfigurationException
	 *             shouldn't happen
	 */
	@Test
	public void testDeleteProfile() throws ISRegistryException, ISLookUpException, ISStoreException, XPathExpressionException, SAXException, IOException,
			ParserConfigurationException {
		final OpaqueResource resource = new StringOpaqueResource(profileSource);
		final String kind = resource.getResourceKind();
		final String type = resource.getResourceType();

		final String newId = registryLocator.getService().registerProfile(profileSource);

		assertNotNull("check exists", lookupLocator.getService().quickSearchProfile(buildQuery(newId, kind, type)));

		registryLocator.getService().deleteProfile(newId);

		assertNull("check deleted", lookupLocator.getService().quickSearchProfile(buildQuery(newId, kind, type)));
	}

	/**
	 * helper method: generates an xquery expression which matches a profile with a given id with a given type and kind.
	 *
	 * @param resId
	 *            resource id
	 * @param kind
	 *            resource kind
	 * @param type
	 *            resource type
	 * @return xquery source
	 */
	private String buildQuery(final String resId, final String kind, final String type) {
		return "collection('/db/DRIVER/" + kind + "/" + type + "')//RESOURCE_PROFILE[.//RESOURCE_IDENTIFIER/@value/string() ='" + resId + "']";
	}

	/**
	 * 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) {
			this.name = name;
		}

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

		public Throwable getThrowable() {
			return throwable;
		}
	}

	/**
	 * test concurrent registration.
	 *
	 * @throws Throwable
	 *             shouldn't happen
	 */
	@Test
	public void testConcurrent() throws Throwable {

		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);
	}

	/**
	 * parallel test job.
	 *
	 * @param arg
	 *            arg
	 * @throws ISRegistryException
	 *             could happen
	 * @throws ISLookUpException
	 *             could happen
	 */
	public void parallelJob(final int arg) throws ISRegistryException, ISLookUpException {
		log.info("job " + arg);

		for (int i = 0; i < PARALLEL_REPEAT; i++) {
			log.info(" starting job " + arg + ", iteration " + i);
			final String newId = registryLocator.getService().registerProfile(profileSource);
			String prof = lookupLocator.getService().getResourceProfile(newId);
			registryLocator.getService().updateProfile(newId, prof, "RepositoryServiceResourceType");

			final String invalid = registryLocator.getService().invalidateProfile(newId);
			registryLocator.getService().validateProfile(invalid);

			log.info("finishing job " + arg + ", iteration " + i);
		}

	}

	/**
	 * test resource type addition.
	 *
	 * @throws ISRegistryException
	 *             could happen.
	 * @throws IOException
	 *             shouldn't happen
	 */
	@Test
	public void testAddResourceType() throws ISRegistryException, IOException {
		final StringWriter resourceSchema = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("userProfileResourceSchema.xsd"), resourceSchema);
		registryLocator.getService().addResourceType("UserDSResourceType", resourceSchema.toString());

		final StringWriter profile = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("userProfile.xml"), profile);
		registryLocator.getService().registerProfile(profile.toString());
	}

	/**
	 * test resource type addition.
	 *
	 * @throws ISRegistryException
	 *             could happen.
	 * @throws IOException
	 *             shouldn't happen
	 */
	@Test
	public void testDeleteResourceType() throws ISRegistryException, IOException {
		final StringWriter resourceSchema = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("userProfileResourceSchema.xsd"), resourceSchema);
		registryLocator.getService().addResourceType("UserDSResourceType", resourceSchema.toString());

		final StringWriter profile = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("userProfile.xml"), profile);
		registryLocator.getService().registerProfile(profile.toString());

		registryLocator.getService().deleteResourceType("UserDSResourceType", false);

		boolean catched = false;
		try {
			registryLocator.getService().registerProfile(profile.toString());
		} catch (ISRegistryException e) {
			assertEquals("exception message", "profile is not conforming to the schema: no registered schema for " + "UserDSResourceType", e.getMessage());
			catched = true;
		}
		assertTrue("exception expected", catched);
	}

	/**
	 * secure profile registration.
	 *
	 * @throws IOException could happen
	 * @throws ISRegistryException could happen
	 * @throws ISLookUpException could happen
	 * @throws ParserConfigurationException shouldn't happen
	 * @throws SAXException  could't happen
	 * @throws XPathExpressionException  shouldn't happen
	 */
	@Test
	public void testRegisterSecureProfile() throws IOException, ISRegistryException, ISLookUpException, XPathExpressionException, SAXException, ParserConfigurationException {
		final StringWriter resourceSchema = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("userProfileResourceSchema.xsd"), resourceSchema);
		registryLocator.getService().addOrUpdateResourceType("UserDSResourceType", resourceSchema.toString());

		final StringWriter securitySchema = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("securityProfileResourceSchema.xsd"), securitySchema);
		registryLocator.getService().addOrUpdateResourceType("SecurityProfileDSResourceType", securitySchema.toString());

		final StringWriter profile = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("pendingUserProfile.xml"), profile);
		final String resourceProfId = registryLocator.getService().insertProfileForValidation("UserDSResourceType", profile.toString());

		final StringWriter security = new StringWriter();
		IOUtils.copy(getClass().getResourceAsStream("securityProfile.xml"), security);
		final String securityProfId = registryLocator.getService().registerProfile(security.toString());

		final String securedProfId = registryLocator.getService().registerSecureProfile(resourceProfId, securityProfId);

		assertNotNull("secure profile registration", securedProfId);

		final String updatedSecurityProfile = lookupLocator.getService().getResourceProfile(securityProfId);
		assertNotNull("security profile", updatedSecurityProfile);

		final OpaqueResource securityProfile = new StringOpaqueResource(updatedSecurityProfile);

		final String rid = XPathFactory.newInstance().newXPath().evaluate("/RESOURCE_PROFILE/BODY/CONFIGURATION/resourceId", securityProfile.asDom());
		assertEquals("check resource id", securedProfId, rid);
	}

	/**
	 * expect ISRegistryDocumentNotFoundException when deleting a inexistent profile.
	 *
	 * @throws ISRegistryException shouldn't happen
	 */
	@Test(expected = ISRegistryDocumentNotFoundException.class)
	public void testDeleteInexistentProfile() throws ISRegistryException {
		final String newId = registryLocator.getService().registerProfile(profileSource);

		final String wrongId = "xxy" + newId;
		registryLocator.getService().deleteProfile(wrongId);
	}
}
