/*******************************************************************************
 * Copyright (c) 2018-2019 Aston University, and others
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
 *
 * Contributors:
 *     Antonio Garcia-Dominguez - initial API and implementation
 *     Horacio Hoyos Rodriguez - Add proper Git support
 ******************************************************************************/
package org.eclipse.hawk.git;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.VcsChangeType;
import org.eclipse.hawk.core.VcsCommit;
import org.eclipse.hawk.core.VcsCommitItem;
import org.eclipse.hawk.core.VcsRepositoryDelta;
import org.eclipse.hawk.git.JGitRepository;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RmCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

public class JGitRepositoryTest {

	private static final String NULL_REV = "0000000000000000000000000000000000000000";

	private enum Version {
		v0("Initial Commit", "one.txt"), v1("Adds file two", "one.txt", "two.txt"),
		v2("Fixes file one", "one.txt", "two.txt"), v3("Fixes file two, deletes file one", "two.txt");

		private final String[] allFiles = new String[] { "one.txt", "two.txt" };
		private final String msg;
		private final Set<String> names;

		Version(String msg, String... files) {
			this.msg = msg;
			this.names = new HashSet<>(Arrays.asList(files));
		}

		public void commit(TemporaryFolder folder, Map<String, File> files) throws IOException {
			updateFiles(folder, files);
			commitChanges(folder, files);
		}

		private void commitChanges(TemporaryFolder folder, Map<String, File> files) {
			try (org.eclipse.jgit.api.Git repo = org.eclipse.jgit.api.Git.open(folder.getRoot())) {
				try {
					AddCommand add = repo.add();
					RmCommand rm = repo.rm();
					for (String name : allFiles) {
						if (names.contains(name)) {
							add.addFilepattern(name);
						} else {
							rm.addFilepattern(name);
						}
					}
					// Some commands can be empty
					try {
						add.call();
					} catch (NoFilepatternException e) {
						/* Ignore */ }
					try {
						rm.call();
					} catch (NoFilepatternException e) {
						/* Ignore */ }
					repo.commit().setMessage(msg).call();
				} catch (GitAPIException e) {
					throw new IllegalStateException("Failed to commit");
				}
			} catch (IOException e2) {
				throw new IllegalStateException("Failed to commit");
			}
		}

		private void updateFiles(TemporaryFolder folder, Map<String, File> files)
				throws IOException, FileNotFoundException {
			File tempFile;
			for (String name : allFiles) {
				tempFile = files.get(name);
				if (names.contains(name)) {
					if (tempFile != null) {
						// Replace it
						tempFile.delete();
					}
					tempFile = folder.newFile(name);
					copy(name, tempFile);
					files.put(name, tempFile);
				} else {
					if (tempFile != null) {
						tempFile.delete();
						files.remove(name);
					}
				}
			}
		}

		private void copy(String name, File tempFile) throws FileNotFoundException, IOException {
			InputStream in = JGitRepositoryTest.class.getResourceAsStream(String.format("/%s/%s", this.name(), name));
			try (OutputStream out = new FileOutputStream(tempFile)) {
				byte[] buf = new byte[1024];
				int length;
				while ((length = in.read(buf)) > 0) {
					out.write(buf, 0, length);
				}
			}
		}

	}

	@Rule
	public TemporaryFolder folder = new TemporaryFolder();

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	private final Map<String, File> files = new HashMap<>();

	private JGitRepository vcs;
	private IModelIndexer indexer;

	@Before
	public void setup() throws Exception {
		org.eclipse.jgit.api.Git.init().setDirectory(folder.getRoot()).call();

		indexer = mock(IModelIndexer.class);
		vcs = new JGitRepository();
		vcs.init(folder.getRoot().getCanonicalPath(), indexer);
		vcs.run();
	}

	@After
	public void clean() throws Exception {
		vcs.shutdown();
		files.clear();
	}

	@Test
	public void currentRevisionWithoutCommits() throws Exception {
		String currRev = vcs.getCurrentRevision();
		// Git repo with no commits has revision 0
		assertEquals(NULL_REV, currRev);
	}

	@Test
	public void currentRevisionOneCommit() throws Exception {
		Version.v0.commit(folder, files);
		String currRev = vcs.getCurrentRevision();
		// If there are commits there should be a hash
		assertNotEquals(NULL_REV, currRev);
	}

