/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.tests.util;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.internal.ArrayComparisonFailure;
import org.syncany.chunk.Transformer;
import org.syncany.database.ChunkEntry;
import org.syncany.database.DatabaseConnectionFactory;
import org.syncany.database.DatabaseVersion;
import org.syncany.database.FileContent;
import org.syncany.database.FileVersionComparator;
import org.syncany.database.FileVersionComparator.FileChange;
import org.syncany.database.FileVersionComparator.FileProperties;
import org.syncany.database.FileVersionComparator.FileVersionComparison;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.MultiChunkEntry;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.VectorClock;
import org.syncany.tests.unit.util.TestFileUtil;
import org.syncany.util.CollectionUtil;
import org.syncany.util.FileUtil;
import org.syncany.util.StringUtil;

import com.google.common.collect.Lists;

public class TestAssertUtil {
	private static final Logger logger = Logger.getLogger(TestAssertUtil.class.getSimpleName());

	public static void assertCollectionEquals(String message, Collection<? extends Object> expected, Collection<? extends Object> actual) {
		assertEquals(message + ": Different amount of objects.", expected.size(), actual.size());

		int i = 0;
		for (Iterator<? extends Object> expectedIt = expected.iterator(), actualIt = actual.iterator(); expectedIt.hasNext() && actualIt.hasNext();) {
			Object expectedObj = expectedIt.next();
			Object actualObj = actualIt.next();

			assertEquals(message + ": actual[" + i + "] differs from expected[" + i + "]: ", expectedObj, actualObj);
			i++;
		}
	}

	public static void assertFileListEqualsExcludeLockedAndNoRead(File expectedFilesRoot, File actualFilesRoot) throws ArrayComparisonFailure,
			Exception {
		Map<String, File> expectedFiles = TestFileUtil.getLocalFilesExcludeLockedAndNoRead(expectedFilesRoot);
		Map<String, File> actualFiles = TestFileUtil.getLocalFilesExcludeLockedAndNoRead(actualFilesRoot);

		assertFileListEquals("File list does not match", expectedFiles, actualFiles);
	}

	public static void assertFileListEquals(Map<String, File> expectedFiles, Map<String, File> actualFiles) throws ArrayComparisonFailure, Exception {
		assertFileListEquals("File list does not match", expectedFiles, actualFiles);
	}

	public static void assertFileListEquals(String message, Map<String, File> expectedFiles, Map<String, File> actualFiles)
			throws ArrayComparisonFailure, Exception {
		assertCollectionEquals("Actual file list (" + actualFiles.size() + " entries) differs from expected file list (" + expectedFiles.size()
				+ " entries)", expectedFiles.keySet(), actualFiles.keySet());

		for (Map.Entry<String, File> expectedFileEntry : expectedFiles.entrySet()) {
			File expectedFile = expectedFileEntry.getValue();
			File actualFile = actualFiles.remove(expectedFileEntry.getKey());

			assertFileEquals(expectedFile, actualFile);
		}
	}

	public static void assertConflictingFileExists(String originalFile, Map<String, File> actualFiles) {
		boolean conflictingFileExists = conflictingFileExists(originalFile, actualFiles);
		assertTrue("Conflicting file for '" + originalFile + "' does NOT exist, but it should exist.", conflictingFileExists);
	}

	public static void assertConflictingFileNotExists(String originalFile, Map<String, File> actualFiles) {
		boolean conflictingFileExists = conflictingFileExists(originalFile, actualFiles);
		assertFalse("Conflicting file for '" + originalFile + "' does exist, but it should NOT exist.", conflictingFileExists);
	}

	private static boolean conflictingFileExists(String originalFile, Map<String, File> actualFiles) {
		String fileNameWithoutExtention = TestFileUtil.getBasename(originalFile);
		Pattern conflictFilePattern = Pattern.compile(fileNameWithoutExtention + ".*conflicted.*");

		boolean conflictingFileFound = false;

		for (Map.Entry<String, File> actualFileEntry : actualFiles.entrySet()) {
			File actualFile = actualFileEntry.getValue();
			Matcher matcher = conflictFilePattern.matcher(actualFile.getName());

			if (matcher.matches()) {
				conflictingFileFound = true;
				break;
			}
		}

		return conflictingFileFound;
	}

	public static void assertFileEquals(File expectedFile, File actualFile) throws ArrayComparisonFailure, Exception {
		assertFileEquals(expectedFile, actualFile, new FileChange[] { FileChange.CHANGED_PATH });
	}

