package net.sf.jabref.shared;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.shared.exception.InvalidDBMSConnectionPropertiesException;
import net.sf.jabref.shared.exception.OfflineLockException;
import net.sf.jabref.testutils.category.DatabaseTests;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
@Category(DatabaseTests.class)
public class DBMSProcessorTest {

    private DBMSConnection dbmsConnection;
    private DBMSProcessor dbmsProcessor;

    @Parameter
    public DBMSType dbmsType;


    @Before
    public void setUp() throws SQLException, InvalidDBMSConnectionPropertiesException {
        dbmsConnection = TestConnector.getTestDBMSConnection(dbmsType);
        dbmsProcessor = DBMSProcessor.getProcessorInstance(dbmsConnection);
        dbmsProcessor.setupSharedDatabase();
    }

    @Parameters(name = "Test with {0} database system")
    public static Collection<DBMSType> getTestingDatabaseSystems() {
        return TestManager.getDBMSTypeTestParameter();
    }

    @Test
    public void testCheckBaseIntegrity() throws SQLException {
        Assert.assertTrue(dbmsProcessor.checkBaseIntegrity());
        clear();
        Assert.assertFalse(dbmsProcessor.checkBaseIntegrity());
    }

    @Test
    public void testSetUpSharedDatabase() throws SQLException {
        clear();
        dbmsProcessor.setupSharedDatabase();
        Assert.assertTrue(dbmsProcessor.checkBaseIntegrity());
    }

    @Test
    public void testInsertEntry() throws SQLException {
        BibEntry expectedEntry = getBibEntryExample();

        dbmsProcessor.insertEntry(expectedEntry);

        BibEntry emptyEntry = new BibEntry();
        emptyEntry.getSharedBibEntryData().setSharedID(1);
        dbmsProcessor.insertEntry(emptyEntry); // does not insert, due to same sharedID.

        Map<String, String> actualFieldMap = new HashMap<>();

        try (ResultSet entryResultSet = selectFrom("ENTRY")) {
            Assert.assertTrue(entryResultSet.next());
            Assert.assertEquals(1, entryResultSet.getInt("SHARED_ID"));
            Assert.assertEquals("inproceedings", entryResultSet.getString("TYPE"));
            Assert.assertEquals(1, entryResultSet.getInt("VERSION"));
            Assert.assertFalse(entryResultSet.next());

            try (ResultSet fieldResultSet = selectFrom("FIELD")) {
                while (fieldResultSet.next()) {
                    actualFieldMap.put(fieldResultSet.getString("NAME"), fieldResultSet.getString("VALUE"));
                }
            }
        }

        Map<String, String> expectedFieldMap = expectedEntry.getFieldMap();

        Assert.assertEquals(expectedFieldMap, actualFieldMap);
    }

    @Test
    public void testUpdateEntry() throws OfflineLockException, SQLException {
        BibEntry expectedEntry = getBibEntryExample();

        dbmsProcessor.insertEntry(expectedEntry);

        expectedEntry.setType("book");
        expectedEntry.setField("author", "Michael J and Hutchings");
        expectedEntry.setField("customField", "custom value");
        expectedEntry.clearField("booktitle");

        dbmsProcessor.updateEntry(expectedEntry);

        Optional<BibEntry> actualEntryOptional = dbmsProcessor
                .getSharedEntry(expectedEntry.getSharedBibEntryData().getSharedID());

        if (actualEntryOptional.isPresent()) {
            Assert.assertEquals(expectedEntry, actualEntryOptional.get());
        } else {
            Assert.fail();
        }
    }

    @Test(expected = OfflineLockException.class)
    public void testUpdateNewerEntry() throws OfflineLockException, SQLException {
        BibEntry bibEntry = getBibEntryExample();

        dbmsProcessor.insertEntry(bibEntry);

        bibEntry.getSharedBibEntryData().setVersion(0); // simulate older version
        bibEntry.setField("year", "1993");

        dbmsProcessor.updateEntry(bibEntry);
    }

    @Test
    public void testUpdateEqualEntry() throws OfflineLockException, SQLException {
        BibEntry expectedBibEntry = getBibEntryExample();

        dbmsProcessor.insertEntry(expectedBibEntry);

        expectedBibEntry.getSharedBibEntryData().setVersion(0); // simulate older version

        dbmsProcessor.updateEntry(expectedBibEntry);

        Optional<BibEntry> actualBibEntryOptional = dbmsProcessor
                .getSharedEntry(expectedBibEntry.getSharedBibEntryData().getSharedID());

        if (actualBibEntryOptional.isPresent()) {
            Assert.assertEquals(expectedBibEntry, actualBibEntryOptional.get());
        } else {
            Assert.fail();
        }
    }

    @Test
    public void testRemoveEntry() throws SQLException {
        BibEntry bibEntry = getBibEntryExample();
        dbmsProcessor.insertEntry(bibEntry);
        dbmsProcessor.removeEntry(bibEntry);

        try (ResultSet resultSet = selectFrom("ENTRY")) {
            Assert.assertFalse(resultSet.next());
        }
    }

    @Test
    public void testGetSharedEntries() {
        BibEntry bibEntry = getBibEntryExampleWithEmptyFields();

        dbmsProcessor.insertEntry(bibEntry);

        List<BibEntry> expectedEntries = Arrays.asList(bibEntry);
        List<BibEntry> actualEntries = dbmsProcessor.getSharedEntries();

        Assert.assertEquals(expectedEntries, actualEntries);
    }