	@Test
	public void currentRevisionTwoCommits() throws Exception {
		Version.v0.commit(folder, files);
		String v0Rev = vcs.getCurrentRevision();
		System.out.println(v0Rev);
		assertNotEquals(NULL_REV, v0Rev);
		Version.v1.commit(folder, files);
		String v1Rev = vcs.getCurrentRevision();
		System.out.println(v1Rev);
		assertNotEquals(NULL_REV, v1Rev);
		assertNotEquals(v0Rev, v1Rev);
	}

	@Test
	public void firstRevisionIsConstant() throws Exception {
		Version.v0.commit(folder, files);
		String rev0 = vcs.getCurrentRevision();
		assertEquals(rev0, vcs.getFirstRevision());
		Version.v1.commit(folder, files);
		assertEquals(rev0, vcs.getFirstRevision());
		Version.v2.commit(folder, files);
		assertEquals(rev0, vcs.getFirstRevision());
		Version.v3.commit(folder, files);
		assertEquals(rev0, vcs.getFirstRevision());
	}

	@Test
	public void delta1ArgWithNullRevision() throws Exception {
		Version.v0.commit(folder, files);
		List<VcsCommitItem> delta = vcs.getDelta(null);

		assertEquals("1-arg delta with null starts from first revision", 1, delta.size());
		assertEquals(VcsChangeType.ADDED, delta.get(0).getChangeType());
	}

	@Test
	public void delta1ArgWithCurrent() throws Exception {
		Version.v0.commit(folder, files);
		Collection<VcsCommitItem> delta = vcs.getDelta(vcs.getCurrentRevision());
		assertEquals(0, delta.size());
	}

	@Test
	public void delta1ArgWithTwoCommits() throws Exception {
		Version.v0.commit(folder, files);
		String rev0 = vcs.getCurrentRevision();
		Version.v1.commit(folder, files);
		List<VcsCommitItem> delta = vcs.getDelta(rev0);
		assertEquals(1, delta.size());
		assertSame(vcs, delta.get(0).getCommit().getDelta().getManager());
		assertEquals(1L, delta.stream()
				.filter(i -> i.getChangeType().equals(VcsChangeType.ADDED) && i.getPath().equals("/two.txt")).count());
	}

	@Test
	public void delta1ArgWithUpdatedFile() throws Exception {
		Version.v0.commit(folder, files);
		Version.v1.commit(folder, files);
		String rev1 = vcs.getCurrentRevision();
		Version.v2.commit(folder, files);
		Collection<VcsCommitItem> delta = vcs.getDelta(rev1);
		assertEquals(1, delta.size());
		assertEquals(1L,
				delta.stream()
						.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/one.txt"))
						.count());
	}

	@Test
	public void delta1ArgTwoChanges() throws Exception {
		Version.v0.commit(folder, files);
		Version.v1.commit(folder, files);
		Version.v2.commit(folder, files);
		final String rev2 = vcs.getCurrentRevision();
		Version.v3.commit(folder, files);

		Collection<VcsCommitItem> delta = vcs.getDelta(rev2);
		assertEquals(2, delta.size());
		assertEquals(1L,
				delta.stream()
						.filter(i -> i.getChangeType().equals(VcsChangeType.DELETED) && i.getPath().equals("/one.txt"))
						.count());
		assertEquals(1L,
				delta.stream()
						.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/two.txt"))
						.count());
	}

	@Test
	public void delta2ArgAfterFirstOfTwo() throws Exception {
		Version.v0.commit(folder, files);
		Version.v1.commit(folder, files);
		VcsRepositoryDelta delta = vcs.getDelta(vcs.getFirstRevision(), vcs.getCurrentRevision());
		assertEquals(1, delta.size());

		Collection<VcsCommitItem> commits = delta.getCommits().iterator().next().getItems();
		assertEquals(1, commits.size());
		assertEquals(1L, commits.stream().filter(
			i -> i.getChangeType().equals(VcsChangeType.ADDED) && i.getPath().equals("/two.txt")
		).count());
	}