	public static void assertFileEquals(File expectedFile, File actualFile, FileChange... allowedChanges) throws ArrayComparisonFailure, Exception {
		if (expectedFile == null && actualFile == null) {
			return;
		}

		assertNotNull("Files are not equal: Actual file is " + actualFile + ", expected file is null.", expectedFile);
		assertNotNull("Files are not equal: Expected file is " + expectedFile + ", actual file is null.", actualFile);

		Path root = Paths.get(actualFile.getAbsolutePath()).getRoot();
		FileVersionComparator fileVersionComparator = new FileVersionComparator(root.toFile(), "SHA1");

		if (!FileUtil.exists(expectedFile)) {
			fail("Files are not equal: Expected file " + expectedFile + " does not exist.");
		}

		if (!FileUtil.exists(actualFile)) {
			fail("Files are not equal: Actual file " + actualFile + " does not exist.");
		}

		if (FileUtil.isSymlink(actualFile) && FileUtil.isSymlink(expectedFile)) {
			return;
		}

		if (actualFile.isDirectory() != expectedFile.isDirectory()) {
			fail("Files are not equal: Comparing a directory with a file (actual is dir = " + actualFile.isDirectory() + ", expected is dir = "
					+ expectedFile.isDirectory() + ")");
		}

		if (actualFile.isDirectory() && expectedFile.isDirectory()) {
			return;
		}

		if (actualFile.length() != expectedFile.length()) {
			fail("Files are not equal: Actual file size (" + actualFile + " = " + actualFile.length() + ") does not match expected file size ("
					+ expectedFile + " = " + expectedFile.length() + ")");
		}

		byte[] expectedFileChecksum = TestFileUtil.createChecksum(expectedFile);
		byte[] actualFileChecksum = TestFileUtil.createChecksum(actualFile);

		assertArrayEquals("Files are not equal: Actual file checksum (" + StringUtil.toHex(actualFileChecksum) + ") and expected file checksum ("
				+ StringUtil.toHex(expectedFileChecksum) + ") do not match.", expectedFileChecksum, actualFileChecksum);

		FileProperties actualFileProperties = fileVersionComparator.captureFileProperties(actualFile, null, true);
		FileProperties expectedFileProperties = fileVersionComparator.captureFileProperties(expectedFile, null, true);

		FileVersionComparison fileVersionComparison = fileVersionComparator.compare(expectedFileProperties, actualFileProperties, true);

		List<FileChange> allowedChangesList = new ArrayList<FileChange>(Arrays.asList(allowedChanges));
		allowedChangesList.add(FileChange.CHANGED_PATH);

		if (!CollectionUtil.containsOnly(fileVersionComparison.getFileChanges(), allowedChangesList)) {
			fail("Files are not equal: Actual file differs from expected file: " + fileVersionComparison.getFileChanges());
		}
	}

	public static void assertSqlResultEquals(File databaseFile, String sqlQuery, String expectedResultStr) throws SQLException {
		Connection databaseConnection = DatabaseConnectionFactory.createConnection(databaseFile, true);
		ResultSet resultSet = databaseConnection.prepareStatement(sqlQuery).executeQuery();

		List<String> actualResultStrList = new ArrayList<String>();

		while (resultSet.next()) {
			for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
				actualResultStrList.add(resultSet.getString(i));
			}
		}