    @Test
    public void testGetSharedEntry() {
        BibEntry expectedBibEntry = getBibEntryExampleWithEmptyFields();

        dbmsProcessor.insertEntry(expectedBibEntry);

        Optional<BibEntry> actualBibEntryOptional = dbmsProcessor
                .getSharedEntry(expectedBibEntry.getSharedBibEntryData().getSharedID());

        if (actualBibEntryOptional.isPresent()) {
            Assert.assertEquals(expectedBibEntry, actualBibEntryOptional.get());
        } else {
            Assert.fail();
        }
    }

    @Test
    public void testGetNotExistingSharedEntry() {
        Optional<BibEntry> actualBibEntryOptional = dbmsProcessor.getSharedEntry(1);
        Assert.assertFalse(actualBibEntryOptional.isPresent());
    }

    @Test
    public void testGetSharedIDVersionMapping() throws OfflineLockException, SQLException {
        BibEntry firstEntry = getBibEntryExample();
        BibEntry secondEntry = getBibEntryExample();

        dbmsProcessor.insertEntry(firstEntry);
        dbmsProcessor.insertEntry(secondEntry);
        dbmsProcessor.updateEntry(secondEntry);

        Map<Integer, Integer> expectedIDVersionMap = new HashMap<>();
        expectedIDVersionMap.put(firstEntry.getSharedBibEntryData().getSharedID(), 1);
        expectedIDVersionMap.put(secondEntry.getSharedBibEntryData().getSharedID(), 2);

        Map<Integer, Integer> actualIDVersionMap = dbmsProcessor.getSharedIDVersionMapping();

        Assert.assertEquals(expectedIDVersionMap, actualIDVersionMap);

    }

    @Test
    public void testGetSharedMetaData() {
        insertMetaData("databaseType", "bibtex;");
        insertMetaData("protectedFlag", "true;");
        insertMetaData("saveActions", "enabled;\nauthor[capitalize,html_to_latex]\ntitle[title_case]\n;");
        insertMetaData("saveOrderConfig", "specified;author;false;title;false;year;true;");

        Map<String, String> expectedMetaData = getMetaDataExample();
        Map<String, String> actualMetaData = dbmsProcessor.getSharedMetaData();

        Assert.assertEquals(expectedMetaData, actualMetaData);

    }

    @Test
    public void testSetSharedMetaData() throws SQLException {
        Map<String, String> expectedMetaData = getMetaDataExample();
        dbmsProcessor.setSharedMetaData(expectedMetaData);

        Map<String, String> actualMetaData = dbmsProcessor.getSharedMetaData();

        Assert.assertEquals(expectedMetaData, actualMetaData);
    }

    private Map<String, String> getMetaDataExample() {
        Map<String, String> expectedMetaData = new HashMap<>();

        expectedMetaData.put("databaseType", "bibtex;");
        expectedMetaData.put("protectedFlag", "true;");
        expectedMetaData.put("saveActions", "enabled;\nauthor[capitalize,html_to_latex]\ntitle[title_case]\n;");
        expectedMetaData.put("saveOrderConfig", "specified;author;false;title;false;year;true;");

        return expectedMetaData;
    }

    private BibEntry getBibEntryExampleWithEmptyFields() {
        BibEntry bibEntry = new BibEntry();
        bibEntry.setField("author", "Author");
        bibEntry.setField("title", "");
        bibEntry.setField("year", "");
        bibEntry.getSharedBibEntryData().setSharedID(1);
        return bibEntry;
    }

    private BibEntry getBibEntryExample() {
        BibEntry bibEntry = new BibEntry();
        bibEntry.setType("inproceedings");
        bibEntry.setField("author", "Wirthlin, Michael J and Hutchings, Brad L and Gilson, Kent L");
        bibEntry.setField("title", "The nano processor: a low resource reconfigurable processor");
        bibEntry.setField("booktitle", "FPGAs for Custom Computing Machines, 1994. Proceedings. IEEE Workshop on");
        bibEntry.setField("year", "1994");
        bibEntry.setCiteKey("nanoproc1994");
        return bibEntry;
    }

    private ResultSet selectFrom(String table) {
        try {
            return dbmsConnection.getConnection().createStatement().executeQuery("SELECT * FROM " + escape(table));
        } catch (SQLException e) {
            Assert.fail(e.getMessage());
            return null;
        }
    }

    // Oracle does not support multiple tuple insertion in one INSERT INTO command.
    // Therefore this function was defined to improve the readability and to keep the code short.
    private void insertMetaData(String key, String value) {
        try {
            dbmsConnection.getConnection().createStatement().executeUpdate("INSERT INTO " + escape("METADATA") + "("
                    + escape("KEY") + ", " + escape("VALUE") + ") VALUES("
                    + escapeValue(key) + ", " + escapeValue(value) + ")");
        } catch (SQLException e) {
            Assert.fail(e.getMessage());
        }
    }

    private String escape(String expression) {
        return dbmsProcessor.escape(expression);
    }

    private String escapeValue(String value) {
        return "'" + value + "'";
    }

    @After
    public void clear() throws SQLException {
        TestManager.clearTables(dbmsConnection);
    }
}