	@Test
	public void delta2ArgAfterFirstOfThree() throws Exception {
		Version.v0.commit(folder, files);
		Version.v1.commit(folder, files);
		Version.v2.commit(folder, files);
		Version.v3.commit(folder, files);

		VcsRepositoryDelta delta = vcs.getDelta(vcs.getFirstRevision(), vcs.getCurrentRevision());
		assertEquals(3, delta.size());
		Collection<VcsCommitItem> items = delta.getCompactedCommitItems();
		assertEquals(2, items.size());
		assertEquals(1L,
				items.stream()
					.filter(i -> i.getChangeType().equals(VcsChangeType.UPDATED) && i.getPath().equals("/two.txt"))
					.count());
		assertEquals(1L,
				items.stream()
					.filter(i -> i.getChangeType().equals(VcsChangeType.DELETED) && i.getPath().equals("/one.txt"))
					.count());
	}

	@Test
	public void importFile() throws Exception {
		Version.v0.commit(folder, files);
		String rev0 = vcs.getCurrentRevision();
		Version.v1.commit(folder, files);
		String rev1 = vcs.getCurrentRevision();
		Version.v2.commit(folder, files);
		String rev2 = vcs.getCurrentRevision();
		Version.v3.commit(folder, files);
		String rev3 = vcs.getCurrentRevision();
		
		File temp = vcs.importFile(rev0, "one.txt", folder.newFile());
		List<String> lines = Files.lines(temp.toPath()).collect(Collectors.toList());
		assertEquals(1, lines.size());
		assertEquals("one", lines.get(0));

		// Since JGitRepository produces paths starting with "/", importFile should be happy with them too
		temp = vcs.importFile(rev0, "/one.txt", folder.newFile());
		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
		assertEquals("/one.txt should work just as well as one.txt (number of lines)", 1, lines.size());
		assertEquals("/one.txt should work just as well as one.txt (line content)", "one", lines.get(0));
		
		assertNull("two.txt does not exist in first revision", vcs.importFile(rev0, "two.txt", folder.newFile()));
		
		temp = vcs.importFile(rev1, "two.txt", folder.newFile());
		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
		assertEquals(1, lines.size());
		assertEquals("two", lines.get(0));
		
		temp = vcs.importFile(rev2, "one.txt", folder.newFile());
		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
		assertEquals(2, lines.size());
		assertEquals("one", lines.get(0));
		assertEquals("one", lines.get(1));
		
		temp = vcs.importFile(rev3, "two.txt", folder.newFile());
		lines = Files.lines(temp.toPath()).collect(Collectors.toList());
		assertEquals(2, lines.size());
		assertEquals("two", lines.get(0));
		assertEquals("two", lines.get(1));
		
		assertNull("one.txt was deleted in fourth revision", vcs.importFile(rev3, "one.txt", folder.newFile()));
	}

	@Test
	public void gitHonorsCurrentBranch() throws Exception {
		Version.v0.commit(folder, files);
		String rev0 = vcs.getCurrentRevision();
		Version.v1.commit(folder, files);
		String rev1 = vcs.getCurrentRevision();

		final String newBranchName = "newbranch";
		try (Git repo = Git.open(folder.getRoot())) {
			repo.checkout().setName(newBranchName).setCreateBranch(true).call();
			Version.v2.commit(folder, files);
		}

		assertEquals(rev1, vcs.getCurrentRevision());
		VcsRepositoryDelta delta = vcs.getDelta(null, vcs.getCurrentRevision());
		assertEquals(2, delta.size());

		{
			Iterator<VcsCommit> itCommits = delta.getCommits().iterator();
			assertEquals(rev0, itCommits.next().getRevision());
			assertEquals(rev1, itCommits.next().getRevision());
		}

		delta = vcs.getDelta(rev0, vcs.getCurrentRevision());
		assertEquals(1, delta.size());
		assertEquals(rev1, delta.getCommits().iterator().next().getRevision());

		// Now try creating a VCS pointing at the new branch
		
		final JGitRepository repoNewBranch = new JGitRepository();
		final IModelIndexer indexer = mock(IModelIndexer.class);
		repoNewBranch.init(String.format(
				"%s?%s=%s",
				folder.getRoot().toURI().toString(),
				JGitRepository.BRANCH_QPARAM,
				newBranchName), indexer);
		repoNewBranch.run();

		assertEquals(3, repoNewBranch.getDelta(null,
			repoNewBranch.getCurrentRevision()).size());
	}
	
}
