/*
 * Copyright (C) 2012, 2015 François Rey <eclipse.org_@_francois_._rey_._name>
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.pgm;

import static org.eclipse.jgit.lib.Constants.MASTER;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.junit.Assert.assertTrue;

import java.io.IOException;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;

public class StatusTest extends CLIRepositoryTestCase {

	@Test
	public void testPathOptionHelp() throws Exception {
		String[] result = execute("git status -h");
		assertTrue("Unexpected argument: " + result[1],
				result[1].endsWith("[-- path ...]"));
	}

	@Test
	public void testStatusDefault() throws Exception {
		executeTest("git status", false, true);
	}

	@Test
	public void testStatusU() throws Exception {
		executeTest("git status -u", false, true);
	}

	@Test
	public void testStatusUno() throws Exception {
		executeTest("git status -uno", false, false);
	}

	@Test
	public void testStatusUall() throws Exception {
		executeTest("git status -uall", false, true);
	}

	@Test
	public void testStatusUntrackedFiles() throws Exception {
		executeTest("git status --untracked-files", false, true);
	}

	@Test
	public void testStatusUntrackedFilesNo() throws Exception {
		executeTest("git status --untracked-files=no", false, false);
	}

	@Test
	public void testStatusUntrackedFilesAll() throws Exception {
		executeTest("git status --untracked-files=all", false, true);
	}

	@Test
	public void testStatusPorcelain() throws Exception {
		executeTest("git status --porcelain", true, true);
	}

	@Test
	public void testStatusPorcelainU() throws Exception {
		executeTest("git status --porcelain -u", true, true);
	}

	@Test
	public void testStatusPorcelainUno() throws Exception {
		executeTest("git status --porcelain -uno", true, false);
	}

	@Test
	public void testStatusPorcelainUall() throws Exception {
		executeTest("git status --porcelain -uall", true, true);
	}

	@Test
	public void testStatusPorcelainUntrackedFiles() throws Exception {
		executeTest("git status --porcelain --untracked-files", true, true);
	}

	@Test
	public void testStatusPorcelainUntrackedFilesNo() throws Exception {
		executeTest("git status --porcelain --untracked-files=no", true, false);
	}

	@Test
	public void testStatusPorcelainUntrackedFilesAll() throws Exception {
		executeTest("git status --porcelain --untracked-files=all", true, true);
	}

	/**
	 * Executes the test sequence.
	 *
	 * @param command
	 *            full git command and parameters to be used
	 * @param porcelain
	 *            indicates that porcelain format is expected in the output
	 * @param untrackedFiles
	 *            indicates that untracked files are expected in the output
	 *
	 * @throws Exception
	 *             if error during test execution
	 */
	private void executeTest(String command, boolean porcelain,
			boolean untrackedFiles) throws Exception {
		Git git = new Git(db);
		// Write all files
		writeAllFiles();
		// Test untracked
		assertUntrackedFiles(command, porcelain, untrackedFiles);
		// Add to index
		addFilesToIndex(git);
		// Test staged count
		assertStagedFiles(command, porcelain, untrackedFiles);
		// Commit
		makeInitialCommit(git);
		assertAfterInitialCommit(command, porcelain, untrackedFiles);
		// Make some changes and stage them
		makeSomeChangesAndStageThem(git);
		// Test staged/not-staged status
		assertStagedStatus(command, porcelain, untrackedFiles);
		// Create unmerged file
		createUnmergedFile(git);
		// Commit pending changes
		commitPendingChanges(git);
		assertUntracked(command, porcelain, untrackedFiles, "master");
		// Checkout new branch
		checkoutTestBranch(git);
		// Test branch status
		assertUntracked(command, porcelain, untrackedFiles, "test");
		// Commit change and checkout master again
		RevCommit testBranch = commitChangesInTestBranch(git);
		assertUntracked(command, porcelain, untrackedFiles, "test");
		checkoutMasterBranch(git);
		// Change the same file and commit
		changeUnmergedFileAndCommit(git);
		assertUntracked(command, porcelain, untrackedFiles, "master");
		// Merge test branch into master
		mergeTestBranchInMaster(git, testBranch);
		// Test unmerged status
		assertUntrackedAndUnmerged(command, porcelain, untrackedFiles, "master");
		// Test detached head
		detachHead(git);
		assertUntrackedAndUnmerged(command, porcelain, untrackedFiles, null);
	}

	private void writeAllFiles() throws IOException {
		writeTrashFile("tracked", "tracked");
		writeTrashFile("stagedNew", "stagedNew");
		writeTrashFile("stagedModified", "stagedModified");
		writeTrashFile("stagedDeleted", "stagedDeleted");
		writeTrashFile("trackedModified", "trackedModified");
		writeTrashFile("trackedDeleted", "trackedDeleted");
		writeTrashFile("untracked", "untracked");
	}

	private void addFilesToIndex(Git git) throws GitAPIException {
		git.add().addFilepattern("tracked").call();
		git.add().addFilepattern("stagedModified").call();
		git.add().addFilepattern("stagedDeleted").call();
		git.add().addFilepattern("trackedModified").call();
		git.add().addFilepattern("trackedDeleted").call();
	}

	private void makeInitialCommit(Git git) throws GitAPIException {
		git.commit().setMessage("initial commit").call();
	}

	private void makeSomeChangesAndStageThem(Git git) throws IOException,
			GitAPIException {
		writeTrashFile("stagedModified", "stagedModified modified");
		deleteTrashFile("stagedDeleted");
		writeTrashFile("trackedModified", "trackedModified modified");
		deleteTrashFile("trackedDeleted");
		git.add().addFilepattern("stagedModified").call();
		git.rm().addFilepattern("stagedDeleted").call();
		git.add().addFilepattern("stagedNew").call();
	}

	private void createUnmergedFile(Git git) throws IOException,
			GitAPIException {
		writeTrashFile("unmerged", "unmerged");
		git.add().addFilepattern("unmerged").call();
	}

	private void commitPendingChanges(Git git) throws GitAPIException {
		git.add().addFilepattern("trackedModified").call();
		git.rm().addFilepattern("trackedDeleted").call();
		git.commit().setMessage("commit before branching").call();
	}

	private void checkoutTestBranch(Git git) throws GitAPIException {
		git.checkout().setCreateBranch(true).setName("test").call();
	}

	private RevCommit commitChangesInTestBranch(Git git) throws IOException,
			GitAPIException {
		writeTrashFile("unmerged", "changed in test branch");
		git.add().addFilepattern("unmerged").call();
		return git.commit()
				.setMessage("changed unmerged in test branch").call();
	}

	private void checkoutMasterBranch(Git git) throws GitAPIException {
		git.checkout().setName("master").call();
	}

	private void changeUnmergedFileAndCommit(Git git) throws IOException,
			GitAPIException {
		writeTrashFile("unmerged", "changed in master branch");
		git.add().addFilepattern("unmerged").call();
		git.commit().setMessage("changed unmerged in master branch").call();
	}

	private void mergeTestBranchInMaster(Git git, RevCommit aCommit)
			throws GitAPIException {
		git.merge().include(aCommit.getId()).call();
	}

	private void detachHead(Git git) throws IOException, GitAPIException {
		String commitId = db.exactRef(R_HEADS + MASTER).getObjectId().name();
		git.checkout().setName(commitId).call();
	}

	private void assertUntrackedFiles(String command, boolean porcelain,
			boolean untrackedFiles) throws Exception {
		String[] output = new String[0];

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"?? stagedDeleted", //
						"?? stagedModified", //
						"?? stagedNew", //
						"?? tracked", //
						"?? trackedDeleted", //
						"?? trackedModified", //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						"On branch master", //
						"Untracked files:", //
						"",//
						"\tstagedDeleted", //
						"\tstagedModified", //
						"\tstagedNew", //
						"\ttracked", //
						"\ttrackedDeleted", //
						"\ttrackedModified", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"On branch master", //
						"" //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}

	private void assertStagedFiles(String command, boolean porcelain,
			boolean untrackedFiles) throws Exception {
		String[] output = new String[0];

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"A  stagedDeleted", //
						"A  stagedModified", //
						"A  tracked", //
						"A  trackedDeleted", //
						"A  trackedModified", //
						"?? stagedNew", //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"A  stagedDeleted", //
						"A  stagedModified", //
						"A  tracked", //
						"A  trackedDeleted", //
						"A  trackedModified", //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						"On branch master", //
						"Changes to be committed:", //
						"", //
						"\tnew file:   stagedDeleted", //
						"\tnew file:   stagedModified", //
						"\tnew file:   tracked", //
						"\tnew file:   trackedDeleted", //
						"\tnew file:   trackedModified", //
						"", //
						"Untracked files:", //
						"", //
						"\tstagedNew", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"On branch master", //
						"Changes to be committed:", //
						"", //
						"\tnew file:   stagedDeleted", //
						"\tnew file:   stagedModified", //
						"\tnew file:   tracked", //
						"\tnew file:   trackedDeleted", //
						"\tnew file:   trackedModified", //
						"" //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}

	private void assertAfterInitialCommit(String command, boolean porcelain,
			boolean untrackedFiles) throws Exception {
		String[] output = new String[0];

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"?? stagedNew", //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						"On branch master", //
						"Untracked files:", //
						"", //
						"\tstagedNew", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"On branch master", //
						"" //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}

	private void assertStagedStatus(String command, boolean porcelain,
			boolean untrackedFiles) throws Exception {
		String[] output = new String[0];

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"D  stagedDeleted", //
						"M  stagedModified", //
						"A  stagedNew", //
						" D trackedDeleted", //
						" M trackedModified", //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"D  stagedDeleted", //
						"M  stagedModified", //
						"A  stagedNew", //
						" D trackedDeleted", //
						" M trackedModified", //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						"On branch master", //
						"Changes to be committed:", //
						"", //
						"\tdeleted:    stagedDeleted", //
						"\tmodified:   stagedModified", //
						"\tnew file:   stagedNew", //
						"", //
						"Changes not staged for commit:", //
						"", //
						"\tdeleted:    trackedDeleted", //
						"\tmodified:   trackedModified", //
						"", //
						"Untracked files:", //
						"", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"On branch master", //
						"Changes to be committed:", //
						"", //
						"\tdeleted:    stagedDeleted", //
						"\tmodified:   stagedModified", //
						"\tnew file:   stagedNew", //
						"", //
						"Changes not staged for commit:", //
						"", //
						"\tdeleted:    trackedDeleted", //
						"\tmodified:   trackedModified", //
						"", //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}

	private void assertUntracked(String command,
			boolean porcelain,
			boolean untrackedFiles, String branch) throws Exception {
		String[] output = new String[0];
		String branchHeader = "On branch " + branch;

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						branchHeader, //
						"Untracked files:", //
						"", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						branchHeader, //
						"" //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}

	private void assertUntrackedAndUnmerged(String command, boolean porcelain,
			boolean untrackedFiles, String branch) throws Exception {
		String[] output = new String[0];
		String branchHeader = (branch == null) //
				? "Not currently on any branch." //
				: "On branch " + branch;

		if (porcelain) {
			if (untrackedFiles) {
				output = new String[] { //
						"UU unmerged", //
						"?? untracked", //
						"" //
				};
			} else {
				output = new String[] { //
						"UU unmerged", //
						"" //
				};
			}
		} else {
			if (untrackedFiles) {
				output = new String[] { //
						branchHeader, //
						"Unmerged paths:", //
						"", //
						"\tboth modified:      unmerged", //
						"", //
						"Untracked files:", //
						"", //
						"\tuntracked", //
						"" //
				};
			} else {
				output = new String[] { //
						branchHeader, //
						"Unmerged paths:", //
						"", //
						"\tboth modified:      unmerged", //
						"" //
				};
			}
		}

		assertArrayOfLinesEquals(output, execute(command));
	}
}
