/*
 * 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.integration.database.dao;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.syncany.tests.util.TestAssertUtil.assertDatabaseVersionEquals;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.syncany.config.Logging;
import org.syncany.database.ChunkEntry;
import org.syncany.database.ChunkEntry.ChunkChecksum;
import org.syncany.database.DatabaseVersion;
import org.syncany.database.FileContent;
import org.syncany.database.FileContent.FileChecksum;
import org.syncany.database.FileVersion;
import org.syncany.database.FileVersion.FileStatus;
import org.syncany.database.FileVersion.FileType;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.MultiChunkEntry;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.PartialFileHistory.FileHistoryId;
import org.syncany.database.VectorClock;
import org.syncany.database.dao.DatabaseXmlSerializer;
import org.syncany.database.dao.DatabaseXmlSerializer.DatabaseReadType;
import org.syncany.tests.unit.util.TestFileUtil;
import org.syncany.tests.util.TestAssertUtil;
import org.syncany.tests.util.TestDatabaseUtil;

public class XmlDatabaseDaoTest {
	private File tempDir;

	static {
		Logging.init();
	}

	@Before
	public void setUp() throws Exception {
		tempDir = TestFileUtil.createTempDirectoryInSystemTemp();
	}

	@After
	public void tearDown() {
		TestFileUtil.deleteDirectory(tempDir);
	}

	@Test
	public void testWriteAndReadChunks() throws IOException {
		// Prepare
		MemoryDatabase newDatabase = new MemoryDatabase();
		DatabaseVersion newDatabaseVersion = createDatabaseVersion();

		// Create chunks
		ChunkEntry chunkA1 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 2, 3, 4, 5, 7, 8, 9, 0 }), 12);
		ChunkEntry chunkA2 = new ChunkEntry(new ChunkChecksum(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 }), 34);
		ChunkEntry chunkA3 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1 }), 56);
		ChunkEntry chunkA4 = new ChunkEntry(new ChunkChecksum(new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2 }), 78);

		newDatabaseVersion.addChunk(chunkA1);
		newDatabaseVersion.addChunk(chunkA2);
		newDatabaseVersion.addChunk(chunkA3);
		newDatabaseVersion.addChunk(chunkA4);

		// Add database version
		newDatabase.addDatabaseVersion(newDatabaseVersion);

		// Write database to disk, read it again, and compare them
		MemoryDatabase loadedDatabase = writeReadAndCompareDatabase(newDatabase);

		// Check chunks
		assertEquals("Chunk not found in database loaded.", chunkA1, loadedDatabase.getChunk(chunkA1.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA2, loadedDatabase.getChunk(chunkA2.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA3, loadedDatabase.getChunk(chunkA3.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA4, loadedDatabase.getChunk(chunkA4.getChecksum()));
	}

	private DatabaseVersion createDatabaseVersion() {
		return createDatabaseVersion(null);
	}

	private DatabaseVersion createDatabaseVersion(DatabaseVersion basedOnDatabaseVersion) {
		VectorClock vectorClock = (basedOnDatabaseVersion != null) ? basedOnDatabaseVersion.getVectorClock().clone() : new VectorClock();
		vectorClock.incrementClock("someclient");

		DatabaseVersion databaseVersion = new DatabaseVersion();

		databaseVersion.setClient("someclient");
		databaseVersion.setTimestamp(new Date());
		databaseVersion.setVectorClock(vectorClock);

		return databaseVersion;
	}

	@Test
	public void testWriteAndReadChunksWithMultiChunks() throws IOException {
		// Prepare
		MemoryDatabase newDatabase = new MemoryDatabase();
		DatabaseVersion newDatabaseVersion = createDatabaseVersion();

		// Create chunks
		ChunkEntry chunkA1 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 2, 3, 4, 5, 7, 8, 9, 0 }), 12);
		ChunkEntry chunkA2 = new ChunkEntry(new ChunkChecksum(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 }), 34);
		ChunkEntry chunkA3 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1 }), 56);
		ChunkEntry chunkA4 = new ChunkEntry(new ChunkChecksum(new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2 }), 78);

		ChunkEntry chunkB1 = new ChunkEntry(new ChunkChecksum(new byte[] { 3, 3, 3, 3, 3, 3, 3, 3, 3 }), 910);
		ChunkEntry chunkB2 = new ChunkEntry(new ChunkChecksum(new byte[] { 4, 4, 4, 4, 4, 4, 4, 4, 4 }), 1112);

		newDatabaseVersion.addChunk(chunkA1);
		newDatabaseVersion.addChunk(chunkA2);
		newDatabaseVersion.addChunk(chunkA3);
		newDatabaseVersion.addChunk(chunkA4);
		newDatabaseVersion.addChunk(chunkB1);
		newDatabaseVersion.addChunk(chunkB2);

		// Distribute chunks to multichunks
		MultiChunkEntry multiChunkA = new MultiChunkEntry(new MultiChunkId(new byte[] { 6, 6, 6, 6, 6, 6, 6, 6, 6 }), 10);
		multiChunkA.addChunk(chunkA1.getChecksum());
		multiChunkA.addChunk(chunkA2.getChecksum());
		multiChunkA.addChunk(chunkA3.getChecksum());
		newDatabaseVersion.addMultiChunk(multiChunkA);

		MultiChunkEntry multiChunkB = new MultiChunkEntry(new MultiChunkId(new byte[] { 7, 7, 7, 7, 7, 7, 7, 7, 7 }), 11);
		multiChunkB.addChunk(chunkA4.getChecksum());
		multiChunkB.addChunk(chunkB1.getChecksum());
		multiChunkB.addChunk(chunkB2.getChecksum());
		newDatabaseVersion.addMultiChunk(multiChunkB);

		// Add database version
		newDatabase.addDatabaseVersion(newDatabaseVersion);

		// Write database to disk, read it again, and compare them
		MemoryDatabase loadedDatabase = writeReadAndCompareDatabase(newDatabase);

		// Check chunks
		assertEquals("Chunk not found in database loaded.", chunkA1, loadedDatabase.getChunk(chunkA1.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA2, loadedDatabase.getChunk(chunkA2.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA3, loadedDatabase.getChunk(chunkA3.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA4, loadedDatabase.getChunk(chunkA4.getChecksum()));

		assertEquals("Chunk not found in database loaded.", chunkB1, loadedDatabase.getChunk(chunkB1.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkB2, loadedDatabase.getChunk(chunkB2.getChecksum()));

		// Check multichunks
		MultiChunkEntry loadedMultiChunkA = loadedDatabase.getMultiChunk(multiChunkA.getId());
		MultiChunkEntry loadedMultiChunkB = loadedDatabase.getMultiChunk(multiChunkB.getId());

		assertEquals("Multichunk not found in database loaded.", multiChunkA, loadedMultiChunkA);
		assertEquals("Multichunk not found in database loaded.", multiChunkB, loadedMultiChunkB);

		assertArrayEquals("Chunks in multichunk expected to be different.", multiChunkA.getChunks().toArray(), loadedMultiChunkA.getChunks()
				.toArray());
		assertArrayEquals("Chunks in multichunk expected to be different.", multiChunkB.getChunks().toArray(), loadedMultiChunkB.getChunks()
				.toArray());
	}

	@Test
	public void testWriteAndReadChunksWithFileContents() throws IOException {
		// Prepare
		MemoryDatabase newDatabase = new MemoryDatabase();
		DatabaseVersion newDatabaseVersion = createDatabaseVersion();

		// Create chunks
		ChunkEntry chunkA1 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 2, 3, 4, 5, 7, 8, 9, 0 }), 12);
		ChunkEntry chunkA2 = new ChunkEntry(new ChunkChecksum(new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 }), 34);
		ChunkEntry chunkA3 = new ChunkEntry(new ChunkChecksum(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1 }), 56);
		ChunkEntry chunkA4 = new ChunkEntry(new ChunkChecksum(new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2 }), 78);

		ChunkEntry chunkB1 = new ChunkEntry(new ChunkChecksum(new byte[] { 3, 3, 3, 3, 3, 3, 3, 3, 3 }), 910);
		ChunkEntry chunkB2 = new ChunkEntry(new ChunkChecksum(new byte[] { 4, 4, 4, 4, 4, 4, 4, 4, 4 }), 1112);

		newDatabaseVersion.addChunk(chunkA1);
		newDatabaseVersion.addChunk(chunkA2);
		newDatabaseVersion.addChunk(chunkA3);
		newDatabaseVersion.addChunk(chunkA4);
		newDatabaseVersion.addChunk(chunkB1);
		newDatabaseVersion.addChunk(chunkB2);

		// Distribute chunks to file contents    	
		FileContent contentA = new FileContent();
		contentA.addChunk(chunkA1.getChecksum());
		contentA.addChunk(chunkA2.getChecksum());
		contentA.addChunk(chunkA3.getChecksum());
		contentA.addChunk(chunkA4.getChecksum());
		contentA.setChecksum(new FileChecksum(new byte[] { 5, 5, 5, 4, 4, 5, 5, 5, 5 }));
		newDatabaseVersion.addFileContent(contentA);

		FileContent contentB = new FileContent();
		contentB.addChunk(chunkB1.getChecksum());
		contentB.addChunk(chunkB2.getChecksum());
		contentB.setChecksum(new FileChecksum(new byte[] { 1, 1, 1, 3, 3, 5, 5, 5, 5 }));
		newDatabaseVersion.addFileContent(contentB);

		// Add database version
		newDatabase.addDatabaseVersion(newDatabaseVersion);

		// Write database to disk, read it again, and compare them
		MemoryDatabase loadedDatabase = writeReadAndCompareDatabase(newDatabase);

		// Check chunks
		assertEquals("Chunk not found in database loaded.", chunkA1, loadedDatabase.getChunk(chunkA1.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA2, loadedDatabase.getChunk(chunkA2.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA3, loadedDatabase.getChunk(chunkA3.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkA4, loadedDatabase.getChunk(chunkA4.getChecksum()));

		assertEquals("Chunk not found in database loaded.", chunkB1, loadedDatabase.getChunk(chunkB1.getChecksum()));
		assertEquals("Chunk not found in database loaded.", chunkB2, loadedDatabase.getChunk(chunkB2.getChecksum()));

		// Check file contents
		FileContent loadedContentA = loadedDatabase.getContent(contentA.getChecksum());
		FileContent loadedContentB = loadedDatabase.getContent(contentB.getChecksum());

		assertEquals("File content not found in database loaded.", contentA, loadedContentA);
		assertEquals("File content not found in database loaded.", contentB, loadedContentB);

		assertArrayEquals("Chunks in file content expected to be different.", contentA.getChunks().toArray(), loadedContentA.getChunks().toArray());
		assertArrayEquals("Chunks in file content expected to be different.", contentB.getChunks().toArray(), loadedContentB.getChunks().toArray());
	}

	@Test
	public void testWriteAndReadFileHistoryAndFileVersion() throws IOException {
		// Prepare
		MemoryDatabase newDatabase = new MemoryDatabase();
		DatabaseVersion newDatabaseVersion = createDatabaseVersion();

		// Create directories (no content!)

		// File A
		PartialFileHistory fileHistoryA = new PartialFileHistory(FileHistoryId.secureRandomFileId());
		newDatabaseVersion.addFileHistory(fileHistoryA);

		FileVersion versionA1 = new FileVersion();
		versionA1.setVersion(1L);
		versionA1.setType(FileType.FOLDER);
		versionA1.setPath("Pictures/2013/New York Folder");
		versionA1.setStatus(FileStatus.NEW);
		versionA1.setSize(0L);
		versionA1.setLastModified(new Date());
		fileHistoryA.addFileVersion(versionA1);

		FileVersion versionA2 = new FileVersion();
		versionA2.setVersion(2L);
		versionA2.setType(FileType.FOLDER);
		versionA2.setPath("Pictures/2013/New York");
		versionA2.setStatus(FileStatus.RENAMED);
		versionA2.setSize(0L);
		versionA2.setLastModified(new Date());
		fileHistoryA.addFileVersion(versionA2);

		// File B
		PartialFileHistory fileHistoryB = new PartialFileHistory(FileHistoryId.secureRandomFileId());
		newDatabaseVersion.addFileHistory(fileHistoryB);

		FileVersion versionB1 = new FileVersion();
		versionB1.setVersion(1L);
		versionB1.setType(FileType.FOLDER);
		versionB1.setPath("Pictures/2013/Egypt Folder");
		versionB1.setStatus(FileStatus.NEW);
		versionB1.setSize(0L);
		versionB1.setLastModified(new Date());
		fileHistoryB.addFileVersion(versionB1);

		FileVersion versionB2 = new FileVersion();
		versionB2.setVersion(2L);
		versionB2.setType(FileType.FOLDER);
		versionB2.setPath("Pictures/2013/Egypt");
		versionB2.setStatus(FileStatus.RENAMED);
		versionB2.setSize(0L);
		versionB2.setLastModified(new Date());
		fileHistoryB.addFileVersion(versionB2);

		// Add database version
		newDatabase.addDatabaseVersion(newDatabaseVersion);

		// Write database to disk, read it again, and compare them
		MemoryDatabase loadedDatabase = writeReadAndCompareDatabase(newDatabase);

		// File histories
		PartialFileHistory loadedFileHistoryA = loadedDatabase.getFileHistory(fileHistoryA.getFileHistoryId());
		PartialFileHistory loadedFileHistoryB = loadedDatabase.getFileHistory(fileHistoryB.getFileHistoryId());

		assertEquals("File history not found in database loaded.", fileHistoryA, loadedFileHistoryA);
		assertEquals("File history not found in database loaded.", fileHistoryB, loadedFileHistoryB);

		assertArrayEquals("File versions differ in loaded database.", fileHistoryA.getFileVersions().values().toArray(),
				loadedFileHistoryA.getFileVersions().values().toArray());

		assertArrayEquals("File versions differ in loaded database.", fileHistoryB.getFileVersions().values().toArray(),
				loadedFileHistoryB.getFileVersions().values().toArray());
	}

	@Test
	public void testWriteAndReadVectorClock() throws IOException {
		// Prepare
		MemoryDatabase newDatabase = new MemoryDatabase();
		DatabaseVersion newDatabaseVersion = createDatabaseVersion();

		// Create new vector clock
		VectorClock vc = new VectorClock();

		vc.setClock("UserA", 14234234L);
		vc.setClock("UserB", 9433431232432L);
		vc.setClock("UserC", 1926402374L);

		newDatabaseVersion.setVectorClock(vc);

		// Add database version
		newDatabase.addDatabaseVersion(newDatabaseVersion);

		// Write database to disk, read it again, and compare them
		MemoryDatabase loadedDatabase = writeReadAndCompareDatabase(newDatabase);

		// Check VC
		DatabaseVersion loadedDatabaseVersionSelectedByVectorClock = loadedDatabase.getDatabaseVersion(vc);
		DatabaseVersion loadedDatabaseVersionSelectedFirst = loadedDatabase.getDatabaseVersions().get(0);

		assertEquals("Vector clocks do not match (selected by vector clock)", vc, loadedDatabaseVersionSelectedByVectorClock.getVectorClock());
		assertEquals("Vector clocks do not match (selected first)", vc, loadedDatabaseVersionSelectedFirst.getVectorClock());
		assertEquals("Database versions do not match.", loadedDatabaseVersionSelectedByVectorClock, loadedDatabaseVersionSelectedFirst);
	}

	@Test
	public void testWriteAndReadMultipleDatabaseVersions() throws IOException {
		MemoryDatabase writtenDatabase = new MemoryDatabase();
		List<DatabaseVersion> writtenDatabaseVersions = new ArrayList<DatabaseVersion>();

		for (int i = 0; i < 10; i++) {
			DatabaseVersion basedOnDatabaseVersion = (i > 0) ? writtenDatabaseVersions.get(i - 1) : null;
			DatabaseVersion newDatabaseVersion = createDatabaseVersion(basedOnDatabaseVersion);

			// Some random chunks
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));

			// Add to database
			writtenDatabase.addDatabaseVersion(newDatabaseVersion);

			// Add to test array
			writtenDatabaseVersions.add(newDatabaseVersion);
		}

		// Write database to disk, read it again, and compare them
		writeReadAndCompareDatabase(writtenDatabase);
	}

	@Test
	public void testWritePartialDatabaseOneToFive() throws IOException {
		MemoryDatabase writtenDatabase = new MemoryDatabase();
		List<DatabaseVersion> writtenDatabaseVersions = new ArrayList<DatabaseVersion>();

		for (int i = 0; i < 10; i++) {
			DatabaseVersion basedOnDatabaseVersion = (i > 0) ? writtenDatabaseVersions.get(i - 1) : null;
			DatabaseVersion newDatabaseVersion = createDatabaseVersion(basedOnDatabaseVersion);

			// Some random chunks
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));
			newDatabaseVersion.addChunk(new ChunkEntry(new ChunkChecksum(TestFileUtil.createRandomArray(20)), 32 * 1024));

			// Add to database
			writtenDatabase.addDatabaseVersion(newDatabaseVersion);

			// Add to test array
			writtenDatabaseVersions.add(newDatabaseVersion);
		}

		// Write database to disk, read it again, and compare them
		File writtenDatabaseFile = new File(tempDir + "/db-" + Math.random() + "-" + Math.abs(new Random().nextInt(Integer.MAX_VALUE)));

		DatabaseXmlSerializer writeDAO = new DatabaseXmlSerializer();
		writeDAO.save(writtenDatabase.getDatabaseVersions(), writtenDatabaseFile);

		// Read again
		MemoryDatabase readDatabase = new MemoryDatabase();

		DatabaseXmlSerializer readDAO = new DatabaseXmlSerializer();
		readDAO.load(readDatabase, writtenDatabaseFile, null, null, DatabaseReadType.FULL);

		for (int i = 0; i < 10; i++) {
			DatabaseVersion writtenDatabaseVersion = writtenDatabaseVersions.get(i);
			DatabaseVersion readDatabaseVersion = readDatabase.getDatabaseVersion(writtenDatabaseVersion.getVectorClock());

			assertNotNull(readDatabaseVersion);
			assertDatabaseVersionEquals(writtenDatabaseVersion, readDatabaseVersion);
		}

		assertEquals(10, readDatabase.getDatabaseVersions().size());
	}

	private MemoryDatabase writeReadAndCompareDatabase(MemoryDatabase writtenDatabase) throws IOException {
		File writtenDatabaseFile = new File(tempDir + "/db-" + Math.random() + "-" + Math.abs(new Random().nextInt(Integer.MAX_VALUE)));
		TestDatabaseUtil.writeDatabaseFileToDisk(writtenDatabase, writtenDatabaseFile, null);
		MemoryDatabase readDatabase = TestDatabaseUtil.readDatabaseFileFromDisk(writtenDatabaseFile, null);

		TestAssertUtil.assertDatabaseEquals(writtenDatabase, readDatabase);

		return readDatabase;
	}
}
