/*******************************************************************************
 * Copyright (c) 2021 Hannes Wellmann and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Hannes Wellmann - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.tests.container;

import static org.eclipse.osgi.tests.container.NamespaceListTest.build;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderAddAll;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderAddAllFiltered;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderAddAllFilteredAfterLastMatch;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderCreate;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderGetNamespaceElements;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderRemoveElementsOfNamespaceIf;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderRemoveNamespaceIf;
import static org.eclipse.osgi.tests.container.NamespaceListTest.builderTransformIntoCopy;
import static org.eclipse.osgi.tests.container.NamespaceListTest.createEmptyNamespaceList;
import static org.eclipse.osgi.tests.container.NamespaceListTest.getList;
import static org.eclipse.osgi.tests.container.NamespaceListTest.newNamespace;
import static org.eclipse.osgi.tests.container.NamespaceListTest.populate;
import static org.eclipse.osgi.tests.container.NamespaceListTest.randomListSort;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.osgi.tests.container.NamespaceListTest.NamespaceElement;
import org.junit.Test;

public class NamespaceListBuilderTest {

	@Test
	public void testCreate() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		assertTrue("Builder is not initially empty", builder.isEmpty());
	}

	@Test
	public void testIteratorsElementSequence_multipleNamespace() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		for (int i = 0; i < 12; i++) {
			assertTrue(iterator.hasNext());
			assertSame(elements.get(i), iterator.next());
		}
		assertFalse(iterator.hasNext());
	}

	@Test
	public void testIteratorsElementSequence_oneNamespace() throws Exception {
		List<NamespaceElement> elements = populate(3, "ns-1");

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		for (int i = 0; i < 3; i++) {
			assertTrue(iterator.hasNext());
			assertSame(elements.get(i), iterator.next());
		}
		assertFalse(iterator.hasNext());
	}

	@Test
	public void testIteratorsElementSequence_empty() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		Iterator<NamespaceElement> iterator = builder.iterator();

		assertFalse(iterator.hasNext());
	}

	@Test
	public void testIteratorsElementSequence_iterationBeyoundEnd_NoSuchElementException() throws Exception {
		List<NamespaceElement> elements = populate(2, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		for (int i = 0; i < 6; i++) {
			assertTrue(iterator.hasNext());
			assertSame(elements.get(i), iterator.next());
		}
		assertFalse(iterator.hasNext());
		assertThrows(NoSuchElementException.class, iterator::next);
	}

	@Test
	public void testIteratorsElementSequence_iterationBeyoundEndOfEmptyBuilder_NoSuchElementException()
			throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		Iterator<NamespaceElement> iterator = builder.iterator();

		assertFalse(iterator.hasNext());
		assertThrows(NoSuchElementException.class, iterator::next);
	}

	@Test
	public void testIteratorRemove_removeOneElement() throws Exception {

		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		for (int i = 0; i < 6; i++) {
			iterator.next();
		}
		iterator.remove();

		elements.remove(5);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testIteratorRemove_RemoveAllElements() throws Exception {

		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		for (int i = 0; i < 12; i++) {
			iterator.next();
			iterator.remove();
		}

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testIteratorRemove_RemoveTwice_IllegalStateException() throws Exception {

		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		iterator.next();
		iterator.remove();
		assertThrows(IllegalStateException.class, iterator::remove);

		elements.remove(0);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testIteratorRemove_NextNotCalled_IllegalStateException() throws Exception {

		List<NamespaceElement> elements = populate(1, 1);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Iterator<NamespaceElement> iterator = builder.iterator();

		assertThrows(IllegalStateException.class, iterator::remove);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testClear() throws Exception {
		List<NamespaceElement> elements = populate(3, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Object namespaceList = build(builder);

		builder.clear();

		assertEquals(0, builder.size());
		assertIterationOrderEquals(Collections.emptyList(), builder);

		// assert that a previously build list is not affected

		assertEquals(getList(namespaceList, null), elements);
		assertEquals(getList(namespaceList, "ns-0"), elements.subList(0, 3));
		assertEquals(getList(namespaceList, "ns-1"), elements.subList(3, 6));
	}

	@Test
	public void testGetNamespaceElements_contaiendNamespace() throws Exception {
		List<NamespaceElement> elements = populate(3, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> namespaceElements = builderGetNamespaceElements(builder, "ns-1");
		assertEquals(namespaceElements, elements.subList(3, 6));
	}

	@Test
	public void testGetNamespaceElements_nullNamespace() throws Exception {
		List<NamespaceElement> elements = populate(3, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> namespaceElements = builderGetNamespaceElements(builder, null);
		assertEquals(elements, namespaceElements);
	}

	@Test
	public void testGetNamespaceElements_notContainedNamespace() throws Exception {
		List<NamespaceElement> elements = populate(3, 3);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> namespaceElements = builderGetNamespaceElements(builder, "ns-100");
		assertEquals(Collections.emptyList(), namespaceElements);
	}

	@Test
	public void testTransformIntoCopy_notEmptyBuilder() throws Exception {
		List<NamespaceElement> elements = populate(3, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Function<String, String> getNamespace = e -> e.substring(0, 4);

		Collection<String> transformedBuilder = builderTransformIntoCopy(builder, Object::toString, getNamespace);

		List<String> transformedElements = elements.stream().map(Object::toString).collect(Collectors.toList());
		assertStricEqualContent(transformedBuilder, transformedElements, getNamespace);
		assertNotSame(builder, transformedBuilder);
		// check that the original builder is not changed
		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testTransformIntoCopy_emptyBuilder() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		Function<String, String> getNamespace = e -> e.substring(0, 4);

		Collection<String> transformedBuilder = builderTransformIntoCopy(builder, Object::toString, getNamespace);

		assertNotSame(builder, transformedBuilder);
		assertStricEqualContent(transformedBuilder, Collections.emptyList(), getNamespace);
		// check that the original builder is not changed
		assertStricEqualContent(builder, Collections.emptyList());
	}

	// --- test addition ---

	@Test
	public void testAdd_singleElement() throws Exception {
		NamespaceElement e = new NamespaceElement(1, "ns-1");

		Collection<NamespaceElement> builder = builderCreate();

		builder.add(e);

		assertEquals(Collections.singletonList(e), getList(build(builder), "ns-1"));

		assertStricEqualContent(builder, Collections.singletonList(e));
	}

	@Test
	public void testAdd_multipleElements() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();

		for (NamespaceElement element : elements) {
			builder.add(element);
		}

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAll_namespaceSortedList() throws Exception {
		List<NamespaceElement> elements = populate(4, 7);

		Collection<NamespaceElement> builder = builderCreate();

		builder.addAll(elements);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAll_randomlySortedList() throws Exception {
		List<NamespaceElement> elements = populate(4, 7);
		randomListSort(elements);

		Collection<NamespaceElement> builder = builderCreate();

		builder.addAll(elements);

		assertEqualContent(builder, elements);
	}

	@Test
	public void testAddAll_emptyList() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		builder.addAll(Collections.emptyList());

		assertEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testAddAll_namespaceList() throws Exception {
		List<NamespaceElement> elements = populate(5, 5);

		Object namespaceList = newNamespace(elements);

		Collection<NamespaceElement> builder = builderCreate();

		builderAddAll(builder, namespaceList);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAll_emptyNamespaceList() throws Exception {

		Object namespaceList = createEmptyNamespaceList(NamespaceElement::getNamespace);

		Collection<NamespaceElement> builder = builderCreate();

		builderAddAll(builder, namespaceList);

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testAddAll_builderArgument() throws Exception {
		List<NamespaceElement> elements = populate(2, 2);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Collection<NamespaceElement> builder2 = builderCreate();
		List<NamespaceElement> elements2 = populate(2, 2);
		builder2.addAll(elements2);

		builder.addAll(builder2);

		elements.addAll(4, elements2.subList(2, 4));
		elements.addAll(2, elements2.subList(0, 2));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAllFiltered_notEmptyBuilder() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> newElements = new ArrayList<>();
		newElements.add(new NamespaceElement(0, "ns-1"));
		newElements.add(new NamespaceElement(10, "ns-1"));
		newElements.add(new NamespaceElement(2, "ns-20"));
		newElements.add(new NamespaceElement(0, "ns-2"));
		newElements.add(new NamespaceElement(2, "ns-0"));
		newElements.add(new NamespaceElement(1, "ns-2"));
		newElements.add(new NamespaceElement(10, "ns-20"));
		newElements.add(new NamespaceElement(0, "ns-20"));
		Object namespaceList = newNamespace(newElements);

		builderAddAllFiltered(builder, namespaceList, n -> !n.equals("ns-2"), e -> e.id < 10);

		List<NamespaceElement> expectedElements = new ArrayList<>();
		expectedElements.add(new NamespaceElement(0, "ns-0"));
		expectedElements.add(new NamespaceElement(1, "ns-0"));
		expectedElements.add(new NamespaceElement(2, "ns-0"));
		expectedElements.add(new NamespaceElement(3, "ns-0"));
		expectedElements.add(new NamespaceElement(2, "ns-0"));

		expectedElements.add(new NamespaceElement(0, "ns-1"));
		expectedElements.add(new NamespaceElement(1, "ns-1"));
		expectedElements.add(new NamespaceElement(2, "ns-1"));
		expectedElements.add(new NamespaceElement(3, "ns-1"));
		expectedElements.add(new NamespaceElement(0, "ns-1"));

		expectedElements.add(new NamespaceElement(0, "ns-2"));
		expectedElements.add(new NamespaceElement(1, "ns-2"));
		expectedElements.add(new NamespaceElement(2, "ns-2"));
		expectedElements.add(new NamespaceElement(3, "ns-2"));

		expectedElements.add(new NamespaceElement(2, "ns-20"));
		expectedElements.add(new NamespaceElement(0, "ns-20"));

		assertStricEqualContent(builder, expectedElements);
	}

	@Test
	public void testAddAllFiltered_emptyBuilder() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		List<NamespaceElement> newElements2 = new ArrayList<>();
		newElements2.add(new NamespaceElement(0, "ns-1"));
		newElements2.add(new NamespaceElement(10, "ns-1"));
		newElements2.add(new NamespaceElement(2, "ns-20"));
		newElements2.add(new NamespaceElement(0, "ns-2"));
		newElements2.add(new NamespaceElement(2, "ns-0"));
		newElements2.add(new NamespaceElement(1, "ns-2"));
		newElements2.add(new NamespaceElement(10, "ns-20"));
		newElements2.add(new NamespaceElement(0, "ns-20"));
		Object namespaceList = newNamespace(newElements2);

		builderAddAllFiltered(builder, namespaceList, n -> !n.equals("ns-2"), e -> e.id < 10);

		List<NamespaceElement> expectedElements = new ArrayList<>();
		expectedElements.add(new NamespaceElement(0, "ns-1"));
		expectedElements.add(new NamespaceElement(2, "ns-20"));
		expectedElements.add(new NamespaceElement(0, "ns-20"));
		expectedElements.add(new NamespaceElement(2, "ns-0"));

		assertStricEqualContent(builder, expectedElements);
	}

	@Test
	public void testAddAllFiltered_allElementsOfNewNamespaceFiltered() throws Exception {

		Collection<NamespaceElement> builder = builderCreate();

		List<NamespaceElement> newElements = new ArrayList<>();
		newElements.add(new NamespaceElement(10, "ns-1"));
		newElements.add(new NamespaceElement(11, "ns-1"));
		Object namespaceList = newNamespace(newElements);

		builderAddAllFiltered(builder, namespaceList, n -> true, e -> e.id < 5);
		// All elements of namespace ns-1 are filtered

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testAddAllFilteredAfterLastMatch_matchUpUntilTheMiddle() throws Exception {
		List<NamespaceElement> elements = populate(4, "ns-0");

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		NamespaceElement element = new NamespaceElement(5, "ns-0");
		Object namespaceList = newNamespace(Collections.singletonList(element));
		builderAddAllFilteredAfterLastMatch(builder, namespaceList, n -> true, e -> true, (toAdd, e) -> e.id < 2);

		elements.add(2, element);
		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAllFilteredAfterLastMatch_allElementsMatches() throws Exception {
		List<NamespaceElement> elements = populate(4, "ns-0");

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		NamespaceElement element = new NamespaceElement(5, "ns-0");
		Object namespaceList = newNamespace(Collections.singletonList(element));
		builderAddAllFilteredAfterLastMatch(builder, namespaceList, n -> true, e -> true, (toAdd, e) -> true);

		elements.add(4, element);
		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAllFilteredAfterLastMatch_noMatch() throws Exception {
		List<NamespaceElement> elements = populate(4, "ns-0");

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		NamespaceElement element = new NamespaceElement(5, "ns-0");
		Object namespaceList = newNamespace(Collections.singletonList(element));
		builderAddAllFilteredAfterLastMatch(builder, namespaceList, n -> true, e -> true, (toAdd, e) -> e.id > 100);

		elements.add(0, element);
		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testAddAllFilteredAfterLastMatch_emptyNamespaceList() throws Exception {
		Collection<NamespaceElement> builder = builderCreate();

		NamespaceElement element = new NamespaceElement(5, "ns-0");
		Object namespaceList = newNamespace(Collections.singletonList(element));
		builderAddAllFilteredAfterLastMatch(builder, namespaceList, n -> true, e -> true, (toAdd, e) -> e.id < 2);

		assertStricEqualContent(builder, Collections.singletonList(element));
	}

	// --- test removal ---

	@Test
	public void testRemove_elementIsOneOfMultipleOfNamespace() throws Exception {
		List<NamespaceElement> elements = populate(4, 4);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertTrue(builder.remove(new NamespaceElement(2, "ns-0")));

		elements.remove(2);
		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemove_onlyElementOfNamspace() throws Exception {
		Collection<NamespaceElement> builder = builderCreate();
		builder.add(new NamespaceElement(3, "ns-0"));

		assertTrue(builder.remove(new NamespaceElement(3, "ns-0")));

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testRemove_elementNotContainedInNamespaceList() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertFalse(builder.remove(new NamespaceElement(100, "ns-0")));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemove_elementWithNotPresentNamespace() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertFalse(builder.remove(new NamespaceElement(1, "ns-100")));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemove_emptyBuilder() throws Exception {
		Collection<NamespaceElement> builder = builderCreate();

		assertFalse(builder.remove(new NamespaceElement(3, "ns-0")));

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	@SuppressWarnings("unlikely-arg-type")
	public void testRemove_argumentOfOtherClass() throws Exception {
		List<NamespaceElement> elements = populate(2, 3);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertFalse(builder.remove("someString"));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveAll_multipleElementsInMultipleNamsespaces() throws Exception {
		List<NamespaceElement> elements = populate(4, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> toRemove = new ArrayList<>();
		toRemove.add(new NamespaceElement(1, "ns-0")); // has total index 1
		toRemove.add(new NamespaceElement(2, "ns-1")); // has total index 6
		toRemove.add(new NamespaceElement(0, "ns-1")); // has total index 4
		toRemove.add(new NamespaceElement(3, "ns-3")); // has total index 15
		toRemove.add(new NamespaceElement(2, "ns-3")); // has total index 14

		assertTrue(builder.removeAll(toRemove));

		elements.remove(15);
		elements.remove(14);
		elements.remove(6);
		elements.remove(4);
		elements.remove(1);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveAll_multipleElementsInMultipleNamsespacesAndSomeNotPresent() throws Exception {
		List<NamespaceElement> elements = populate(4, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<NamespaceElement> toRemove = new ArrayList<>();
		toRemove.add(new NamespaceElement(1, "ns-0")); // has total index 1
		toRemove.add(new NamespaceElement(2, "ns-1")); // has total index 6
		toRemove.add(new NamespaceElement(100, "ns-2")); // not present
		toRemove.add(new NamespaceElement(0, "ns-1")); // has total index 4
		toRemove.add(new NamespaceElement(3, "ns-3")); // has total index 15
		toRemove.add(new NamespaceElement(100, "ns-3")); // not present
		toRemove.add(new NamespaceElement(2, "ns-3")); // has total index 14
		toRemove.add(new NamespaceElement(100, "ns-3")); // not present
		toRemove.add(new NamespaceElement(1, "ns-100")); // not present

		assertTrue(builder.removeAll(toRemove));

		elements.remove(15);
		elements.remove(14);
		elements.remove(6);
		elements.remove(4);
		elements.remove(1);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveAll_listOfAllElementsInBuilder() throws Exception {
		List<NamespaceElement> elements = populate(4, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertTrue(builder.removeAll(elements));

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testRemoveAll_emptyList() throws Exception {
		List<NamespaceElement> elements = populate(4, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		assertFalse(builder.removeAll(Collections.emptyList()));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveAll_emptyBuilder() throws Exception {
		Collection<NamespaceElement> builder = builderCreate();

		assertFalse(builder.removeAll(populate(4, 3)));

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	@SuppressWarnings("unlikely-arg-type")
	public void testRemoveAll_argumentListOfOtherClass() throws Exception {
		List<NamespaceElement> elements = populate(2, 3);
		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		List<String> listOfOtherElements = Arrays.asList("someString", "other");
		assertFalse(builder.removeAll(listOfOtherElements));

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveNamespaceIf_NamespaceMatches() throws Exception {
		List<NamespaceElement> elements = populate(4, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Collection<String> namespacesToRemove = Arrays.asList("ns-0", "ns-2");

		builderRemoveNamespaceIf(builder, namespacesToRemove::contains);

		elements.subList(8, 12).clear();
		elements.subList(0, 4).clear();

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveNamespaceIf_NamespaceMatchesExpunging() throws Exception {
		List<NamespaceElement> elements = populate(3, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveNamespaceIf(builder, n -> true);

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testRemoveNamespaceIf_NoNamespaceMatches() throws Exception {
		List<NamespaceElement> elements = populate(3, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveNamespaceIf(builder, "ns-100"::equals);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveIf_multipleMatches() throws Exception {
		List<NamespaceElement> elements = populate(5, 5);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builder.removeIf(e -> e.id % 3 == 0 || "ns-1".equals(e.namespace));

		elements.remove(23); // first and third of 20-25
		elements.remove(20);

		elements.remove(18); // first and third of 15-20
		elements.remove(15);

		elements.remove(13); // first and third of 10-15
		elements.remove(10);

		elements.subList(5, 10).clear(); // all of 5-10

		elements.remove(3); // first and third of 0-5
		elements.remove(0);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveIf_allMatches() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builder.removeIf(e -> true);

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testRemoveIf_noMatches() throws Exception {
		List<NamespaceElement> elements = populate(4, 3);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builder.removeIf(e -> false);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveElementsOfNamespaceIf_multipleMatches() throws Exception {
		List<NamespaceElement> elements = populate(4, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveElementsOfNamespaceIf(builder, "ns-0", e -> e.id == 1 || e.id == 2);

		elements.remove(2);
		elements.remove(1);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveElementsOfNamespaceIf_allElementsOfNamespaceMatch() throws Exception {
		List<NamespaceElement> elements = populate(4, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveElementsOfNamespaceIf(builder, "ns-0", e -> e.id == 1 || e.id == 2);

		elements.remove(2);
		elements.remove(1);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveElementsOfNamespaceIf_allElementsMatch() throws Exception {
		List<NamespaceElement> elements = populate(4, 1);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveElementsOfNamespaceIf(builder, "ns-0", e -> true);

		assertStricEqualContent(builder, Collections.emptyList());
	}

	@Test
	public void testRemoveElementsOfNamespaceIf_noMatch() throws Exception {
		List<NamespaceElement> elements = populate(4, 1);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveElementsOfNamespaceIf(builder, "ns-0", e -> false);

		assertStricEqualContent(builder, elements);
	}

	@Test
	public void testRemoveElementsOfNamespaceIf_namespaceNotPresent() throws Exception {
		List<NamespaceElement> elements = populate(4, 1);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		builderRemoveElementsOfNamespaceIf(builder, "ns-100", e -> true);

		assertStricEqualContent(builder, elements);
	}

	// --- test build ---

	@Test
	public void testBuild_notEmptyBuilder() throws Exception {
		List<NamespaceElement> elements = populate(4, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Object namespaceList1 = build(builder);

		assertEquals(elements, getList(namespaceList1, null));
		assertEquals(elements.subList(0, 4), getList(namespaceList1, "ns-0"));
		assertEquals(elements.subList(4, 8), getList(namespaceList1, "ns-1"));

		Object namespaceList2 = build(builder);

		assertEquals(elements, getList(namespaceList1, null));
		assertEquals(elements.subList(0, 4), getList(namespaceList1, "ns-0"));
		assertEquals(elements.subList(4, 8), getList(namespaceList1, "ns-1"));

		assertEquals(elements, getList(namespaceList2, null));
		assertEquals(elements.subList(0, 4), getList(namespaceList2, "ns-0"));
		assertEquals(elements.subList(4, 8), getList(namespaceList2, "ns-1"));
	}

	@Test
	public void testBuild_emptyBuilder() throws Exception {
		Collection<NamespaceElement> builder = builderCreate();

		Object namespaceList = build(builder);

		assertEquals(Collections.emptyList(), getList(namespaceList, null));
	}

	@Test
	public void testBuild_subsequentModificationOfBuilder() throws Exception {
		List<NamespaceElement> elements = populate(4, 2);

		Collection<NamespaceElement> builder = builderCreate();
		builder.addAll(elements);

		Object namespaceList1 = build(builder);

		assertEquals(elements, getList(namespaceList1, null));
		assertEquals(elements.subList(0, 4), getList(namespaceList1, "ns-0"));
		assertEquals(elements.subList(4, 8), getList(namespaceList1, "ns-1"));

		List<NamespaceElement> additionalElements = populate(1, 3);
		builder.addAll(additionalElements);

		List<NamespaceElement> newElements = new ArrayList<>(elements);

		newElements.add(8, additionalElements.get(2));
		newElements.add(8, additionalElements.get(1));
		newElements.add(4, additionalElements.get(0));

		// assert the first list build is not modified
		assertEquals(elements, getList(namespaceList1, null));
		assertEquals(elements.subList(0, 4), getList(namespaceList1, "ns-0"));
		assertEquals(elements.subList(4, 8), getList(namespaceList1, "ns-1"));

		// asser the new content of the builder is as expected
		assertStricEqualContent(builder, newElements);
	}

	// --- utility methods ---

	private static void assertStricEqualContent(Collection<NamespaceElement> builder,
			List<NamespaceElement> expectedElements) throws Exception {
		assertStricEqualContent(builder, expectedElements, NamespaceElement::getNamespace);
	}

	private static <E> void assertStricEqualContent(Collection<E> builder, List<E> expectedElements,
			Function<E, String> getNamespace) throws Exception {
		// test all properties of the builder and its build list in order to ensure they
		// are updated correctly

		assertIterationOrderEquals(expectedElements, builder);
		assertEquals(expectedElements.size(), builder.size());

		Map<String, List<E>> namespaceElements = getNamespaceElements(expectedElements, getNamespace);
		Object namespaceList = build(builder);

		assertEquals(expectedElements.size(), getList(namespaceList, null).size());

		for (Entry<String, List<E>> entry : namespaceElements.entrySet()) {
			String namespace = entry.getKey();
			List<E> elements = entry.getValue();
			assertEquals(elements, getList(namespaceList, namespace));
			assertEquals(elements, builderGetNamespaceElements(builder, namespace));
		}

		assertEquals(expectedElements, getList(namespaceList, null));
	}

	private static void assertEqualContent(Collection<NamespaceElement> builder,
			List<NamespaceElement> expectedElements) throws Exception {
		// test all properties of the builder and its build list in order to ensure they
		// are updated correctly

		assertContentEquals(expectedElements, builder);
		assertEquals(expectedElements.size(), builder.size());

		Map<String, List<NamespaceElement>> namespaceElements = getNamespaceElements(expectedElements,
				NamespaceElement::getNamespace);
		Object namespaceList = build(builder);

		assertEquals(expectedElements.size(), getList(namespaceList, null).size());

		for (Entry<String, List<NamespaceElement>> entry : namespaceElements.entrySet()) {
			String namespace = entry.getKey();
			List<NamespaceElement> elements = entry.getValue();
			assertContentEquals(elements, getList(namespaceList, namespace));
			assertContentEquals(elements, builderGetNamespaceElements(builder, namespace));
		}

		assertContentEquals(expectedElements, getList(namespaceList, null));
	}

	private static <E> void assertIterationOrderEquals(Collection<E> expected, Collection<E> actual) {
		// instead of comparing the iterators, simply compare List-copies. They reflect
		// the iteration order.
		assertEquals(new ArrayList<>(expected), new ArrayList<>(actual));
	}

	private static <E> void assertContentEquals(List<E> expected, Collection<E> actual) {
		assertEquals(new HashSet<>(expected), new HashSet<>(actual));
	}

	private static <E> Map<String, List<E>> getNamespaceElements(List<E> elements, Function<E, String> getNamespace) {
		Map<String, List<E>> namespaceElements = new HashMap<>();
		for (E element : elements) {
			namespaceElements.computeIfAbsent(getNamespace.apply(element), n -> new ArrayList<>()).add(element);
		}
		return namespaceElements;
	}
}
