package to.rtc.cli.migrate.git;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import to.rtc.cli.migrate.ChangeSet;
import to.rtc.cli.migrate.ChangeSet.WorkItem;
import to.rtc.cli.migrate.Tag;
import to.rtc.cli.migrate.util.Files;

/**
 * Tests the {@link GitMigrator} implementation.
 *
 * @author patrick.reinhart
 */
public class GitMigratorTest {
	@Rule
	public TemporaryFolder tempFolder = new TemporaryFolder();

	private Charset cs;
	private GitMigrator migrator;
	private Git git;
	private Properties props;
	private File basedir;

	@Before
	public void setUo() {
		cs = Charset.forName("UTF-8");
		props = new Properties();
		migrator = new GitMigrator(props);
		basedir = tempFolder.getRoot();
	}

	@After
	public void tearDown() {
		migrator.close();
		if (git != null) {
			git.close();
		}
	}

	@Test
	public void testClose() {
		migrator.close();
	}

	@Test
	public void testInit_noGitRepoAvailableNoGitIgnore() throws Exception {
		props.setProperty("user.email", "[email protected]");
		props.setProperty("user.name", "John Doe");

		assertFalse(new File(basedir, ".gitattributes").exists());

		migrator.initialize(props);
		migrator.init(basedir);

		checkGit("John Doe", "[email protected]", "Initial commit");
		checkExactLines(new File(basedir, ".gitignore"), GitMigrator.ROOT_IGNORED_ENTRIES);
		assertFalse(new File(basedir, ".gitattributes").exists());
	}

	@Test
	public void testInit_noGitRepoAvailableWithGitIgnore() throws Exception {
		Files.writeLines(new File(basedir, ".gitignore"), Arrays.asList("/.jazz5", "/bin/", "/.jazzShed"), cs, false);
		props.setProperty("global.gitignore.entries", "/projectX/WebContent/node_modules; *.ignored");

		migrator.init(basedir);

		checkGit("RTC 2 git", "[email protected]", "Initial commit");
		checkExactLines(new File(basedir, ".gitignore"), Arrays.asList("/.jazz5", "/bin/", "/.jazzShed", "/.metadata",
				"/projectX/WebContent/node_modules", "*.ignored"));
	}

	@Test
	public void testInit_Gitattributes() throws IOException {
		props.setProperty("gitattributes", "* text=auto");

		migrator.init(basedir);

		File gitattributes = new File(basedir, ".gitattributes");
		assertTrue(gitattributes.exists() && gitattributes.isFile());
		List<String> expectedLines = new ArrayList<String>();
		expectedLines.add("* text=auto");
		assertEquals(expectedLines, Files.readLines(gitattributes, cs));
	}

	@Test
	public void testInit_GitRepoAvailable() throws Exception {
		git = Git.init().setDirectory(basedir).call();

		migrator.init(basedir);

		checkGit("RTC 2 git", "[email protected]", "Initial commit");
		assertEquals(GitMigrator.ROOT_IGNORED_ENTRIES, Files.readLines(new File(basedir, ".gitignore"), cs));
	}

	@Test
	public void testInit_GitConfig() throws Exception {
		git = Git.init().setDirectory(basedir).call();
		StoredConfig config = git.getRepository().getConfig();

		migrator.init(basedir);

		config.load();
		assertFalse(config.getBoolean("core", null, "ignorecase", true));
		assertEquals(File.separatorChar == '/' ? "input" : "true", config.getString("core", null, "autocrlf"));
		assertEquals("simple", config.getString("push", null, "default"));
		assertFalse(config.getBoolean("http", null, "sslverify", true));
	}

	@Test
	public void testGetCommitMessage() {
		props.setProperty("commit.message.regex.1", "^B([0-9]+): (.+)$");
		props.setProperty("commit.message.replacement.1", "BUG-$1 $2");
		migrator.initialize(props);

		assertEquals("gugus BUG-1234 gaga", migrator.getCommitMessage("gugus", "B1234: gaga", ""));
	}

