/*******************************************************************************
 * Copyright (C) 2010, 2012 Mathias Kinzler <mathias.kinzler@sap.com> and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.eclipse.egit.core.test.op;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.ILogListener;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.op.AddToIndexOperation;
import org.eclipse.egit.core.op.BranchOperation;
import org.eclipse.egit.core.op.CloneOperation;
import org.eclipse.egit.core.op.CommitOperation;
import org.eclipse.egit.core.op.PushOperation;
import org.eclipse.egit.core.op.PushOperationResult;
import org.eclipse.egit.core.op.PushOperationSpecification;
import org.eclipse.egit.core.test.DualRepositoryTestCase;
import org.eclipse.egit.core.test.TestRepository;
import org.eclipse.egit.core.test.TestUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.URIish;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class PushOperationTest extends DualRepositoryTestCase {


	private static final String INVALID_URI = "invalid-uri";

	File workdir;

	File workdir2;

	String projectName = "PushTest";

	/**
	 * Set up repository1 with branch "master", create some project and commit
	 * it; then clone into repository2; finally create a branch "test" on top of
	 * "master" in repository2
	 *
	 * @throws Exception
	 */
	@Before
	public void setUp() throws Exception {

		workdir = testUtils.createTempDir("Repository1");
		workdir2 = testUtils.createTempDir("Repository2");

		repository1 = new TestRepository(new File(workdir, Constants.DOT_GIT));

		// now we create a project in repo1
		IProject project = testUtils.createProjectInLocalFileSystem(workdir,
				projectName);
		testUtils.addFileToProject(project, "folder1/file1.txt", "Hello world");

		repository1.connect(project);
		repository1.trackAllFiles(project);
		repository1.commit("Initial commit");

		// let's get rid of the project
		project.delete(false, false, null);

		// let's clone repository1 to repository2
		URIish uri = repository1.getUri();
		CloneOperation clop = new CloneOperation(uri, true, null, workdir2,
				"refs/heads/master", "origin", 0);
		clop.run(null);

		Repository repo2 = Activator.getDefault().getRepositoryCache().lookupRepository(new File(workdir2,
				Constants.DOT_GIT));
		repository2 = new TestRepository(repo2);
		// we push to branch "test" of repository2
		RefUpdate createBranch = repository2.getRepository().updateRef(
				"refs/heads/test");
		createBranch.setNewObjectId(repository2.getRepository().resolve(
				"refs/heads/master"));
		createBranch.update();
	}

	@After
	public void tearDown() throws Exception {
		repository1.dispose();
		repository2.dispose();
		repository1 = null;
		repository2 = null;
		testUtils.deleteTempDirs();
	}

	/**
	 * Push from repository1 "master" into "test" of repository2.
	 *
	 * @throws Exception
	 */
	@Test
	public void testPush() throws Exception {

		// push from repository1 to repository2
		PushOperation pop = createPushOperation();
		pop.run(new NullProgressMonitor());
		assertEquals(Status.UP_TO_DATE, getStatus(pop.getOperationResult()));

		// let's add a new file to the project shared with repository1
		IProject proj = importProject(repository1, projectName);
		ArrayList<IFile> files = new ArrayList<IFile>();
		IFile newFile = testUtils.addFileToProject(proj, "folder2/file2.txt",
				"New file");
		files.add(newFile);
		IFile[] fileArr = files.toArray(new IFile[files.size()]);

		AddToIndexOperation trop = new AddToIndexOperation(files);
		trop.execute(null);
		CommitOperation cop = new CommitOperation(fileArr, files, TestUtils.AUTHOR,
				TestUtils.COMMITTER, "Added file");
		cop.execute(null);

		proj.delete(false, false, null);

		pop = createPushOperation();
		pop.run(null);
		assertEquals(Status.OK, getStatus(pop.getOperationResult()));

		try {
			// assert that we can not run this again
			pop.run(null);
			fail("Expected Exception not thrown");
		} catch (IllegalStateException e) {
			// expected
		}

		pop = createPushOperation();
		pop.run(null);
		assertEquals(Status.UP_TO_DATE, getStatus(pop.getOperationResult()));

		String newFilePath = newFile.getFullPath().toOSString();

		File testFile = new File(workdir2, newFilePath);
		assertFalse(testFile.exists());
		testFile = new File(workdir, newFilePath);
		assertTrue(testFile.exists());

		// check out test and verify the file is there
		BranchOperation bop = new BranchOperation(repository2.getRepository(),
				"refs/heads/test");
		bop.execute(null);
		testFile = new File(workdir2, newFilePath);
		assertTrue(testFile.exists());
	}

	/**
	 * An invalid URI should yield an operation result with an error message
	 * and the exception should be logged
	 *
	 * @throws Exception
	 */
	@Test
	public void testInvalidUriDuringPush() throws Exception {
		ILog log = Activator.getDefault().getLog();
		LogListener listener = new LogListener();
		log.addLogListener(listener);

		PushOperation pop = createInvalidPushOperation();
		pop.run(new NullProgressMonitor());
		PushOperationResult result = pop.getOperationResult();
		String errorMessage = result.getErrorMessage(new URIish(INVALID_URI));
		assertNotNull(errorMessage);
		assertTrue(errorMessage.contains(INVALID_URI));

		assertTrue(listener.loggedSomething());
		assertTrue(listener.loggedException());

	}

	private PushOperation createInvalidPushOperation() throws Exception {
		// set up push with invalid URI to provoke an exception
		PushOperationSpecification spec = new PushOperationSpecification();
		// the remote is invalid
		URIish remote = new URIish(INVALID_URI);
		// update master upon master
		Repository local = repository1.getRepository();
		RemoteRefUpdate update = new RemoteRefUpdate(local, "HEAD", "refs/heads/test",
				false, null, null);
		spec.addURIRefUpdates(remote, Collections.singletonList(update));
		// now we can construct the push operation
		PushOperation pop = new PushOperation(local, spec, false, 0);
		return pop;
	}

	private static final class LogListener implements ILogListener {
		private boolean loggedSomething = false;
		private boolean loggedException = false;

		public void logging(IStatus status, String plugin) {
			loggedSomething = true;
			loggedException = status.getException() != null;
		}

		public boolean loggedSomething() {
			return loggedSomething;
		}

		public boolean loggedException() {
			return loggedException;
		}

}

	/**
	 * We should get an {@link IllegalStateException} if we run
	 * getOperationResult before run()
	 *
	 * @throws Exception
	 */
	@Test
	public void testIllegalStateExceptionOnGetResultWithoutRun()
			throws Exception {
		// push from repository1 to repository2
		PushOperation pop = createPushOperation();
		try {
			pop.getOperationResult();
			fail("Expected Exception not thrown");
		} catch (IllegalStateException e) {
			// expected
		}
	}

	/**
	 * We should get an {@link IllegalStateException} if the spec was re-used
	 *
	 * @throws Exception
	 */
	@Test
	public void testPushWithReusedSpec() throws Exception {

		PushOperationSpecification spec = new PushOperationSpecification();
		// the remote is repo2
		URIish remote = repository2.getUri();
		// update master upon master
		List<RemoteRefUpdate> refUpdates = new ArrayList<RemoteRefUpdate>();
		RemoteRefUpdate update = new RemoteRefUpdate(repository1
				.getRepository(), "HEAD", "refs/heads/test", false, null, null);
		refUpdates.add(update);
		spec.addURIRefUpdates(remote, refUpdates);

		PushOperation pop = new PushOperation(repository1.getRepository(),
				spec, false, 0);
		pop.run(null);

		pop = new PushOperation(repository1.getRepository(), spec, false, 0);
		try {
			pop.run(null);
			fail("Expected Exception not thrown");
		} catch (IllegalStateException e) {
			// expected
		}
	}

	@Test
	public void testUpdateTrackingBranchIfSpecifiedInRemoteRefUpdate() throws Exception {
		// Commit on repository 2
		IProject project = importProject(repository2, projectName);
		RevCommit commit = repository2.addAndCommit(project, new File(workdir2, "test.txt"), "Commit in repository 2");
		project.delete(false, false, null);

		// We want to push from repository 2 to 1 (because repository 2 already
		// has tracking set up)
		URIish remote = repository1.getUri();
		String trackingRef = "refs/remotes/origin/master";
		RemoteRefUpdate update = new RemoteRefUpdate(
				repository2.getRepository(), "HEAD", "refs/heads/master", false,
				trackingRef, null);
		PushOperationSpecification spec = new PushOperationSpecification();
		spec.addURIRefUpdates(remote, Arrays.asList(update));

		PushOperation push = new PushOperation(repository2.getRepository(),
				spec, false, 0);
		push.run(null);

		PushOperationResult result = push.getOperationResult();
		PushResult pushResult = result.getPushResult(remote);
		assertNotNull("Expected result to have tracking ref update", pushResult.getTrackingRefUpdate(trackingRef));

		ObjectId trackingId = repository2.getRepository().resolve(trackingRef);
		assertEquals("Expected tracking branch to be updated", commit.getId(), trackingId);
	}

	private Status getStatus(PushOperationResult operationResult) {
		URIish uri = operationResult.getURIs().iterator().next();
		return operationResult.getPushResult(uri).getRemoteUpdates().iterator()
				.next().getStatus();
	}

	private PushOperation createPushOperation() throws Exception {
		// set up push from repository1 to repository2
		// we can not re-use the RemoteRefUpdate!!!
		PushOperationSpecification spec = new PushOperationSpecification();
		// the remote is repo2
		URIish remote = new URIish("file:///"
				+ repository2.getRepository().getDirectory().toString());
		// update master upon master
		List<RemoteRefUpdate> refUpdates = new ArrayList<RemoteRefUpdate>();
		RemoteRefUpdate update = new RemoteRefUpdate(repository1
				.getRepository(), "HEAD", "refs/heads/test", false, null, null);
		refUpdates.add(update);
		spec.addURIRefUpdates(remote, refUpdates);
		// now we can construct the push operation
		PushOperation pop = new PushOperation(repository1.getRepository(),
				spec, false, 0);
		return pop;
	}

}
