/*
 * Copyright 2015-2019 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v2.0 which
 * accompanies this distribution and is available at
 *
 * https://www.eclipse.org/legal/epl-v20.html
 */

package org.junit.jupiter.api;

import static org.junit.jupiter.api.AssertionUtils.buildPrefix;
import static org.junit.jupiter.api.AssertionUtils.fail;
import static org.junit.jupiter.api.AssertionUtils.formatIndexes;
import static org.junit.jupiter.api.AssertionUtils.formatValues;
import static org.junit.jupiter.api.AssertionUtils.nullSafeGet;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

/**
 * {@code AssertIterable} is a collection of utility methods that support asserting
 * Iterable equality in tests.
 *
 * @since 5.0
 */
class AssertIterableEquals {

	private AssertIterableEquals() {
		/* no-op */
	}

	static void assertIterableEquals(Iterable<?> expected, Iterable<?> actual) {
		assertIterableEquals(expected, actual, (String) null);
	}

	static void assertIterableEquals(Iterable<?> expected, Iterable<?> actual, String message) {
		assertIterableEquals(expected, actual, new ArrayDeque<>(), message);
	}

	static void assertIterableEquals(Iterable<?> expected, Iterable<?> actual, Supplier<String> messageSupplier) {
		assertIterableEquals(expected, actual, new ArrayDeque<>(), messageSupplier);
	}

	private static void assertIterableEquals(Iterable<?> expected, Iterable<?> actual, Deque<Integer> indexes,
			Object messageOrSupplier) {

		if (expected == actual) {
			return;
		}
		assertIterablesNotNull(expected, actual, indexes, messageOrSupplier);

		Iterator<?> expectedIterator = expected.iterator();
		Iterator<?> actualIterator = actual.iterator();

		int processed = 0;
		while (expectedIterator.hasNext() && actualIterator.hasNext()) {
			processed++;
			Object expectedElement = expectedIterator.next();
			Object actualElement = actualIterator.next();

			if (expectedElement == actualElement) {
				continue;
			}

			indexes.addLast(processed - 1);
			assertIterableElementsEqual(expectedElement, actualElement, indexes, messageOrSupplier);
			indexes.removeLast();
		}

		assertIteratorsAreEmpty(expectedIterator, actualIterator, processed, indexes, messageOrSupplier);
	}

	private static void assertIterableElementsEqual(Object expected, Object actual, Deque<Integer> indexes,
			Object messageOrSupplier) {
		if (expected instanceof Iterable && actual instanceof Iterable) {
			assertIterableEquals((Iterable<?>) expected, (Iterable<?>) actual, indexes, messageOrSupplier);
		}
		else if (!Objects.equals(expected, actual)) {
			assertIterablesNotNull(expected, actual, indexes, messageOrSupplier);
			failIterablesNotEqual(expected, actual, indexes, messageOrSupplier);
		}
	}

	private static void assertIterablesNotNull(Object expected, Object actual, Deque<Integer> indexes,
			Object messageOrSupplier) {

		if (expected == null) {
			failExpectedIterableIsNull(indexes, messageOrSupplier);
		}
		if (actual == null) {
			failActualIterableIsNull(indexes, messageOrSupplier);
		}
	}

	private static void failExpectedIterableIsNull(Deque<Integer> indexes, Object messageOrSupplier) {
		fail(buildPrefix(nullSafeGet(messageOrSupplier)) + "expected iterable was <null>" + formatIndexes(indexes));
	}

	private static void failActualIterableIsNull(Deque<Integer> indexes, Object messageOrSupplier) {
		fail(buildPrefix(nullSafeGet(messageOrSupplier)) + "actual iterable was <null>" + formatIndexes(indexes));
	}

	private static void assertIteratorsAreEmpty(Iterator<?> expected, Iterator<?> actual, int processed,
			Deque<Integer> indexes, Object messageOrSupplier) {

		if (expected.hasNext() || actual.hasNext()) {
			AtomicInteger expectedCount = new AtomicInteger(processed);
			expected.forEachRemaining(e -> expectedCount.incrementAndGet());

			AtomicInteger actualCount = new AtomicInteger(processed);
			actual.forEachRemaining(e -> actualCount.incrementAndGet());

			String prefix = buildPrefix(nullSafeGet(messageOrSupplier));
			String message = "iterable lengths differ" + formatIndexes(indexes) + ", expected: <" + expectedCount.get()
					+ "> but was: <" + actualCount.get() + ">";
			fail(prefix + message);
		}
	}

	private static void failIterablesNotEqual(Object expected, Object actual, Deque<Integer> indexes,
			Object messageOrSupplier) {

		String prefix = buildPrefix(nullSafeGet(messageOrSupplier));
		String message = "iterable contents differ" + formatIndexes(indexes) + ", " + formatValues(expected, actual);
		fail(prefix + message);
	}

}