	@Test
	public void testGetCommitMessage_withCustomFormat() {
		props.setProperty("commit.message.format", "%1s%n%n%2s");
		migrator.init(basedir);
		String lf = System.getProperty("line.separator");

		assertEquals("gug%us" + lf + lf + "ga%ga", migrator.getCommitMessage("gug%us", "ga%ga", ""));
	}

	@Test
	public void testGetCommitMessage_withCustomFormat1() {
		props.setProperty("commit.message.format", "%1s%n%n%2s%n%n%3s");
		migrator.init(basedir);
		String lf = System.getProperty("line.separator");

		assertEquals("gug%us" + lf + lf + "ga%ga" + lf + lf + "gi%gl",
				migrator.getCommitMessage("gug%us", "ga%ga", "gi%gl"));
	}

	@Test
	public void testGetWorkItemNumbers_noWorkItems() {
		assertEquals("", migrator.getWorkItemNumbers(Collections.<WorkItem> emptyList()));
	}

	@Test
	public void testGetWorkItemNumbers_singleWorkItem() {
		migrator.init(basedir);
		List<WorkItem> items = new ArrayList<WorkItem>();
		items.add(TestWorkItem.INSTANCE1);
		assertEquals("4711", migrator.getWorkItemNumbers(items));
	}

	@Test
	public void testGetWorkItemNumbers_singleWorkItem_customFormat() {
		props.setProperty("rtc.workitem.number.format", "RTC-%s");
		migrator.init(basedir);

		List<WorkItem> items = new ArrayList<WorkItem>();
		items.add(TestWorkItem.INSTANCE1);
		assertEquals("RTC-4711", migrator.getWorkItemNumbers(items));
	}

	@Test
	public void testGetWorkItemNumbers_multipleWorkItems() {
		props.setProperty("rtc.workitem.number.format", "RTC-%s");
		migrator.init(basedir);

		List<WorkItem> items = new ArrayList<WorkItem>();
		items.add(TestWorkItem.INSTANCE1);
		items.add(TestWorkItem.INSTANCE2);
		assertEquals("RTC-4711 RTC-4712", migrator.getWorkItemNumbers(items));
	}

	@Test
	public void testCommitChanges() throws Exception {
		migrator.init(basedir);

		File testFile = new File(basedir, "somefile");
		Files.writeLines(testFile, Collections.singletonList("somevalue"), cs, false);

		migrator.commitChanges(TestChangeSet.INSTANCE);

		checkGit("Heiri Mueller", "[email protected]", "4711 the checkin comment");
		checkExactLines(testFile, Collections.singletonList("somevalue"));
	}

	@Test
	public void testCommitChanges_noWorkItem() throws Exception {
		migrator.init(basedir);

		File testFile = new File(basedir, "somefile");
		Files.writeLines(testFile, Collections.singletonList("somevalue"), cs, false);

		migrator.commitChanges(TestChangeSet.NO_WORKITEM_INSTANCE);

		checkGit("Heiri Mueller", "[email protected]", "the checkin comment");
		checkExactLines(testFile, Collections.singletonList("somevalue"));
	}

	@Test
	public void testGetGitattributeLines() throws Exception {
		props.setProperty("gitattributes", " # handle text files; * text=auto; *.sql text");
		migrator.initialize(props);

		List<String> lines = migrator.getGitattributeLines();
		assertNotNull(lines);
		assertEquals(3, lines.size());
		assertEquals("# handle text files", lines.get(0));
		assertEquals("* text=auto", lines.get(1));
		assertEquals("*.sql text", lines.get(2));
	}