		String actualResultStr = StringUtil.join(actualResultStrList, ",");
		assertEquals("SQL query result differs: " + sqlQuery, expectedResultStr, actualResultStr);
	}

	public static void assertSqlDatabaseEquals(File expectedDatabaseFile, File actualDatabaseFile) throws IOException, SQLException {
		// Compare tables + ignore columns
		String[][] compareTablesAndIgnoreColumns = new String[][] {
				new String[] { "chunk", "DATABASEVERSION_ID" },
				new String[] { "databaseversion", "ID" },
				new String[] { "databaseversion_vectorclock", "DATABASEVERSION_ID" },
				new String[] { "filecontent", "DATABASEVERSION_ID" },
				new String[] { "filecontent_chunk" },
				new String[] { "filehistory", "DATABASEVERSION_ID" },
				new String[] { "fileversion", "DATABASEVERSION_ID" },
				// skipped known_databases
				new String[] { "multichunk", "DATABASEVERSION_ID" },
				new String[] { "multichunk_chunk" }
		};

		assertSqlDatabaseTablesEqual(expectedDatabaseFile, actualDatabaseFile, compareTablesAndIgnoreColumns);
	}

	public static void assertSqlDatabaseTablesEqual(File expectedDatabaseFile, File actualDatabaseFile, String[]... compareTablesAndIgnoreColumns)
			throws IOException, SQLException {
		Connection expectedDatabaseConnection = DatabaseConnectionFactory.createConnection(expectedDatabaseFile, true);
		Connection actualDatabaseConnection = DatabaseConnectionFactory.createConnection(actualDatabaseFile, true);

		for (String[] tableNameAndIgnoreColumns : compareTablesAndIgnoreColumns) {
			String tableName = tableNameAndIgnoreColumns[0];

			List<String> ignoreColumnNames = Lists.newArrayList(tableNameAndIgnoreColumns);
			ignoreColumnNames.remove(0);

			// Get table's primary keys
			List<String> primaryKeys = new ArrayList<String>();
			ResultSet resultSet = actualDatabaseConnection.getMetaData().getPrimaryKeys(null, null, tableName.toUpperCase());

			while (resultSet.next()) {
				primaryKeys.add(resultSet.getString("COLUMN_NAME"));
			}

			// Get table's columns
			List<String> tableColumns = new ArrayList<String>();
			resultSet = actualDatabaseConnection.getMetaData().getColumns(null, null, tableName.toUpperCase(), null);

			while (resultSet.next()) {
				String columnName = resultSet.getString("COLUMN_NAME");

				if (!ignoreColumnNames.contains(columnName)) {
					tableColumns.add(columnName);
				}
			}

			// Get all entries of both tables, sorted by the primary keys
			String columnNameList = StringUtil.join(tableColumns, ", ");
			String primaryKeysOrderByClause = StringUtil.join(primaryKeys, " asc, ") + " asc";
			String selectQuery = String.format("select %s from %s order by %s", columnNameList, tableName, primaryKeysOrderByClause);

			logger.log(Level.FINE, " Comparing database table: " + selectQuery);

			ResultSet expectedResultSet = expectedDatabaseConnection.prepareStatement(selectQuery).executeQuery();
			ResultSet actualResultSet = actualDatabaseConnection.prepareStatement(selectQuery).executeQuery();

			while (true) {
				boolean expectedNext = expectedResultSet.next();
				boolean actualNext = actualResultSet.next();

				if (expectedNext && !actualNext) {
					fail("Table " + tableName + ": Actual is missing the following row from expected: " + getFormattedColumn(expectedResultSet));
				}
				else if (!expectedNext && actualNext) {
					fail("Table " + tableName + ": Actual has a row that was not expected: " + getFormattedColumn(actualResultSet));
				}
				else if (!expectedNext && !actualNext) {
					break;
				}
				else {
					String expectedFormattedColumn = getFormattedColumn(expectedResultSet);
					String actualFormattedColumn = getFormattedColumn(actualResultSet);

					assertEquals("Table " + tableName + ": Columns of actual and expected differ.", expectedFormattedColumn, actualFormattedColumn);
				}
			}
		}
	}

	private static String getFormattedColumn(ResultSet resultSet) throws SQLException {
		ResultSetMetaData metaData = resultSet.getMetaData();
		List<String> formattedColumnLine = new ArrayList<String>();

		for (int i = 0; i < metaData.getColumnCount(); i++) {
			formattedColumnLine.add(metaData.getColumnName(i + 1) + "=" + resultSet.getString(i + 1));
		}

		return StringUtil.join(formattedColumnLine, ", ");
	}

	public static void assertXmlDatabaseFileEquals(File expectedDatabaseFile, File actualDatabaseFile, Transformer transformer) throws IOException {
		MemoryDatabase expectedDatabase = TestDatabaseUtil.readDatabaseFileFromDisk(expectedDatabaseFile, transformer);
		MemoryDatabase actualDatabase = TestDatabaseUtil.readDatabaseFileFromDisk(actualDatabaseFile, transformer);

		assertDatabaseEquals(expectedDatabase, actualDatabase);
	}

	public static void assertDatabaseEquals(MemoryDatabase expectedDatabase, MemoryDatabase actualDatabase) {
		logger.log(Level.INFO, "--");
		logger.log(Level.INFO, "Now comparing two databases.");
		logger.log(Level.INFO, "DON'T WORRY. This can take a long time or even overload the heap space.");

		List<DatabaseVersion> writtenDatabaseVersions = expectedDatabase.getDatabaseVersions();
		List<DatabaseVersion> readDatabaseVersions = actualDatabase.getDatabaseVersions();

		assertEquals("Different number of database versions.", writtenDatabaseVersions.size(), readDatabaseVersions.size());

		for (DatabaseVersion writtenDatabaseVersion : writtenDatabaseVersions) {
			DatabaseVersion readDatabaseVersion = null;

			for (DatabaseVersion aReadDatabaseVersion : readDatabaseVersions) {
				if (aReadDatabaseVersion.equals(writtenDatabaseVersion)) {
					readDatabaseVersion = aReadDatabaseVersion;
					break;
				}
			}

			assertNotNull("Database version " + writtenDatabaseVersion + " does not exist in read database.", readDatabaseVersion);

			assertDatabaseVersionEquals(writtenDatabaseVersion, readDatabaseVersion);
		}

		logger.log(Level.INFO, "End of comparing databases");
		logger.log(Level.INFO, "--");
	}

	public static void assertDatabaseVersionEquals(DatabaseVersion expectedDatabaseVersion, DatabaseVersion actualDatabaseVersion) {
		assertVectorClockEquals(expectedDatabaseVersion.getVectorClock(), actualDatabaseVersion.getVectorClock());
		compareDatabaseVersionChunks(expectedDatabaseVersion.getChunks(), actualDatabaseVersion.getChunks());
		compareDatabaseVersionMultiChunks(expectedDatabaseVersion.getMultiChunks(), actualDatabaseVersion.getMultiChunks());
		compareDatabaseVersionFileContents(expectedDatabaseVersion.getFileContents(), actualDatabaseVersion.getFileContents());
		compareDatabaseVersionFileHistories(expectedDatabaseVersion.getFileHistories(), actualDatabaseVersion.getFileHistories());
	}

	public static void assertVectorClockEquals(VectorClock expectedVectorClock, VectorClock actualVectorClock) {
		assertEquals("Vector clocks differ.", expectedVectorClock, actualVectorClock);
	}

	private static void compareDatabaseVersionChunks(Collection<ChunkEntry> writtenChunks, Collection<ChunkEntry> readChunks) {
		assertEquals("Different amount of Chunk objects.", writtenChunks.size(), readChunks.size());
		assertTrue("Chunk objects in written/read database version different.", writtenChunks.containsAll(readChunks));
		//assertCollectionEquals("Chunk objects in written/read database version different.", writtenChunks, readChunks);
	}

	private static void compareDatabaseVersionMultiChunks(Collection<MultiChunkEntry> writtenMultiChunks, Collection<MultiChunkEntry> readMultiChunks) {
		assertEquals("Different amount of MultiChunk objects.", writtenMultiChunks.size(), readMultiChunks.size());
		assertTrue("MultiChunk objects in written/read database version different.", writtenMultiChunks.containsAll(readMultiChunks));
		//assertCollectionEquals("MultiChunk objects in written/read database version different.", writtenMultiChunks, readMultiChunks);
	}

	private static void compareDatabaseVersionFileContents(Collection<FileContent> writtenFileContents, Collection<FileContent> readFileContents) {
		assertEquals("Different amount of FileContent objects.", writtenFileContents.size(), readFileContents.size());
		assertTrue("FileContent objects in written/read database version different.", writtenFileContents.containsAll(readFileContents));
		//assertCollectionEquals("FileContent objects in written/read database version different.", writtenFileContents, readFileContents);
	}

	private static void compareDatabaseVersionFileHistories(Collection<PartialFileHistory> writtenFileHistories,
			Collection<PartialFileHistory> readFileHistories) {
		assertTrue("FileHistory objects in written/read database version different.", writtenFileHistories.containsAll(readFileHistories));
	}

	public static void assertErrorStackTraceContains(String expectedContains, Exception e) {
		StringWriter errors = new StringWriter();
		e.printStackTrace(new PrintWriter(errors));

		String stackTrace = errors.toString();

		if (stackTrace.contains(expectedContains)) {
			return;
		}

		e.printStackTrace();
		fail("Stack trace expected to contain " + expectedContains);
	}

	public static void assertRegexInLines(String expectedLinePattern, String[] lines) {
		Pattern expectedPattern = Pattern.compile(expectedLinePattern);

		for (String line : lines) {
			if (expectedPattern.matcher(line).find()) {
				return;
			}
		}

		fail("Output does not contain " + expectedLinePattern);
	}
}
