/*
 * Copyright 2012-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.devtools.restart.classloader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests for {@link RestartClassLoader}.
 *
 * @author Phillip Webb
 */
@SuppressWarnings("resource")
public class RestartClassLoaderTests {

	private static final String PACKAGE = RestartClassLoaderTests.class.getPackage()
			.getName();

	private static final String PACKAGE_PATH = PACKAGE.replace('.', '/');

	private static final Charset UTF_8 = Charset.forName("UTF-8");

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	@Rule
	public TemporaryFolder temp = new TemporaryFolder();

	private File sampleJarFile;

	private URLClassLoader parentClassLoader;

	private ClassLoaderFiles updatedFiles;

	private RestartClassLoader reloadClassLoader;

	@Before
	public void setup() throws Exception {
		this.sampleJarFile = createSampleJarFile();
		URL url = this.sampleJarFile.toURI().toURL();
		ClassLoader classLoader = getClass().getClassLoader();
		URL[] urls = new URL[] { url };
		this.parentClassLoader = new URLClassLoader(urls, classLoader);
		this.updatedFiles = new ClassLoaderFiles();
		this.reloadClassLoader = new RestartClassLoader(this.parentClassLoader, urls,
				this.updatedFiles);
	}

	private File createSampleJarFile() throws IOException {
		File file = this.temp.newFile("sample.jar");
		JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(file));
		jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.class"));
		StreamUtils.copy(getClass().getResourceAsStream("Sample.class"), jarOutputStream);
		jarOutputStream.closeEntry();
		jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.txt"));
		StreamUtils.copy("fromchild", UTF_8, jarOutputStream);
		jarOutputStream.closeEntry();
		jarOutputStream.close();
		return file;
	}

	@Test
	public void parentMustNotBeNull() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("Parent must not be null");
		new RestartClassLoader(null, new URL[] {});
	}

	@Test
	public void updatedFilesMustNotBeNull() throws Exception {
		this.thrown.expect(IllegalArgumentException.class);
		this.thrown.expectMessage("UpdatedFiles must not be null");
		new RestartClassLoader(this.parentClassLoader, new URL[] {}, null);
	}

	@Test
	public void getResourceFromReloadableUrl() throws Exception {
		String content = readString(
				this.reloadClassLoader.getResourceAsStream(PACKAGE_PATH + "/Sample.txt"));
		assertThat(content).startsWith("fromchild");
	}

	@Test
	public void getResourceFromParent() throws Exception {
		String content = readString(
				this.reloadClassLoader.getResourceAsStream(PACKAGE_PATH + "/Parent.txt"));
		assertThat(content).startsWith("fromparent");
	}

	@Test
	public void getResourcesFiltersDuplicates() throws Exception {
		List<URL> resources = toList(
				this.reloadClassLoader.getResources(PACKAGE_PATH + "/Sample.txt"));
		assertThat(resources.size()).isEqualTo(1);
	}

	@Test
	public void loadClassFromReloadableUrl() throws Exception {
		Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
		assertThat(loaded.getClassLoader()).isEqualTo(this.reloadClassLoader);
	}

	@Test
	public void loadClassFromParent() throws Exception {
		Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent");
		assertThat(loaded.getClassLoader()).isEqualTo(getClass().getClassLoader());
	}

	@Test
	public void getDeletedResource() throws Exception {
		String name = PACKAGE_PATH + "/Sample.txt";
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
		assertThat(this.reloadClassLoader.getResource(name)).isEqualTo(null);
	}

	@Test
	public void getDeletedResourceAsStream() throws Exception {
		String name = PACKAGE_PATH + "/Sample.txt";
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
		assertThat(this.reloadClassLoader.getResourceAsStream(name)).isEqualTo(null);
	}

	@Test
	public void getUpdatedResource() throws Exception {
		String name = PACKAGE_PATH + "/Sample.txt";
		byte[] bytes = "abc".getBytes();
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
		URL resource = this.reloadClassLoader.getResource(name);
		assertThat(FileCopyUtils.copyToByteArray(resource.openStream())).isEqualTo(bytes);
	}

	@Test
	public void getResourcesWithDeleted() throws Exception {
		String name = PACKAGE_PATH + "/Sample.txt";
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
		List<URL> resources = toList(this.reloadClassLoader.getResources(name));
		assertThat(resources.size()).isEqualTo(0);
	}

	@Test
	public void getResourcesWithUpdated() throws Exception {
		String name = PACKAGE_PATH + "/Sample.txt";
		byte[] bytes = "abc".getBytes();
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
		List<URL> resources = toList(this.reloadClassLoader.getResources(name));
		assertThat(FileCopyUtils.copyToByteArray(resources.get(0).openStream()))
				.isEqualTo(bytes);
	}

	@Test
	public void getDeletedClass() throws Exception {
		String name = PACKAGE_PATH + "/Sample.class";
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
		this.thrown.expect(ClassNotFoundException.class);
		this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
	}

	@Test
	public void getUpdatedClass() throws Exception {
		String name = PACKAGE_PATH + "/Sample.class";
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, new byte[10]));
		this.thrown.expect(ClassFormatError.class);
		this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
	}

	@Test
	public void getAddedClass() throws Exception {
		String name = PACKAGE_PATH + "/SampleParent.class";
		byte[] bytes = FileCopyUtils
				.copyToByteArray(getClass().getResourceAsStream("SampleParent.class"));
		this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.ADDED, bytes));
		Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent");
		assertThat(loaded.getClassLoader()).isEqualTo(this.reloadClassLoader);
	}

	private String readString(InputStream in) throws IOException {
		return new String(FileCopyUtils.copyToByteArray(in));
	}

	private <T> List<T> toList(Enumeration<T> enumeration) {
		List<T> list = new ArrayList<>();
		if (enumeration != null) {
			while (enumeration.hasMoreElements()) {
				list.add(enumeration.nextElement());
			}
		}
		return list;
	}

}
