package eu.dnetlib.dlms.jdbc;

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

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.sql.Date;
import java.sql.ParameterMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Calendar;

import javax.annotation.Resource;

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.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import eu.dnetlib.dlms.jdbc.DorotyObjectEnum.DorotyCode;
import eu.dnetlib.dlms.jdbc.parser.DummyParserFactory;
import eu.dnetlib.dlms.lowlevel.objects.Atom;
import eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject;
import eu.dnetlib.dlms.lowlevel.objects.Relation;
import eu.dnetlib.dlms.lowlevel.objects.structures.BooleanBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.DateBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.IntBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.StringBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.Structure;

/**
 * Test class for DOLPreparedStatement. TODO: This test will work until there's no real control on the types of
 * parameters since there are 2 placeholders in the dolString and this class' methods set in them different types of
 * object as parameters
 * 
 * @author lexis
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestDOLPreparedStatement {
	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(TestDOLPreparedStatement.class);
	/** DataSource istance. */
	@Resource
	private DorotyDataSource dataSource;

	/** PreparedStatement instance under test. */
	private DOLPreparedStatement statement;
	/** Number of parameter placeholders in the string in Doroty language used for test (this.dolString). */
	private final int numOfParamInDolString = 2;
	/** Fake dolString with two parameters placeholders used in test methods. */
	private final String dolString = "Fake dolString with 2 parameter's placeholder: param1 = ? and param2 = ?";

	/** Parser factory used by the engine to create parsers. */
	@Resource
	private DummyParserFactory parserFactory;

	/**
	 * Init this.statement with a statement that, when executed will not generate a Resultset.
	 * 
	 * @throws SQLException
	 *             getting connection from the datasorurce.
	 */
	@Before
	public void beforeMethod() throws SQLException {
		this.statement = new DOLPreparedStatement(this.dataSource.getConnection(), this.dolString);
		this.statement.setResultSetFactory(this.dataSource.getResultSetFactory());
	}

	/**
	 * Test method for
	 * {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#DOLPreparedStatement(eu.dnetlib.dlms.jdbc.DorotyConnection)}.
	 * 
	 * @throws SQLException
	 *             getting connection from datasource
	 */
	@Test
	@DirtiesContext
	public void testDOLPreparedStatementDorotyConnection() throws SQLException {
		this.statement = new DOLPreparedStatement(this.dataSource.getConnection());
		assertNotNull(this.statement);
		this.statement = null;
	}

	/**
	 * Test method for
	 * {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#DOLPreparedStatement(eu.dnetlib.dlms.jdbc.DorotyConnection, java.lang.String)}
	 * .
	 * 
	 * @throws SQLException
	 *             getting connection from datasource
	 */
	@Test
	@DirtiesContext
	public void testDOLPreparedStatementDorotyConnectionString() throws SQLException {
		this.statement = new DOLPreparedStatement(this.dataSource.getConnection(), this.dolString);
		assertNotNull(this.statement);
		this.statement = null;
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#close()}.
	 * 
	 * @throws SQLException
	 *             on closing statement
	 */
	@Test
	@DirtiesContext
	public void testClose() throws SQLException {
		assertFalse(this.statement.isClosed());
		this.statement.close();
		assertTrue(this.statement.isClosed());
		this.statement = null;
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#getParameters()}.
	 */
	@Test
	@DirtiesContext
	public void testGetParameters() {
		ParamInfo[] param = this.statement.getParameters();
		assertNotNull(param);
		assertTrue(param.length == this.numOfParamInDolString);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#addBatch()}.
	 * 
	 * @throws SQLException
	 *             addBatch method currently always throws an SQLFeatureNotSupportedException
	 */
	@Test(expected = SQLFeatureNotSupportedException.class)
	@DirtiesContext
	public void testAddBatch() throws SQLException {
		this.statement.addBatch();
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#clearParameters()}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testClearParameters() throws SQLException {
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		ParamInfo[] beforeClear = this.statement.getParameters();
		for (ParamInfo p : beforeClear)
			assertNotNull(p);
		this.statement.clearParameters();
		ParamInfo[] afterClear = this.statement.getParameters();
		for (ParamInfo p : afterClear)
			assertNull(p);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#execute()}.
	 * 
	 * @throws SQLException
	 *             executing statement
	 */
	@Test
	@DirtiesContext
	public void testExecuteNoRS() throws SQLException {
		this.parserFactory.setResultSetExpected(false);
		this.dataSource.getDolEngineFactory().setParserFactory(this.parserFactory);
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		assertFalse(this.statement.execute());
		assertNull(this.statement.getResultSet());
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#execute()}.
	 * 
	 * @throws SQLException
	 *             executing statement
	 */
	@Test
	@DirtiesContext
	public void testExecuteRS() throws SQLException {
		this.parserFactory.setResultSetExpected(true);
		this.dataSource.getDolEngineFactory().setParserFactory(this.parserFactory);
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		assertTrue(this.statement.execute());
		DOLResultSet rs = this.statement.getResultSet();
		assertNotNull(rs);
		while (rs.next()) {
			log.debug("Current row in DOLResultSet: " + rs.getCurrentRow().toString());
		}
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#executeQuery()}.
	 * 
	 * @throws SQLException
	 *             executing statement
	 */
	@Test
	@DirtiesContext
	public void testExecuteQuery() throws SQLException {
		this.parserFactory.setResultSetExpected(true);
		this.dataSource.getDolEngineFactory().setParserFactory(this.parserFactory);
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		ResultSet rs = this.statement.executeQuery();
		ResultSetMetaData rsMd = this.statement.getMetaData();
		int columnNum = rsMd.getColumnCount();
		while (rs.next()) {
			for (int i = 1; i <= columnNum; i++)
				log.info(rs.getObject(i));
		}
		rs.close();
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#executeQuery()}.
	 * 
	 * @throws SQLException
	 *             executing statement
	 */
	@Test(expected = SQLException.class)
	@DirtiesContext
	public void testExecuteQueryExce() throws SQLException {
		this.parserFactory.setResultSetExpected(false);
		this.dataSource.getDolEngineFactory().setParserFactory(this.parserFactory);
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		ResultSet rs = this.statement.executeQuery();
		rs.close();
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#executeUpdate()}.
	 * 
	 * @throws SQLException
	 *             executeUpdate method currently always throw an SQLFeatureNotSupportedException
	 */
	@Test(expected = SQLFeatureNotSupportedException.class)
	@DirtiesContext
	public void testExecuteUpdate() throws SQLException {
		this.statement.executeUpdate();
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#getParameterMetaData()}.
	 * 
	 * @throws SQLException
	 *             getting parameter metadata or setting parameter values
	 */
	@Test
	@DirtiesContext
	public void testGetParameterMetaData() throws SQLException {
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		ParameterMetaData pmd = this.statement.getParameterMetaData();
		assertNotNull(pmd);
		assertTrue(pmd.getParameterCount() == this.numOfParamInDolString);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setBoolean(int, boolean)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetBoolean() throws SQLException {
		boolean boolValue = true;
		Boolean boolObj = new Boolean(false);
		this.statement.setBoolean(1, boolValue);
		this.statement.setBoolean(2, boolObj);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setDate(int, java.sql.Date)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetDateIntDate() throws SQLException {
		Date aDate = new Date(System.currentTimeMillis());
		this.statement.setDate(1, aDate);
		Date bDate = new Date(System.currentTimeMillis());
		this.statement.setDate(2, bDate);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setDate(int, java.sql.Date, java.util.Calendar)}
	 * .
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetDateIntDateCalendar() throws SQLException {
		Date aDate = new Date(System.currentTimeMillis());
		Calendar cal = Calendar.getInstance();
		this.statement.setDate(1, aDate, cal);
		Date bDate = new Date(System.currentTimeMillis());
		this.statement.setDate(2, bDate, cal);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setDouble(int, double)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetDouble() throws SQLException {
		double doubleValue = 0;
		Double doubleObj = new Double(1);
		this.statement.setDouble(1, doubleValue);
		this.statement.setDouble(2, doubleObj);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Checks if the info array and its content is not null and prints logs with info on its contents.
	 * 
	 * @param info
	 *            array of ParamInfo[] objects
	 */
	private void logParamInfo(final ParamInfo[] info) {
		assertNotNull(info);
		for (int i = 0; i < info.length; i++) {
			ParamInfo current = info[i];
			assertNotNull(current);
			log.info("Param " + i + " is a " + current.getJavaClassName() + ", its DorotyObjectEnum = " + current.getType() + ", its value is "
					+ current.getValue());
		}
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setInt(int, int)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetInt() throws SQLException {
		this.statement.setInt(1, 1);
		this.statement.setInt(2, 2);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setLong(int, long)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetLong() throws SQLException {
		this.statement.setLong(1, 1);
		this.statement.setLong(2, 2);
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setObject(int, java.lang.Object)}. Actually
	 * always throws an SQLFeatureNotSupportedException. The method to use to set objects is the one that has the type
	 * of the object as a parameter (look after)
	 * 
	 * @throws URISyntaxException
	 *             URI creation issues
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test(expected = SQLFeatureNotSupportedException.class)
	@DirtiesContext
	public void testSetObjectIntObject() throws URISyntaxException, SQLException {
		this.statement.setObject(1, null);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setObject(int, java.lang.Object, int)}.
	 * 
	 * @throws URISyntaxException
	 *             URI creation issues
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetObjectIntObjectInt() throws URISyntaxException, SQLException {
		DOLPreparedStatement stm = new DOLPreparedStatement(this.dataSource.getConnection(),
				"Put here any placeholder u need for parameter count: ? ? ? ? ? ? ? ?");
		this.statement.setResultSetFactory(this.dataSource.getResultSetFactory());
		//integer increased to set parameters at subsequent indices
		int i = 1;
		//First let's try with an Atom
		Atom atom1 = new Atom();
		atom1.setId(i);
		atom1.setInfo("Atom to test PreparedStatement.setObject(int, Object, int)");
		atom1.setLocalCopyEnabled(false);
		atom1.setUriToPayload(new URI("http://www.google.it"));
		stm.setObject(i++, atom1, DorotyCode.ATOM);
		//second parameter is a StringValue
		StringBasicValue strValue = new StringBasicValue();
		strValue.setValue("StringValue's value");
		stm.setObject(i++, strValue, DorotyCode.STRINGVALUE);
		//IntegerValue
		IntBasicValue intValue = new IntBasicValue();
		intValue.setValue(0);
		stm.setObject(i++, intValue, DorotyCode.INTEGERVALUE);
		//DateBasicValue
		DateBasicValue dateValue = new DateBasicValue();
		dateValue.setValue(Calendar.getInstance().getTime());
		stm.setObject(i++, dateValue, DorotyCode.DATEVALUE);
		//BooleanBasicValue
		BooleanBasicValue boolValue = new BooleanBasicValue();
		boolValue.setValue(true);
		stm.setObject(i++, boolValue, DorotyCode.BOOLEANVALUE);
		Relation rel = new Relation();
		rel.setId(i);
		rel.setInfo("Relation to test PreparedStatement.setObject(int, Object, int)");
		rel.setFstObj(atom1);
		rel.setSndObj(atom1);
		stm.setObject(i++, rel, DorotyCode.RELATION);
		Structure structure = new Structure();
		structure.setId(i);
		structure.setInfo("Structure to test PreparedStatement.setObject(int, Object, int)");
		stm.setObject(i++, structure, DorotyCode.STRUCTURE);
		LLDigitalObject obj = new LLDigitalObject();
		obj.setId(i);
		obj.setInfo("Object to test PreparedStatement.setObject(int, Object, int)");
		stm.setObject(i++, obj, DorotyCode.OBJECT);
		ParamInfo[] info = stm.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setString(int, java.lang.String)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 */
	@Test
	@DirtiesContext
	public void testSetString() throws SQLException {
		this.statement.setString(1, "String1");
		this.statement.setString(2, "String2");
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

	/**
	 * Test method for {@link eu.dnetlib.dlms.jdbc.DOLPreparedStatement#setURL(int, java.net.URL)}.
	 * 
	 * @throws SQLException
	 *             setting parameters
	 * @throws MalformedURLException
	 *             creating URLs
	 */
	@Test
	@DirtiesContext
	public void testSetURL() throws MalformedURLException, SQLException {
		this.statement.setURL(1, new URL("http://www.google.it"));
		this.statement.setURL(2, new URL("http://www.nin.com"));
		ParamInfo[] info = this.statement.getParameters();
		this.logParamInfo(info);
	}

}