	@Test
	public void testGetIgnoredFileExtensions() throws Exception {
		props.setProperty("ignore.file.extensions", ".zip; .jar; .exe; .dll");
		migrator.initialize(props);

		Set<String> ignoredExtensions = migrator.getIgnoredFileExtensions();
		HashSet<String> expected = new HashSet<String>(Arrays.asList(".zip", ".jar", ".exe", ".dll"));

		assertEquals(expected, ignoredExtensions);
	}

	@Test
	public void testAddMissing() {
		List<String> existing = new ArrayList<String>();
		existing.add("0");
		existing.add("3");
		List<String> adding = new ArrayList<String>();
		adding.add("0");
		adding.add("1");
		adding.add("2");
		adding.add("4");
		List<String> expectedLines = new ArrayList<String>();
		expectedLines.add("0");
		expectedLines.add("3");
		expectedLines.add("1");
		expectedLines.add("2");
		expectedLines.add("4");
		migrator.addMissing(existing, adding);
		assertEquals(expectedLines, existing);
	}

	@Test
	public void testAddUpdateGitignoreIfJazzignoreAddedOrChanged() throws Exception {
		migrator.init(basedir);
		File jazzignore = new File(basedir, ".jazzignore");

		Files.writeLines(jazzignore, Arrays.asList("core.ignore = {*.suo}", "core.ignore.recursive = {*.class}"), cs,
				false);

		migrator.commitChanges(TestChangeSet.INSTANCE);

		checkGit("Heiri Mueller", "[email protected]", "4711 the checkin comment");
		checkExactLines(new File(basedir, ".gitignore"), Arrays.asList("/*.suo", "*.class"));
	}

	@Test
	public void testAddUpdateGitignoreIfJazzignoreAddedOrChangedInSubdirectory() throws Exception {
		migrator.init(basedir);
		File subdir = tempFolder.newFolder("subdir");
		File jazzignore = new File(subdir, ".jazzignore");

		Files.writeLines(jazzignore, Arrays.asList("core.ignore = {*.suo}", "core.ignore.recursive = {*.class}"), cs,
				false);

		migrator.commitChanges(TestChangeSet.INSTANCE);

		checkGit("Heiri Mueller", "[email protected]", "4711 the checkin comment");
		checkExactLines(new File(subdir, ".gitignore"), Arrays.asList("/*.suo", "*.class"));
	}

	@Test
	public void testRemovedGitignoreIfJazzignoreRemoved() throws Exception {
		migrator.init(basedir);
		File jazzignore = new File(basedir, ".jazzignore");
		File gitignore = new File(basedir, ".gitignore");

		Files.writeLines(jazzignore, Arrays.asList("core.ignore = {*.suo}", "core.ignore.recursive = {*.class}"), cs,
				false);
		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertTrue(jazzignore.delete());

		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertFalse(gitignore.exists());
	}

	@Test
	public void testRestoreGitignoreIfJazzignoreNotRemoved() throws Exception {
		migrator.init(basedir);
		File jazzignore = new File(basedir, ".jazzignore");
		File gitignore = new File(basedir, ".gitignore");

		Files.writeLines(jazzignore, Arrays.asList("core.ignore = {*.suo}", "core.ignore.recursive = {*.class}"), cs,
				false);
		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertTrue(gitignore.delete());

		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertTrue(gitignore.exists());
	}

	@Test
	public void testRestoreGitignoreIfJazzignoreNotRemovedInSubdirectory() throws Exception {
		migrator.init(basedir);
		File subdir = tempFolder.newFolder("subdir");
		File jazzignore = new File(subdir, ".jazzignore");
		File gitignore = new File(subdir, ".gitignore");

		Files.writeLines(jazzignore, Arrays.asList("core.ignore = {*.suo}", "core.ignore.recursive = {*.class}"), cs,
				false);
		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertTrue(gitignore.delete());

		migrator.commitChanges(TestChangeSet.INSTANCE);

		assertTrue(gitignore.exists());
	}

