/*
 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
 * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
 * 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.lib;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

import org.eclipse.jgit.errors.CheckoutConflictException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;

public abstract class ReadTreeTest extends RepositoryTestCase {
	protected Tree theHead;
	protected Tree theMerge;

	// Each of these rules are from the read-tree manpage
	// go there to see what they mean.
	// Rule 0 is left out for obvious reasons :)
	public void testRules1thru3_NoIndexEntry() throws IOException {
		Tree head = new Tree(db);
		FileTreeEntry headFile = head.addFile("foo");
		ObjectId objectId = ObjectId.fromString("ba78e065e2c261d4f7b8f42107588051e87e18e9");
		headFile.setId(objectId);
		Tree merge = new Tree(db);

		prescanTwoTrees(head, merge);

		assertTrue(getRemoved().contains("foo"));

		prescanTwoTrees(merge, head);

		assertEquals(objectId, getUpdated().get("foo"));

		ObjectId anotherId = ObjectId.fromString("ba78e065e2c261d4f7b8f42107588051e87e18ee");
		merge.addFile("foo").setId(anotherId);

		prescanTwoTrees(head, merge);

		assertEquals(anotherId, getUpdated().get("foo"));
	}

	void setupCase(HashMap<String, String> headEntries,
			HashMap<String, String> mergeEntries,
			HashMap<String, String> indexEntries) throws IOException {
		theHead = buildTree(headEntries);
		theMerge = buildTree(mergeEntries);
		buildIndex(indexEntries);
	}

	private void buildIndex(HashMap<String, String> indexEntries) throws IOException {
		GitIndex index = new GitIndex(db);

		if (indexEntries != null) {
			for (java.util.Map.Entry<String,String> e : indexEntries.entrySet()) {
				index.add(trash, writeTrashFile(e.getKey(), e.getValue())).forceRecheck();
			}
		}

		index.write();
		db.getIndex().read();
	}

	private Tree buildTree(HashMap<String, String> headEntries) throws IOException {
		Tree tree = new Tree(db);
		ObjectWriter ow = new ObjectWriter(db);
		if (headEntries == null)
			return tree;
		FileTreeEntry fileEntry;
		Tree parent;
		for (java.util.Map.Entry<String, String> e : headEntries.entrySet()) {
			fileEntry = tree.addFile(e.getKey());
			fileEntry.setId(genSha1(e.getValue()));
			parent = fileEntry.getParent();
			while (parent != null) {
				parent.setId(ow.writeTree(parent));
				parent = parent.getParent();
			}
		}

		return tree;
	}

	ObjectId genSha1(String data) {
		ObjectInserter w = db.newObjectInserter();
		try {
			ObjectId id = w.insert(Constants.OBJ_BLOB, data.getBytes());
			w.flush();
			return id;
		} catch (IOException e) {
			fail(e.toString());
		} finally {
			w.release();
		}
		return null;
	}

	protected void go() throws IllegalStateException, IOException {
		prescanTwoTrees(theHead, theMerge);
	}

	// for these rules, they all have clean yes/no options
	// but it doesn't matter if the entry is clean or not
	// so we can just ignore the state in the filesystem entirely
	public void testRules4thru13_IndexEntryNotInHead() throws IOException {
		// rules 4 and 5
		HashMap<String, String> idxMap;

		idxMap = new HashMap<String, String>();
		idxMap.put("foo", "foo");
		setupCase(null, null, idxMap);
		go();

		assertTrue(getUpdated().isEmpty());
		assertTrue(getRemoved().isEmpty());
		assertTrue(getConflicts().isEmpty());

		// rules 6 and 7
		idxMap = new HashMap<String, String>();
		idxMap.put("foo", "foo");
		setupCase(null, idxMap, idxMap);
		go();

		assertAllEmpty();

		// rules 8 and 9
		HashMap<String, String> mergeMap;
		mergeMap = new HashMap<String, String>();

		mergeMap.put("foo", "merge");
		setupCase(null, mergeMap, idxMap);
		go();

		assertTrue(getUpdated().isEmpty());
		assertTrue(getRemoved().isEmpty());
		assertTrue(getConflicts().contains("foo"));

		// rule 10

		HashMap<String, String> headMap = new HashMap<String, String>();
		headMap.put("foo", "foo");
		setupCase(headMap, null, idxMap);
		go();

		assertTrue(getRemoved().contains("foo"));
		assertTrue(getUpdated().isEmpty());
		assertTrue(getConflicts().isEmpty());

		// rule 11
		setupCase(headMap, null, idxMap);
		new File(trash, "foo").delete();
		writeTrashFile("foo", "bar");
		db.getIndex().getMembers()[0].forceRecheck();
		go();

		assertTrue(getRemoved().isEmpty());
		assertTrue(getUpdated().isEmpty());
		assertTrue(getConflicts().contains("foo"));

		// rule 12 & 13
		headMap.put("foo", "head");
		setupCase(headMap, null, idxMap);
		go();

		assertTrue(getRemoved().isEmpty());
		assertTrue(getUpdated().isEmpty());
		assertTrue(getConflicts().contains("foo"));

		// rules 14 & 15
		setupCase(headMap, headMap, idxMap);
		go();

		assertAllEmpty();

		// rules 16 & 17
		setupCase(headMap, mergeMap, idxMap); go();
		assertTrue(getConflicts().contains("foo"));

		// rules 18 & 19
		setupCase(headMap, idxMap, idxMap); go();
		assertAllEmpty();

		// rule 20
		setupCase(idxMap, mergeMap, idxMap); go();
		assertTrue(getUpdated().containsKey("foo"));

		// rules 21
		setupCase(idxMap, mergeMap, idxMap);
		new File(trash, "foo").delete();
		writeTrashFile("foo", "bar");
		db.getIndex().getMembers()[0].forceRecheck();
		go();
		assertTrue(getConflicts().contains("foo"));
	}

	private void assertAllEmpty() {
		assertTrue(getRemoved().isEmpty());
		assertTrue(getUpdated().isEmpty());
		assertTrue(getConflicts().isEmpty());
	}

	public void testDirectoryFileSimple() throws IOException {
		Tree treeDF = buildTree(mkmap("DF", "DF"));
		Tree treeDFDF = buildTree(mkmap("DF/DF", "DF/DF"));
		buildIndex(mkmap("DF", "DF"));

		prescanTwoTrees(treeDF, treeDFDF);

		assertTrue(getRemoved().contains("DF"));
		assertTrue(getUpdated().containsKey("DF/DF"));

		recursiveDelete(new File(trash, "DF"));
		buildIndex(mkmap("DF/DF", "DF/DF"));

		prescanTwoTrees(treeDFDF, treeDF);
		assertTrue(getRemoved().contains("DF/DF"));
		assertTrue(getUpdated().containsKey("DF"));
	}

	/*
	 * Directory/File Conflict cases:
	 * It's entirely possible that in practice a number of these may be equivalent
	 * to the cases described in git-read-tree.txt. As long as it does the right thing,
	 * that's all I care about. These are basically reverse-engineered from
	 * what git currently does. If there are tests for these in git, it's kind of
	 * hard to track them all down...
	 *
	 *     H        I       M     Clean     H==M     H==I    I==M         Result
	 *     ------------------------------------------------------------------
	 *1    D        D       F       Y         N       Y       N           Update
	 *2    D        D       F       N         N       Y       N           Conflict
	 *3    D        F       D                 Y       N       N           Update
	 *4    D        F       D                 N       N       N           Update
	 *5    D        F       F       Y         N       N       Y           Keep
	 *6    D        F       F       N         N       N       Y           Keep
	 *7    F        D       F       Y         Y       N       N           Update
	 *8    F        D       F       N         Y       N       N           Conflict
	 *9    F        D       F       Y         N       N       N           Update
	 *10   F        D       D                 N       N       Y           Keep
	 *11   F		D		D				  N		  N		  N			  Conflict
	 *12   F		F		D		Y		  N		  Y		  N			  Update
	 *13   F		F		D		N		  N		  Y		  N			  Conflict
	 *14   F		F		D				  N		  N		  N			  Conflict
	 *15   0		F		D				  N		  N		  N			  Conflict
	 *16   0		D		F		Y		  N		  N		  N			  Update
	 *17   0		D		F		 		  N		  N		  N			  Conflict
	 *18   F        0       D    										  Update
	 *19   D	    0       F											  Update
	 */

	public void testDirectoryFileConflicts_1() throws Exception {
		// 1
		doit(mk("DF/DF"), mk("DF"), mk("DF/DF"));
		assertNoConflicts();
		assertUpdated("DF");
		assertRemoved("DF/DF");
	}

	public void testDirectoryFileConflicts_2() throws Exception {
		// 2
		setupCase(mk("DF/DF"), mk("DF"), mk("DF/DF"));
		writeTrashFile("DF/DF", "different");
		go();
		assertConflict("DF/DF");

	}

	public void testDirectoryFileConflicts_3() throws Exception {
		// 3 - the first to break!
		doit(mk("DF/DF"), mk("DF/DF"), mk("DF"));
		assertUpdated("DF/DF");
		assertRemoved("DF");
	}

	public void testDirectoryFileConflicts_4() throws Exception {
		// 4 (basically same as 3, just with H and M different)
		doit(mk("DF/DF"), mkmap("DF/DF", "foo"), mk("DF"));
		assertUpdated("DF/DF");
		assertRemoved("DF");

	}

	public void testDirectoryFileConflicts_5() throws Exception {
		// 5
		doit(mk("DF/DF"), mk("DF"), mk("DF"));
		assertRemoved("DF/DF");

	}

	public void testDirectoryFileConflicts_6() throws Exception {
		// 6
		setupCase(mk("DF/DF"), mk("DF"), mk("DF"));
		writeTrashFile("DF", "different");
		go();
		assertRemoved("DF/DF");
	}

	public void testDirectoryFileConflicts_7() throws Exception {
		// 7
		doit(mk("DF"), mk("DF"), mk("DF/DF"));
		assertUpdated("DF");
		assertRemoved("DF/DF");

		cleanUpDF();
		setupCase(mk("DF/DF"), mk("DF/DF"), mk("DF/DF/DF/DF/DF"));
		go();
		assertRemoved("DF/DF/DF/DF/DF");
		assertUpdated("DF/DF");

		cleanUpDF();
		setupCase(mk("DF/DF"), mk("DF/DF"), mk("DF/DF/DF/DF/DF"));
		writeTrashFile("DF/DF/DF/DF/DF", "diff");
		go();
		assertConflict("DF/DF/DF/DF/DF");
		assertUpdated("DF/DF");

	}

	// 8 ?

	public void testDirectoryFileConflicts_9() throws Exception {
		// 9
		doit(mk("DF"), mkmap("DF", "QP"), mk("DF/DF"));
		assertRemoved("DF/DF");
		assertUpdated("DF");
	}

	public void testDirectoryFileConflicts_10() throws Exception {
		// 10
		cleanUpDF();
		doit(mk("DF"), mk("DF/DF"), mk("DF/DF"));
		assertNoConflicts();

	}

	public void testDirectoryFileConflicts_11() throws Exception {
		// 11
		doit(mk("DF"), mk("DF/DF"), mkmap("DF/DF", "asdf"));
		assertConflict("DF/DF");
	}

	public void testDirectoryFileConflicts_12() throws Exception {
		// 12
		cleanUpDF();
		doit(mk("DF"), mk("DF/DF"), mk("DF"));
		assertRemoved("DF");
		assertUpdated("DF/DF");
	}

	public void testDirectoryFileConflicts_13() throws Exception {
		// 13
		cleanUpDF();
		setupCase(mk("DF"), mk("DF/DF"), mk("DF"));
		writeTrashFile("DF", "asdfsdf");
		go();
		assertConflict("DF");
		assertUpdated("DF/DF");
	}

	public void testDirectoryFileConflicts_14() throws Exception {
		// 14
		cleanUpDF();
		doit(mk("DF"), mk("DF/DF"), mkmap("DF", "Foo"));
		assertConflict("DF");
		assertUpdated("DF/DF");
	}

	public void testDirectoryFileConflicts_15() throws Exception {
		// 15
		doit(mkmap(), mk("DF/DF"), mk("DF"));
		assertRemoved("DF");
		assertUpdated("DF/DF");
	}

	public void testDirectoryFileConflicts_15b() throws Exception {
		// 15, take 2, just to check multi-leveled
		doit(mkmap(), mk("DF/DF/DF/DF"), mk("DF"));
		assertRemoved("DF");
		assertUpdated("DF/DF/DF/DF");
	}

	public void testDirectoryFileConflicts_16() throws Exception {
		// 16
		cleanUpDF();
		doit(mkmap(), mk("DF"), mk("DF/DF/DF"));
		assertRemoved("DF/DF/DF");
		assertUpdated("DF");
	}

	public void testDirectoryFileConflicts_17() throws Exception {
		// 17
		cleanUpDF();
		setupCase(mkmap(), mk("DF"), mk("DF/DF/DF"));
		writeTrashFile("DF/DF/DF", "asdf");
		go();
		assertConflict("DF/DF/DF");
		assertUpdated("DF");
	}

	public void testDirectoryFileConflicts_18() throws Exception {
		// 18
		cleanUpDF();
		doit(mk("DF/DF"), mk("DF/DF/DF/DF"), null);
		assertRemoved("DF/DF");
		assertUpdated("DF/DF/DF/DF");
	}

	public void testDirectoryFileConflicts_19() throws Exception {
		// 19
		cleanUpDF();
		doit(mk("DF/DF/DF/DF"), mk("DF/DF/DF"), null);
		assertRemoved("DF/DF/DF/DF");
		assertUpdated("DF/DF/DF");
	}

	private void cleanUpDF() throws Exception {
		tearDown();
		setUp();
		recursiveDelete(new File(trash, "DF"));
	}

	private void assertConflict(String s) {
		assertTrue(getConflicts().contains(s));
	}

	private void assertUpdated(String s) {
		assertTrue(getUpdated().containsKey(s));
	}

	private void assertRemoved(String s) {
		assertTrue(getRemoved().contains(s));
	}

	private void assertNoConflicts() {
		assertTrue(getConflicts().isEmpty());
	}

	private void doit(HashMap<String, String> h, HashMap<String, String> m,
			HashMap<String, String> i) throws IOException {
		setupCase(h, m, i);
		go();
	}

	protected static HashMap<String, String> mk(String a) {
		return mkmap(a, a);
	}

	protected static HashMap<String, String> mkmap(String... args) {
		if ((args.length % 2) > 0)
			throw new IllegalArgumentException("needs to be pairs");

		HashMap<String, String> map = new HashMap<String, String>();
		for (int i = 0; i < args.length; i += 2) {
			map.put(args[i], args[i+1]);
		}

		return map;
	}

	public void testUntrackedConflicts() throws IOException {
		setupCase(null, mk("foo"), null);
		writeTrashFile("foo", "foo");
		go();

		assertConflict("foo");

		recursiveDelete(new File(trash, "foo"));
		setupCase(null, mk("foo"), null);
		writeTrashFile("foo/bar/baz", "");
		writeTrashFile("foo/blahblah", "");
		go();

		assertConflict("foo/bar/baz");
		assertConflict("foo/blahblah");

		recursiveDelete(new File(trash, "foo"));

		setupCase(mkmap("foo/bar", "", "foo/baz", ""),
				mk("foo"), mkmap("foo/bar", "", "foo/baz", ""));
		assertTrue(new File(trash, "foo/bar").exists());
		go();

		assertNoConflicts();
	}

	public void testCloseNameConflictsX0() throws IOException {
		setupCase(mkmap("a/a", "a/a-c"), mkmap("a/a","a/a", "b.b/b.b","b.b/b.bs"), mkmap("a/a", "a/a-c") );
		checkout();
		assertIndex(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs"));
		assertWorkDir(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs"));
		go();
		assertIndex(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs"));
		assertWorkDir(mkmap("a/a", "a/a", "b.b/b.b", "b.b/b.bs"));
		assertNoConflicts();
	}

	public void testCloseNameConflicts1() throws IOException {
		setupCase(mkmap("a/a", "a/a-c"), mkmap("a/a","a/a", "a.a/a.a","a.a/a.a"), mkmap("a/a", "a/a-c") );
		checkout();
		assertIndex(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a"));
		assertWorkDir(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a"));
		go();
		assertIndex(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a"));
		assertWorkDir(mkmap("a/a", "a/a", "a.a/a.a", "a.a/a.a"));
		assertNoConflicts();
	}

	public void testCheckoutOutChanges() throws IOException {
		setupCase(mk("foo"), mk("foo/bar"), mk("foo"));
		checkout();
		assertIndex(mk("foo/bar"));
		assertWorkDir(mk("foo/bar"));

		assertFalse(new File(trash, "foo").isFile());
		assertTrue(new File(trash, "foo/bar").isFile());
		recursiveDelete(new File(trash, "foo"));

		assertWorkDir(mkmap());

		setupCase(mk("foo/bar"), mk("foo"), mk("foo/bar"));
		checkout();

		assertIndex(mk("foo"));
		assertWorkDir(mk("foo"));

		assertFalse(new File(trash, "foo/bar").isFile());
		assertTrue(new File(trash, "foo").isFile());

		setupCase(mk("foo"), mkmap("foo", "qux"), mkmap("foo", "bar"));

		assertIndex(mkmap("foo", "bar"));
		assertWorkDir(mkmap("foo", "bar"));

		try {
			checkout();
			fail("did not throw exception");
		} catch (CheckoutConflictException e) {
			assertIndex(mkmap("foo", "bar"));
			assertWorkDir(mkmap("foo", "bar"));
		}
	}

	public void testCheckoutUncachedChanges() throws IOException {
		setupCase(mk("foo"), mk("foo"), mk("foo"));
		writeTrashFile("foo", "otherData");
		checkout();
		assertIndex(mk("foo"));
		assertWorkDir(mkmap("foo", "otherData"));
		assertTrue(new File(trash, "foo").isFile());
	}

	/**
	 * The interface these tests need from a class implementing a checkout
	 */
	interface Checkout {
		HashMap<String, ObjectId> updated();
		ArrayList<String> conflicts();
		ArrayList<String> removed();
		void prescanTwoTrees() throws IOException;
		void checkout() throws IOException;
	}

	public void assertWorkDir(HashMap<String, String> i)
			throws CorruptObjectException, IOException {
		TreeWalk walk = new TreeWalk(db);
		walk.reset();
		walk.setRecursive(true);
		walk.addTree(new FileTreeIterator(db));
		String expectedValue;
		String path;
		int nrFiles = 0;
		FileTreeIterator ft;
		while (walk.next()) {
			ft = walk.getTree(0, FileTreeIterator.class);
			path = ft.getEntryPathString();
			expectedValue = i.get(path);
			assertNotNull("found unexpected file for path "
					+ path + " in workdir", expectedValue);
			File file = new File(db.getWorkTree(), path);
			assertTrue(file.exists());
			if (file.isFile()) {
				FileInputStream is = new FileInputStream(file);
				byte[] buffer = new byte[(int) file.length()];
				int offset = 0;
				int numRead = 0;
				while (offset < buffer.length
						&& (numRead = is.read(buffer, offset, buffer.length
								- offset)) >= 0) {
					offset += numRead;
				}
				is.close();
				assertTrue("unexpected content for path " + path
						+ " in workDir. Expected: <" + expectedValue + ">",
						Arrays.equals(buffer, i.get(path).getBytes()));
				nrFiles++;
			}
		}
		assertEquals("WorkDir has not the right size.", i.size(), nrFiles);
	}


	public void assertIndex(HashMap<String, String> i)
			throws CorruptObjectException, IOException {
		String expectedValue;
		String path;
		GitIndex theIndex=db.getIndex();
		assertEquals("Index has not the right size.", i.size(),
				theIndex.getMembers().length);
		for (int j = 0; j < theIndex.getMembers().length; j++) {
			path = theIndex.getMembers()[j].getName();
			expectedValue = i.get(path);
			assertNotNull("found unexpected entry for path " + path
					+ " in index", expectedValue);
			assertTrue("unexpected content for path " + path
					+ " in index. Expected: <" + expectedValue + ">",
					Arrays.equals(
							db.open(theIndex.getMembers()[j].getObjectId())
									.getCachedBytes(), i.get(path).getBytes()));
		}
	}

	public abstract void prescanTwoTrees(Tree head, Tree merge) throws IllegalStateException, IOException;
	public abstract void checkout() throws IOException;
	public abstract ArrayList<String> getRemoved();
	public abstract HashMap<String, ObjectId> getUpdated();
	public abstract ArrayList<String> getConflicts();
}
