/*
 * Copyright (C) 2010, Sasa Zivkov <sasa.zivkov@sap.com> 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.notes;

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

import java.io.IOException;
import java.util.Iterator;

import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class NoteMapMergerTest extends RepositoryTestCase {
	private TestRepository<Repository> tr;

	private ObjectReader reader;

	private ObjectInserter inserter;

	private NoteMap noRoot;

	private NoteMap empty;

	private NoteMap map_a;

	private NoteMap map_a_b;

	private RevBlob noteAId;

	private String noteAContent;

	private RevBlob noteABlob;

	private RevBlob noteBId;

	private String noteBContent;

	private RevBlob noteBBlob;

	private RevCommit sampleTree_a;

	private RevCommit sampleTree_a_b;

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		tr = new TestRepository<>(db);
		reader = db.newObjectReader();
		inserter = db.newObjectInserter();

		noRoot = NoteMap.newMap(null, reader);
		empty = NoteMap.newEmptyMap();

		noteAId = tr.blob("a");
		noteAContent = "noteAContent";
		noteABlob = tr.blob(noteAContent);
		sampleTree_a = tr.commit()
				.add(noteAId.name(), noteABlob)
				.create();
		tr.parseBody(sampleTree_a);
		map_a = NoteMap.read(reader, sampleTree_a);

		noteBId = tr.blob("b");
		noteBContent = "noteBContent";
		noteBBlob = tr.blob(noteBContent);
		sampleTree_a_b = tr.commit()
				.add(noteAId.name(), noteABlob)
				.add(noteBId.name(), noteBBlob)
				.create();
		tr.parseBody(sampleTree_a_b);
		map_a_b = NoteMap.read(reader, sampleTree_a_b);
	}

	@Override
	@After
	public void tearDown() throws Exception {
		reader.close();
		inserter.close();
		super.tearDown();
	}

	@Test
	public void testNoChange() throws IOException {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		assertEquals(0, countNotes(merger.merge(noRoot, noRoot, noRoot)));
		assertEquals(0, countNotes(merger.merge(empty, empty, empty)));

		result = merger.merge(map_a, map_a, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
	}

	@Test
	public void testOursEqualsTheirs() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		assertEquals(0, countNotes(merger.merge(empty, noRoot, noRoot)));
		assertEquals(0, countNotes(merger.merge(map_a, noRoot, noRoot)));

		assertEquals(0, countNotes(merger.merge(noRoot, empty, empty)));
		assertEquals(0, countNotes(merger.merge(map_a, empty, empty)));

		result = merger.merge(noRoot, map_a, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		result = merger.merge(empty, map_a, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		result = merger.merge(map_a_b, map_a, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		result = merger.merge(map_a, map_a_b, map_a_b);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(noteBBlob, result.get(noteBId));
	}

	@Test
	public void testBaseEqualsOurs() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		assertEquals(0, countNotes(merger.merge(noRoot, noRoot, empty)));
		result = merger.merge(noRoot, noRoot, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		assertEquals(0, countNotes(merger.merge(empty, empty, noRoot)));
		result = merger.merge(empty, empty, map_a);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		assertEquals(0, countNotes(merger.merge(map_a, map_a, noRoot)));
		assertEquals(0, countNotes(merger.merge(map_a, map_a, empty)));
		result = merger.merge(map_a, map_a, map_a_b);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(noteBBlob, result.get(noteBId));
	}

	@Test
	public void testBaseEqualsTheirs() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		assertEquals(0, countNotes(merger.merge(noRoot, empty, noRoot)));
		result = merger.merge(noRoot, map_a, noRoot);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		assertEquals(0, countNotes(merger.merge(empty, noRoot, empty)));
		result = merger.merge(empty, map_a, empty);
		assertEquals(1, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));

		assertEquals(0, countNotes(merger.merge(map_a, noRoot, map_a)));
		assertEquals(0, countNotes(merger.merge(map_a, empty, map_a)));
		result = merger.merge(map_a, map_a_b, map_a);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(noteBBlob, result.get(noteBId));
	}

	@Test
	public void testAddDifferentNotes() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		NoteMap map_a_c = NoteMap.read(reader, sampleTree_a);
		RevBlob noteCId = tr.blob("c");
		RevBlob noteCBlob = tr.blob("noteCContent");
		map_a_c.set(noteCId, noteCBlob);
		map_a_c.writeTree(inserter);

		result = merger.merge(map_a, map_a_b, map_a_c);
		assertEquals(3, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(noteBBlob, result.get(noteBId));
		assertEquals(noteCBlob, result.get(noteCId));
	}

	@Test
	public void testAddSameNoteDifferentContent() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, new DefaultNoteMerger(),
				null);
		NoteMap result;

		NoteMap map_a_b1 = NoteMap.read(reader, sampleTree_a);
		String noteBContent1 = noteBContent + "change";
		RevBlob noteBBlob1 = tr.blob(noteBContent1);
		map_a_b1.set(noteBId, noteBBlob1);
		map_a_b1.writeTree(inserter);

		result = merger.merge(map_a, map_a_b, map_a_b1);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(tr.blob(noteBContent + noteBContent1), result.get(noteBId));
	}

	@Test
	public void testEditSameNoteDifferentContent() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, new DefaultNoteMerger(),
				null);
		NoteMap result;

		NoteMap map_a1 = NoteMap.read(reader, sampleTree_a);
		String noteAContent1 = noteAContent + "change1";
		RevBlob noteABlob1 = tr.blob(noteAContent1);
		map_a1.set(noteAId, noteABlob1);
		map_a1.writeTree(inserter);

		NoteMap map_a2 = NoteMap.read(reader, sampleTree_a);
		String noteAContent2 = noteAContent + "change2";
		RevBlob noteABlob2 = tr.blob(noteAContent2);
		map_a2.set(noteAId, noteABlob2);
		map_a2.writeTree(inserter);

		result = merger.merge(map_a, map_a1, map_a2);
		assertEquals(1, countNotes(result));
		assertEquals(tr.blob(noteAContent1 + noteAContent2),
				result.get(noteAId));
	}

	@Test
	public void testEditDifferentNotes() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap result;

		NoteMap map_a1_b = NoteMap.read(reader, sampleTree_a_b);
		String noteAContent1 = noteAContent + "change";
		RevBlob noteABlob1 = tr.blob(noteAContent1);
		map_a1_b.set(noteAId, noteABlob1);
		map_a1_b.writeTree(inserter);

		NoteMap map_a_b1 = NoteMap.read(reader, sampleTree_a_b);
		String noteBContent1 = noteBContent + "change";
		RevBlob noteBBlob1 = tr.blob(noteBContent1);
		map_a_b1.set(noteBId, noteBBlob1);
		map_a_b1.writeTree(inserter);

		result = merger.merge(map_a_b, map_a1_b, map_a_b1);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob1, result.get(noteAId));
		assertEquals(noteBBlob1, result.get(noteBId));
	}

	@Test
	public void testDeleteDifferentNotes() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);

		NoteMap map_b = NoteMap.read(reader, sampleTree_a_b);
		map_b.set(noteAId, null); // delete note a
		map_b.writeTree(inserter);

		assertEquals(0, countNotes(merger.merge(map_a_b, map_a, map_b)));
	}

	@Test
	public void testEditDeleteConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, new DefaultNoteMerger(),
				null);
		NoteMap result;

		NoteMap map_a_b1 = NoteMap.read(reader, sampleTree_a_b);
		String noteBContent1 = noteBContent + "change";
		RevBlob noteBBlob1 = tr.blob(noteBContent1);
		map_a_b1.set(noteBId, noteBBlob1);
		map_a_b1.writeTree(inserter);

		result = merger.merge(map_a_b, map_a_b1, map_a);
		assertEquals(2, countNotes(result));
		assertEquals(noteABlob, result.get(noteAId));
		assertEquals(noteBBlob1, result.get(noteBId));
	}

	@Test
	public void testLargeTreesWithoutConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);
		NoteMap map1 = createLargeNoteMap("note_1_", "content_1_", 300, 0);
		NoteMap map2 = createLargeNoteMap("note_2_", "content_2_", 300, 0);

		NoteMap result = merger.merge(empty, map1, map2);
		assertEquals(600, countNotes(result));
		// check a few random notes
		assertEquals(tr.blob("content_1_59"), result.get(tr.blob("note_1_59")));
		assertEquals(tr.blob("content_2_10"), result.get(tr.blob("note_2_10")));
		assertEquals(tr.blob("content_2_99"), result.get(tr.blob("note_2_99")));
	}

	@Test
	public void testLargeTreesWithConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, new DefaultNoteMerger(),
				null);
		NoteMap largeTree1 = createLargeNoteMap("note_1_", "content_1_", 300, 0);
		NoteMap largeTree2 = createLargeNoteMap("note_1_", "content_2_", 300, 0);

		NoteMap result = merger.merge(empty, largeTree1, largeTree2);
		assertEquals(300, countNotes(result));
		// check a few random notes
		assertEquals(tr.blob("content_1_59content_2_59"),
				result.get(tr.blob("note_1_59")));
		assertEquals(tr.blob("content_1_10content_2_10"),
				result.get(tr.blob("note_1_10")));
		assertEquals(tr.blob("content_1_99content_2_99"),
				result.get(tr.blob("note_1_99")));
	}

	private NoteMap createLargeNoteMap(String noteNamePrefix,
			String noteContentPrefix, int notesCount, int firstIndex)
			throws Exception {
		NoteMap result = NoteMap.newEmptyMap();
		for (int i = 0; i < notesCount; i++) {
			result.set(tr.blob(noteNamePrefix + (firstIndex + i)),
					tr.blob(noteContentPrefix + (firstIndex + i)));
		}
		result.writeTree(inserter);
		return result;
	}

	@Test
	public void testFanoutAndLeafWithoutConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);

		NoteMap largeTree = createLargeNoteMap("note_1_", "content_1_", 300, 0);
		NoteMap result = merger.merge(map_a, map_a_b, largeTree);
		assertEquals(301, countNotes(result));
	}

	@Test
	public void testFanoutAndLeafWitConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, new DefaultNoteMerger(),
				null);

		NoteMap largeTree_b1 = createLargeNoteMap("note_1_", "content_1_", 300,
				0);
		String noteBContent1 = noteBContent + "change";
		largeTree_b1.set(noteBId, tr.blob(noteBContent1));
		largeTree_b1.writeTree(inserter);

		NoteMap result = merger.merge(map_a, map_a_b, largeTree_b1);
		assertEquals(301, countNotes(result));
		assertEquals(tr.blob(noteBContent + noteBContent1), result.get(noteBId));
	}

	@Test
	public void testCollapseFanoutAfterMerge() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null, null);

		NoteMap largeTree = createLargeNoteMap("note_", "content_", 257, 0);
		assertTrue(largeTree.getRoot() instanceof FanoutBucket);
		NoteMap deleteFirstHundredNotes = createLargeNoteMap("note_", "content_", 157,
				100);
		NoteMap deleteLastHundredNotes = createLargeNoteMap("note_",
				"content_", 157, 0);
		NoteMap result = merger.merge(largeTree, deleteFirstHundredNotes,
				deleteLastHundredNotes);
		assertEquals(57, countNotes(result));
		assertTrue(result.getRoot() instanceof LeafBucket);
	}

	@Test
	public void testNonNotesWithoutNonNoteConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null,
				MergeStrategy.RESOLVE);
		RevCommit treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob) // this is a note
				.add("a.txt", tr.blob("content of a.txt")) // this is a non-note
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap base = NoteMap.read(reader, treeWithNonNotes);

		treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob)
				.add("a.txt", tr.blob("content of a.txt"))
				.add("b.txt", tr.blob("content of b.txt"))
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap ours = NoteMap.read(reader, treeWithNonNotes);

		treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob)
				.add("a.txt", tr.blob("content of a.txt"))
				.add("c.txt", tr.blob("content of c.txt"))
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap theirs = NoteMap.read(reader, treeWithNonNotes);

		NoteMap result = merger.merge(base, ours, theirs);
		assertEquals(3, countNonNotes(result));
	}

	@Test
	public void testNonNotesWithNonNoteConflict() throws Exception {
		NoteMapMerger merger = new NoteMapMerger(db, null,
				MergeStrategy.RESOLVE);
		RevCommit treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob) // this is a note
				.add("a.txt", tr.blob("content of a.txt")) // this is a non-note
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap base = NoteMap.read(reader, treeWithNonNotes);

		treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob)
				.add("a.txt", tr.blob("change 1"))
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap ours = NoteMap.read(reader, treeWithNonNotes);

		treeWithNonNotes =
			tr.commit()
				.add(noteAId.name(), noteABlob)
				.add("a.txt", tr.blob("change 2"))
				.create();
		tr.parseBody(treeWithNonNotes);
		NoteMap theirs = NoteMap.read(reader, treeWithNonNotes);

		try {
			merger.merge(base, ours, theirs);
			fail("NotesMergeConflictException was expected");
		} catch (NotesMergeConflictException e) {
			// expected
		}
	}

	private static int countNotes(NoteMap map) {
		int c = 0;
		Iterator<Note> it = map.iterator();
		while (it.hasNext()) {
			it.next();
			c++;
		}
		return c;
	}

	private static int countNonNotes(NoteMap map) {
		int c = 0;
		NonNoteEntry nonNotes = map.getRoot().nonNotes;
		while (nonNotes != null) {
			c++;
			nonNotes = nonNotes.next;
		}
		return c;
	}
}