	@Test
	public void testGlobalIgnoredFilesAddedToRootGitIgnore() throws Exception {
		props.setProperty("ignore.file.extensions", ".zip; .jar; .exe; .dLL");
		migrator.initialize(props);
		migrator.init(basedir);

		create(new File(basedir, "some.zip"));
		create(new File(basedir, "subdir/some.jar"));
		create(new File(basedir, "subdir/subsub/some.dLL"));

		migrator.commitChanges(TestChangeSet.INSTANCE);

		checkGit("Heiri Mueller", "[email protected]", "4711 the checkin comment");
		checkAllLines(new File(basedir, ".gitignore"), Arrays.asList("/.jazz5", "/.jazzShed", "/.metadata",
				"/subdir/subsub/some.dLL", "/some.zip", "/subdir/some.jar"));
	}

	@Test
	public void testCreateTagNameReplacesWhiteSpacesWithUnderscore() {
		String tagname = migrator.createTagName("tag with whitespaces");

		assertThat(tagname, is("tag_with_whitespaces"));
	}

	@Test
	public void testCreateTag() throws Exception {
		migrator.init(basedir);

		migrator.createTag(TestTag.INSTANCE);

		git = Git.open(basedir);
		List<Ref> tags = git.tagList().call();
		assertEquals(1, tags.size());
		Ref ref = tags.get(0);
		assertEquals("refs/tags/myTag", ref.getName());
	}

	@Test
	public void testGetExistingIgnoredFiles() throws Exception {
		migrator.init(basedir);
		Files.writeLines(new File(basedir, ".gitignore"), Arrays.asList("/.jazz5", "/some.zip", "/someother.zip",
				"/subdir/some.jar", "/subdir/subsub/some.dll", "/subdir/subsub/someother.dll"), cs, false);

		new File(basedir, ".jazz5").mkdir();
		create(new File(basedir, "some.zip"));
		create(new File(basedir, "subdir/some.jar"));
		create(new File(basedir, "subdir/subsub/some.dll"));

		SortedSet<String> expected = new TreeSet<String>(
				Arrays.asList("/some.zip", "/subdir/some.jar", "/subdir/subsub/some.dll"));

		SortedSet<String> stillExistingFiles = migrator.getExistingIgnoredFiles();

		assertEquals(expected, stillExistingFiles);
	}

	@Test
	public void testParseConfigValue() {
		assertEquals(1, migrator.parseConfigValue(null, 1));
		assertEquals(1, migrator.parseConfigValue("", 1));
		assertEquals(1, migrator.parseConfigValue(" ", 1));
		assertEquals(2, migrator.parseConfigValue("2", 1));
		assertEquals(1024, migrator.parseConfigValue("1k", 1));
		assertEquals(1024, migrator.parseConfigValue("1K", 1));
		assertEquals(1024, migrator.parseConfigValue("1 kB", 1));
		assertEquals(1024, migrator.parseConfigValue("1  KB", 1));
		assertEquals(2097152, migrator.parseConfigValue("2 m", 1));
		assertEquals(2097152, migrator.parseConfigValue("2  M", 1));
		assertEquals(2097152, migrator.parseConfigValue("2mb", 1));
		assertEquals(2097152, migrator.parseConfigValue("2MB", 1));
	}

	@Test
	public void testGetGitCacheConfig_defaults() throws Exception {
		// check values
		WindowCacheConfig cfg = migrator.getWindowCacheConfig();
		assertEquals(128, cfg.getPackedGitOpenFiles());
		assertEquals(10 * WindowCacheConfig.MB, cfg.getPackedGitLimit());
		assertEquals(8 * WindowCacheConfig.KB, cfg.getPackedGitWindowSize());
		assertFalse(cfg.isPackedGitMMAP());
		assertEquals(10 * WindowCacheConfig.MB, cfg.getDeltaBaseCacheLimit());
		assertEquals(50 * WindowCacheConfig.MB, cfg.getStreamFileThreshold());
	}

