/*
 * Copyright 2015-2024 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.engine.support;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import org.junit.internal.AssumptionViolatedException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.fixtures.TrackLogRecords;
import org.junit.jupiter.api.function.Executable;
import org.junit.platform.commons.logging.LogRecordListener;
import org.junit.platform.commons.util.ReflectionUtils;

/**
 * Unit tests for {@link OpenTest4JAndJUnit4AwareThrowableCollector}.
 *
 * @since 5.5.2
 */
@TrackLogRecords
class OpenTest4JAndJUnit4AwareThrowableCollectorTests {

	@Test
	void simulateJUnit4NotInTheClasspath(LogRecordListener listener) throws Throwable {
		TestClassLoader classLoader = new TestClassLoader(true, false);

		doWithCustomClassLoader(classLoader, () -> {
			// Ensure that our custom ClassLoader actually throws a ClassNotFoundException
			// when attempting to load the AssumptionViolatedException class.
			assertThrows(ClassNotFoundException.class,
				() -> ReflectionUtils.tryToLoadClass(AssumptionViolatedException.class.getName()).get());

			Class<?> clazz = classLoader.loadClass(OpenTest4JAndJUnit4AwareThrowableCollector.class.getName());
			assertNotNull(ReflectionUtils.newInstance(clazz));

			// @formatter:off
			assertThat(listener.stream(Level.FINE).map(LogRecord::getMessage).findFirst().orElse("<not found>"))
				.isEqualTo(
					"Failed to load class org.junit.internal.AssumptionViolatedException: " +
					"only supporting org.opentest4j.TestAbortedException for aborted execution.");
			// @formatter:on
		});
	}

	@Test
	void simulateHamcrestNotInTheClasspath(LogRecordListener listener) throws Throwable {
		TestClassLoader classLoader = new TestClassLoader(false, true);

		doWithCustomClassLoader(classLoader, () -> {
			// Ensure that our custom ClassLoader actually throws a NoClassDefFoundError
			// when attempting to load the AssumptionViolatedException class.
			assertThrows(NoClassDefFoundError.class,
				() -> ReflectionUtils.tryToLoadClass(AssumptionViolatedException.class.getName()).get());

			Class<?> clazz = classLoader.loadClass(OpenTest4JAndJUnit4AwareThrowableCollector.class.getName());
			assertNotNull(ReflectionUtils.newInstance(clazz));

			// @formatter:off
			assertThat(listener.stream(Level.FINE).map(LogRecord::getMessage).findFirst().orElse("<not found>"))
				.isEqualTo(
					"Failed to load class org.junit.internal.AssumptionViolatedException: " +
					"only supporting org.opentest4j.TestAbortedException for aborted execution. " +
					"Note that org.junit.internal.AssumptionViolatedException requires that Hamcrest is on the classpath.");
			// @formatter:on
		});
	}

	private void doWithCustomClassLoader(ClassLoader classLoader, Executable executable) throws Throwable {
		ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
		try {
			// We have to set our custom ClassLoader as the TCCL so that
			// ReflectionUtils uses it (indirectly via ClassLoaderUtils).
			Thread.currentThread().setContextClassLoader(classLoader);

			executable.execute();
		}
		finally {
			Thread.currentThread().setContextClassLoader(originalClassLoader);
		}
	}

	private static class TestClassLoader extends URLClassLoader {

		private static final URL[] CLASSPATH_URLS = new URL[] {
				OpenTest4JAndJUnit4AwareThrowableCollector.class.getProtectionDomain().getCodeSource().getLocation() };

		private final boolean simulateJUnit4Missing;
		private final boolean simulateHamcrestMissing;

		public TestClassLoader(boolean simulateJUnit4Missing, boolean simulateHamcrestMissing) {
			super(CLASSPATH_URLS, getSystemClassLoader());
			this.simulateJUnit4Missing = simulateJUnit4Missing;
			this.simulateHamcrestMissing = simulateHamcrestMissing;
		}

		@Override
		public Class<?> loadClass(String name) throws ClassNotFoundException {

			// Load a new instance of the OpenTest4JAndJUnit4AwareThrowableCollector class
			if (name.equals(OpenTest4JAndJUnit4AwareThrowableCollector.class.getName())) {
				return findClass(name);
			}

			// Simulate that JUnit 4 is not in the classpath when loading AssumptionViolatedException
			if (this.simulateJUnit4Missing && name.equals(AssumptionViolatedException.class.getName())) {
				throw new ClassNotFoundException(AssumptionViolatedException.class.getName());
			}

			// Simulate that Hamcrest is not in the classpath when loading AssumptionViolatedException
			if (this.simulateHamcrestMissing && name.equals(AssumptionViolatedException.class.getName())) {
				throw new NoClassDefFoundError("org/hamcrest/SelfDescribing");
			}

			// Else
			return super.loadClass(name);
		}

	}

}
