/*
 * Copyright (C) 2015, Google Inc. and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.transport;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.ObjectId.zeroId;
import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
import static org.eclipse.jgit.lib.RefUpdate.Result.LOCK_FAILURE;
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
import static org.eclipse.jgit.lib.RefUpdate.Result.NO_CHANGE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;

public class PushCertificateStoreTest {
	private static final ObjectId ID1 =
		ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");

	private static final ObjectId ID2 =
		ObjectId.fromString("badc0ffebadc0ffebadc0ffebadc0ffebadc0ffe");

	private static PushCertificate newCert(String... updateLines) {
		StringBuilder cert = new StringBuilder(
				"certificate version 0.1\n"
				+ "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
				+ "pushee git://localhost/repo.git\n"
				+ "nonce 1433954361-bde756572d665bba81d8\n"
				+ "\n");
		for (String updateLine : updateLines) {
			cert.append(updateLine).append('\n');
		}
		cert.append(
				"-----BEGIN PGP SIGNATURE-----\n"
				+ "DUMMY/SIGNATURE\n"
				+ "-----END PGP SIGNATURE-----\n");
		try {
			return PushCertificateParser.fromReader(new InputStreamReader(
					new ByteArrayInputStream(
							Constants.encode(cert.toString())),
					UTF_8));
		} catch (IOException e) {
			throw new IllegalArgumentException(e);
		}
	}

	private static String command(ObjectId oldId, ObjectId newId, String ref) {
		return oldId.name() + " " + newId.name() + " " + ref;
	}

	private AtomicInteger ts = new AtomicInteger(1433954361);
	private InMemoryRepository repo;
	private PushCertificateStore store;

	@Before
	public void setUp() throws Exception {
		repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
		store = newStore();
	}

	@Test
	public void missingRef() throws Exception {
		assertCerts("refs/heads/master");
	}

	@Test
	public void saveNoChange() throws Exception {
		assertEquals(NO_CHANGE, store.save());
	}

	@Test
	public void saveOneCertOnOneRef() throws Exception {
		PersonIdent ident = newIdent();
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, ident);
		assertEquals(NEW, store.save());
		assertCerts("refs/heads/master", addMaster);
		assertCerts("refs/heads/branch");

		try (RevWalk rw = new RevWalk(repo)) {
			RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME));
			rw.parseBody(c);
			assertEquals("Store push certificate for refs/heads/master\n",
					c.getFullMessage());
			assertEquals(ident, c.getAuthorIdent());
			assertEquals(ident, c.getCommitterIdent());
		}
	}

	@Test
	public void saveTwoCertsOnSameRefInTwoUpdates() throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertEquals(NEW, store.save());
		PushCertificate updateMaster = newCert(
				command(ID1, ID2, "refs/heads/master"));
		store.put(updateMaster, newIdent());
		assertEquals(FAST_FORWARD, store.save());
		assertCerts("refs/heads/master", updateMaster, addMaster);
	}

	@Test
	public void saveTwoCertsOnSameRefInOneUpdate() throws Exception {
		PersonIdent ident1 = newIdent();
		PersonIdent ident2 = newIdent();
		PushCertificate updateMaster = newCert(
				command(ID1, ID2, "refs/heads/master"));
		store.put(updateMaster, ident2);
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, ident1);
		assertEquals(NEW, store.save());
		assertCerts("refs/heads/master", updateMaster, addMaster);
	}

	@Test
	public void saveTwoCertsOnDifferentRefsInOneUpdate() throws Exception {
		PersonIdent ident1 = newIdent();
		PersonIdent ident3 = newIdent();
		PushCertificate addBranch = newCert(
				command(zeroId(), ID1, "refs/heads/branch"));
		store.put(addBranch, ident3);
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, ident1);
		assertEquals(NEW, store.save());
		assertCerts("refs/heads/master", addMaster);
		assertCerts("refs/heads/branch", addBranch);
	}

	@Test
	public void saveTwoCertsOnDifferentRefsInTwoUpdates() throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertEquals(NEW, store.save());
		PushCertificate addBranch = newCert(
				command(zeroId(), ID1, "refs/heads/branch"));
		store.put(addBranch, newIdent());
		assertEquals(FAST_FORWARD, store.save());
		assertCerts("refs/heads/master", addMaster);
		assertCerts("refs/heads/branch", addBranch);
	}

	@Test
	public void saveOneCertOnMultipleRefs() throws Exception {
		PersonIdent ident = newIdent();
		PushCertificate addMasterAndBranch = newCert(
				command(zeroId(), ID1, "refs/heads/branch"),
				command(zeroId(), ID2, "refs/heads/master"));
		store.put(addMasterAndBranch, ident);
		assertEquals(NEW, store.save());
		assertCerts("refs/heads/master", addMasterAndBranch);
		assertCerts("refs/heads/branch", addMasterAndBranch);

		try (RevWalk rw = new RevWalk(repo)) {
			RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME));
			rw.parseBody(c);
			assertEquals("Store push certificate for 2 refs\n", c.getFullMessage());
			assertEquals(ident, c.getAuthorIdent());
			assertEquals(ident, c.getCommitterIdent());
		}
	}

	@Test
	public void changeRefFileToDirectory() throws Exception {
		PushCertificate deleteRefsHeads = newCert(
				command(ID1, zeroId(), "refs/heads"));
		store.put(deleteRefsHeads, newIdent());
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertEquals(NEW, store.save());
		assertCerts("refs/heads", deleteRefsHeads);
		assertCerts("refs/heads/master", addMaster);
	}

	@Test
	public void getBeforeSaveDoesNotIncludePending() throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertEquals(NEW, store.save());

		PushCertificate updateMaster = newCert(
				command(ID1, ID2, "refs/heads/master"));
		store.put(updateMaster, newIdent());

		assertCerts("refs/heads/master", addMaster);
		assertEquals(FAST_FORWARD, store.save());
		assertCerts("refs/heads/master", updateMaster, addMaster);
	}

	@Test
	public void lockFailure() throws Exception {
		PushCertificateStore store1 = store;
		PushCertificateStore store2 = newStore();
		store2.get("refs/heads/master");

		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store1.put(addMaster, newIdent());
		assertEquals(NEW, store1.save());

		PushCertificate addBranch = newCert(
				command(zeroId(), ID2, "refs/heads/branch"));
		store2.put(addBranch, newIdent());

		assertEquals(LOCK_FAILURE, store2.save());
		// Reread ref after lock failure.
		assertCerts(store2, "refs/heads/master", addMaster);
		assertCerts(store2, "refs/heads/branch");

		assertEquals(FAST_FORWARD, store2.save());
		assertCerts(store2, "refs/heads/master", addMaster);
		assertCerts(store2, "refs/heads/branch", addBranch);
	}

	@Test
	public void saveInBatch() throws Exception {
		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
		assertFalse(store.save(batch));
		assertEquals(0, batch.getCommands().size());
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertTrue(store.save(batch));

		List<ReceiveCommand> commands = batch.getCommands();
		assertEquals(1, commands.size());
		ReceiveCommand cmd = commands.get(0);
		assertEquals("refs/meta/push-certs", cmd.getRefName());
		assertEquals(ReceiveCommand.Result.NOT_ATTEMPTED, cmd.getResult());

		try (RevWalk rw = new RevWalk(repo)) {
			batch.execute(rw, NullProgressMonitor.INSTANCE);
			assertEquals(ReceiveCommand.Result.OK, cmd.getResult());
		}
	}

	@Test
	public void putMatchingWithNoMatchingRefs() throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"),
				command(zeroId(), ID2, "refs/heads/branch"));
		store.put(addMaster, newIdent(), Collections.<ReceiveCommand> emptyList());
		assertEquals(NO_CHANGE, store.save());
	}

	@Test
	public void putMatchingWithNoMatchingRefsInBatchOnEmptyRef()
			throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"),
				command(zeroId(), ID2, "refs/heads/branch"));
		store.put(addMaster, newIdent(), Collections.<ReceiveCommand> emptyList());
		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
		assertFalse(store.save(batch));
		assertEquals(0, batch.getCommands().size());
	}

	@Test
	public void putMatchingWithNoMatchingRefsInBatchOnNonEmptyRef()
			throws Exception {
		PushCertificate addMaster = newCert(
				command(zeroId(), ID1, "refs/heads/master"));
		store.put(addMaster, newIdent());
		assertEquals(NEW, store.save());

		PushCertificate addBranch = newCert(
				command(zeroId(), ID2, "refs/heads/branch"));
		store.put(addBranch, newIdent(), Collections.<ReceiveCommand> emptyList());
		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
		assertFalse(store.save(batch));
		assertEquals(0, batch.getCommands().size());
	}

	@Test
	public void putMatchingWithSomeMatchingRefs() throws Exception {
		PushCertificate addMasterAndBranch = newCert(
				command(zeroId(), ID1, "refs/heads/master"),
				command(zeroId(), ID2, "refs/heads/branch"));
		store.put(addMasterAndBranch, newIdent(),
				Collections.singleton(addMasterAndBranch.getCommands().get(0)));
		assertEquals(NEW, store.save());
		assertCerts("refs/heads/master", addMasterAndBranch);
		assertCerts("refs/heads/branch");
	}

	private PersonIdent newIdent() {
		return new PersonIdent(
				"A U. Thor", "author@example.com", ts.getAndIncrement(), 0);
	}

	private PushCertificateStore newStore() {
		return new PushCertificateStore(repo);
	}

	private void assertCerts(String refName, PushCertificate... expected)
			throws Exception {
		assertCerts(store, refName, expected);
		assertCerts(newStore(), refName, expected);
	}

	private static void assertCerts(PushCertificateStore store, String refName,
			PushCertificate... expected) throws Exception {
		List<PushCertificate> ex = Arrays.asList(expected);
		PushCertificate first = !ex.isEmpty() ? ex.get(0) : null;
		assertEquals(first, store.get(refName));
		assertEquals(ex, toList(store.getAll(refName)));
	}

	private static <T> List<T> toList(Iterable<T> it) {
		List<T> list = new ArrayList<>();
		for (T t : it) {
			list.add(t);
		}
		return list;
	}
}