	@Test
	public void testGetGitCacheConfig() throws Exception {
		props.setProperty("packedgitopenfiles", "129");
		props.setProperty("packedgitlimit", "11m");
		props.setProperty("packedgitwindowsize", "9k");
		props.setProperty("packedgitmmap", "true");
		props.setProperty("deltabasecachelimit", "11m");
		props.setProperty("streamfilethreshold", "51m");
		migrator.initialize(props);
		// check values
		WindowCacheConfig cfg = migrator.getWindowCacheConfig();
		assertEquals(129, cfg.getPackedGitOpenFiles());
		assertEquals(11 * WindowCacheConfig.MB, cfg.getPackedGitLimit());
		assertEquals(9 * WindowCacheConfig.KB, cfg.getPackedGitWindowSize());
		assertTrue(cfg.isPackedGitMMAP());
		assertEquals(11 * WindowCacheConfig.MB, cfg.getDeltaBaseCacheLimit());
		assertEquals(51 * WindowCacheConfig.MB, cfg.getStreamFileThreshold());
	}

	@Test
	public void testGetMaxFileThresholdValue_maxOneFirthOfHeap() {
		assertEquals(10000, migrator.getMaxFileThresholdValue(12000, 40000));
	}

	@Test
	public void testGetMaxFileThresholdValue_lessOrEqulalMaxArraySize() {
		assertEquals(Integer.MAX_VALUE, migrator.getMaxFileThresholdValue(Integer.MAX_VALUE, Long.MAX_VALUE));
	}

	//
	// helper stuff
	//

	private void create(File file) throws Exception {
		file.getParentFile().mkdirs();
		file.createNewFile();
	}

	private void checkExactLines(File fileName, List<String> expected) throws Exception {
		assertEquals(expected, Files.readLines(fileName, cs));
	}

	private void checkAllLines(File fileName, List<String> expected) throws Exception {
		List<String> readLines = Files.readLines(fileName, cs);
		assertEquals(expected.size(), readLines.size());
		for (String line : expected) {
			assertTrue(readLines.contains(line));
		}
	}

	private void checkGit(String userName, String userEmail, String comment) throws Exception {
		git = Git.open(basedir);
		Status status = git.status().call();
		assertTrue(status.isClean());
		Iterator<RevCommit> log = git.log().call().iterator();
		RevCommit revCommit = log.next();
		assertEquals(userEmail, revCommit.getAuthorIdent().getEmailAddress());
		assertEquals(userName, revCommit.getAuthorIdent().getName());
		assertEquals(comment, revCommit.getFullMessage());
	}

	private enum TestChangeSet implements ChangeSet {
		INSTANCE, NO_WORKITEM_INSTANCE {
			@Override
			public List<WorkItem> getWorkItems() {
				return Collections.emptyList();
			}
		};

		@Override
		public String getComment() {
			return "the checkin comment";
		}

		@Override
		public String getCreatorName() {
			return "Heiri Mueller";
		}

		@Override
		public String getEmailAddress() {
			return "[email protected]";
		}

		@Override
		public long getCreationDate() {
			return 0;
		}

		@Override
		public List<WorkItem> getWorkItems() {
			List<WorkItem> items = new ArrayList<WorkItem>();
			items.add(TestWorkItem.INSTANCE1);
			return items;
		}
	}

	private enum TestWorkItem implements WorkItem {
		INSTANCE1 {
			@Override
			public long getNumber() {
				return 4711;
			}

			@Override
			public String getText() {
				return "The one and only";
			}
		},
		INSTANCE2 {
			@Override
			public long getNumber() {
				return 4712;
			}

			@Override
			public String getText() {
				return "The even more and only";
			}
		};
	}

	private enum TestTag implements Tag {
		INSTANCE;

		@Override
		public String getName() {
			return "myTag";
		}

		@Override
		public long getCreationDate() {
			return 0;
		}
	}
}