/*
 * 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.tests;

import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.FixedValue;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * Integration tests for DevTools.
 *
 * @author Andy Wilkinson
 */
@RunWith(Parameterized.class)
public class DevToolsIntegrationTests {

	private LaunchedApplication launchedApplication;

	private final File serverPortFile = new File("target/server.port");

	private final ApplicationLauncher applicationLauncher;

	@Rule
	public JvmLauncher javaLauncher = new JvmLauncher();

	@Parameters(name = "{0}")
	public static Object[] parameters() {
		return new Object[] { new Object[] { new LocalApplicationLauncher() },
				new Object[] { new ExplodedRemoteApplicationLauncher() },
				new Object[] { new JarFileRemoteApplicationLauncher() } };
	}

	public DevToolsIntegrationTests(ApplicationLauncher applicationLauncher) {
		this.applicationLauncher = applicationLauncher;
	}

	@Before
	public void launchApplication() throws Exception {
		this.serverPortFile.delete();
		this.launchedApplication = this.applicationLauncher
				.launchApplication(this.javaLauncher);
	}

	@After
	public void stopApplication() {
		this.launchedApplication.stop();
	}

	@Test
	public void addARequestMappingToAnExistingController() throws Exception {
		TestRestTemplate template = new TestRestTemplate();
		String urlBase = "http://localhost:" + awaitServerPort();
		assertThat(template.getForObject(urlBase + "/one", String.class))
				.isEqualTo("one");
		assertThat(template.getForEntity(urlBase + "/two", String.class).getStatusCode())
				.isEqualTo(HttpStatus.NOT_FOUND);
		controller("com.example.ControllerOne").withRequestMapping("one")
				.withRequestMapping("two").build();
		assertThat(template.getForObject(urlBase + "/one", String.class))
				.isEqualTo("one");
		assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/two",
				String.class)).isEqualTo("two");
	}

	@Test
	public void removeARequestMappingFromAnExistingController() throws Exception {
		TestRestTemplate template = new TestRestTemplate();
		assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/one",
				String.class)).isEqualTo("one");
		controller("com.example.ControllerOne").build();
		assertThat(template.getForEntity("http://localhost:" + awaitServerPort() + "/one",
				String.class).getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
	}

	@Test
	public void createAController() throws Exception {
		TestRestTemplate template = new TestRestTemplate();
		String urlBase = "http://localhost:" + awaitServerPort();
		assertThat(template.getForObject(urlBase + "/one", String.class))
				.isEqualTo("one");
		assertThat(template.getForEntity(urlBase + "/two", String.class).getStatusCode())
				.isEqualTo(HttpStatus.NOT_FOUND);
		controller("com.example.ControllerTwo").withRequestMapping("two").build();
		assertThat(template.getForObject(urlBase + "/one", String.class))
				.isEqualTo("one");
		assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/two",
				String.class)).isEqualTo("two");

	}

	@Test
	public void deleteAController() throws Exception {
		TestRestTemplate template = new TestRestTemplate();
		assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/one",
				String.class)).isEqualTo("one");
		assertThat(new File(this.launchedApplication.getClassesDirectory(),
				"com/example/ControllerOne.class").delete()).isTrue();
		assertThat(template.getForEntity("http://localhost:" + awaitServerPort() + "/one",
				String.class).getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);

	}

	private int awaitServerPort() throws Exception {
		long end = System.currentTimeMillis() + 30000;
		while (this.serverPortFile.length() == 0) {
			if (System.currentTimeMillis() > end) {
				throw new IllegalStateException(String.format(
						"server.port file was not written within 30 seconds. "
								+ "Application output:%n%s",
						FileCopyUtils.copyToString(new FileReader(
								this.launchedApplication.getStandardOut()))));
			}
			Thread.sleep(100);
		}
		FileReader portReader = new FileReader(this.serverPortFile);
		int port = Integer.valueOf(FileCopyUtils.copyToString(portReader));
		this.serverPortFile.delete();
		return port;
	}

	private ControllerBuilder controller(String name) {
		return new ControllerBuilder(name,
				this.launchedApplication.getClassesDirectory());
	}

	private static final class ControllerBuilder {

		private final List<String> mappings = new ArrayList<>();

		private final String name;

		private final File classesDirectory;

		private ControllerBuilder(String name, File classesDirectory) {
			this.name = name;
			this.classesDirectory = classesDirectory;
		}

		public ControllerBuilder withRequestMapping(String mapping) {
			this.mappings.add(mapping);
			return this;
		}

		public void build() throws Exception {
			Builder<Object> builder = new ByteBuddy().subclass(Object.class)
					.name(this.name).annotateType(AnnotationDescription.Builder
							.ofType(RestController.class).build());
			for (String mapping : this.mappings) {
				builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC)
						.intercept(FixedValue.value(mapping)).annotateMethod(
								AnnotationDescription.Builder.ofType(RequestMapping.class)
										.defineArray("value", mapping).build());
			}
			builder.make().saveIn(this.classesDirectory);
		}

	}

}
