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

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.catalina.Container;
import org.apache.catalina.core.StandardWrapper;
import org.apache.jasper.EmbeddedServletOptions;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;

import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.boot.devtools.restart.FailureHandler;
import org.springframework.boot.devtools.restart.MockRestartInitializer;
import org.springframework.boot.devtools.restart.MockRestarter;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.SocketUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;

/**
 * Tests for {@link LocalDevToolsAutoConfiguration}.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @author Vladimir Tsanev
 */
public class LocalDevToolsAutoConfigurationTests {

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

	@Rule
	public MockRestarter mockRestarter = new MockRestarter();

	private int liveReloadPort = SocketUtils.findAvailableTcpPort();

	private ConfigurableApplicationContext context;

	@After
	public void cleanup() {
		if (this.context != null) {
			this.context.close();
		}
	}

	@Test
	public void thymeleafCacheIsFalse() throws Exception {
		this.context = initializeAndRun(Config.class);
		SpringResourceTemplateResolver resolver = this.context
				.getBean(SpringResourceTemplateResolver.class);
		assertThat(resolver.isCacheable()).isFalse();
	}

	@Test
	public void defaultPropertyCanBeOverriddenFromCommandLine() throws Exception {
		this.context = initializeAndRun(Config.class, "--spring.thymeleaf.cache=true");
		SpringResourceTemplateResolver resolver = this.context
				.getBean(SpringResourceTemplateResolver.class);
		assertThat(resolver.isCacheable()).isTrue();
	}

	@Test
	public void defaultPropertyCanBeOverriddenFromUserHomeProperties() throws Exception {
		String userHome = System.getProperty("user.home");
		System.setProperty("user.home",
				new File("src/test/resources/user-home").getAbsolutePath());
		try {
			this.context = initializeAndRun(Config.class);
			SpringResourceTemplateResolver resolver = this.context
					.getBean(SpringResourceTemplateResolver.class);
			assertThat(resolver.isCacheable()).isTrue();
		}
		finally {
			System.setProperty("user.home", userHome);
		}
	}

	@Test
	public void resourceCachePeriodIsZero() throws Exception {
		this.context = initializeAndRun(WebResourcesConfig.class);
		ResourceProperties properties = this.context.getBean(ResourceProperties.class);
		assertThat(properties.getCachePeriod()).isEqualTo(0);
	}

	@Test
	public void liveReloadServer() throws Exception {
		this.context = initializeAndRun(Config.class);
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
		assertThat(server.isStarted()).isTrue();
	}

	@Test
	public void liveReloadTriggeredOnContextRefresh() throws Exception {
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
		reset(server);
		this.context.publishEvent(new ContextRefreshedEvent(this.context));
		verify(server).triggerReload();
	}

	@Test
	public void liveReloadTriggeredOnClassPathChangeWithoutRestart() throws Exception {
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
		reset(server);
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
				Collections.<ChangedFiles>emptySet(), false);
		this.context.publishEvent(event);
		verify(server).triggerReload();
	}

	@Test
	public void liveReloadNotTriggeredOnClassPathChangeWithRestart() throws Exception {
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
		reset(server);
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
				Collections.<ChangedFiles>emptySet(), true);
		this.context.publishEvent(event);
		verify(server, never()).triggerReload();
	}

	@Test
	public void liveReloadDisabled() throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("spring.devtools.livereload.enabled", false);
		this.context = initializeAndRun(Config.class, properties);
		this.thrown.expect(NoSuchBeanDefinitionException.class);
		this.context.getBean(OptionalLiveReloadServer.class);
	}

	@Test
	public void restartTriggeredOnClassPathChangeWithRestart() throws Exception {
		this.context = initializeAndRun(Config.class);
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
				Collections.<ChangedFiles>emptySet(), true);
		this.context.publishEvent(event);
		verify(this.mockRestarter.getMock()).restart(any(FailureHandler.class));
	}

	@Test
	public void restartNotTriggeredOnClassPathChangeWithRestart() throws Exception {
		this.context = initializeAndRun(Config.class);
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
				Collections.<ChangedFiles>emptySet(), false);
		this.context.publishEvent(event);
		verify(this.mockRestarter.getMock(), never()).restart();
	}

	@Test
	public void restartWatchingClassPath() throws Exception {
		this.context = initializeAndRun(Config.class);
		ClassPathFileSystemWatcher watcher = this.context
				.getBean(ClassPathFileSystemWatcher.class);
		assertThat(watcher).isNotNull();
	}

	@Test
	public void restartDisabled() throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("spring.devtools.restart.enabled", false);
		this.context = initializeAndRun(Config.class, properties);
		this.thrown.expect(NoSuchBeanDefinitionException.class);
		this.context.getBean(ClassPathFileSystemWatcher.class);
	}

	@Test
	public void restartWithTriggerFile() throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("spring.devtools.restart.trigger-file", "somefile.txt");
		this.context = initializeAndRun(Config.class, properties);
		ClassPathFileSystemWatcher classPathWatcher = this.context
				.getBean(ClassPathFileSystemWatcher.class);
		Object watcher = ReflectionTestUtils.getField(classPathWatcher,
				"fileSystemWatcher");
		Object filter = ReflectionTestUtils.getField(watcher, "triggerFilter");
		assertThat(filter).isInstanceOf(TriggerFileFilter.class);
	}

	@Test
	public void watchingAdditionalPaths() throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("spring.devtools.restart.additional-paths",
				"src/main/java,src/test/java");
		this.context = initializeAndRun(Config.class, properties);
		ClassPathFileSystemWatcher classPathWatcher = this.context
				.getBean(ClassPathFileSystemWatcher.class);
		Object watcher = ReflectionTestUtils.getField(classPathWatcher,
				"fileSystemWatcher");
		@SuppressWarnings("unchecked")
		Map<File, Object> folders = (Map<File, Object>) ReflectionTestUtils
				.getField(watcher, "folders");
		assertThat(folders).hasSize(2)
				.containsKey(new File("src/main/java").getAbsoluteFile())
				.containsKey(new File("src/test/java").getAbsoluteFile());
	}

	@Test
	public void devToolsSwitchesJspServletToDevelopmentMode() {
		this.context = initializeAndRun(Config.class);
		TomcatWebServer tomcatContainer = (TomcatWebServer) ((ServletWebServerApplicationContext) this.context)
				.getWebServer();
		Container context = tomcatContainer.getTomcat().getHost().findChildren()[0];
		StandardWrapper jspServletWrapper = (StandardWrapper) context.findChild("jsp");
		EmbeddedServletOptions options = (EmbeddedServletOptions) ReflectionTestUtils
				.getField(jspServletWrapper.getServlet(), "options");
		assertThat(options.getDevelopment()).isEqualTo(true);
	}

	private ConfigurableApplicationContext initializeAndRun(Class<?> config,
			String... args) {
		return initializeAndRun(config, Collections.<String, Object>emptyMap(), args);
	}

	private ConfigurableApplicationContext initializeAndRun(Class<?> config,
			Map<String, Object> properties, String... args) {
		Restarter.initialize(new String[0], false, new MockRestartInitializer(), false);
		SpringApplication application = new SpringApplication(config);
		application.setDefaultProperties(getDefaultProperties(properties));
		ConfigurableApplicationContext context = application.run(args);
		return context;
	}

	private Map<String, Object> getDefaultProperties(
			Map<String, Object> specifiedProperties) {
		Map<String, Object> properties = new HashMap<>();
		properties.put("spring.thymeleaf.check-template-location", false);
		properties.put("spring.devtools.livereload.port", this.liveReloadPort);
		properties.put("server.port", 0);
		properties.putAll(specifiedProperties);
		return properties;
	}

	@Configuration
	@Import({ ServletWebServerFactoryAutoConfiguration.class,
			LocalDevToolsAutoConfiguration.class, ThymeleafAutoConfiguration.class })
	public static class Config {

	}

	@Configuration
	@Import({ ServletWebServerFactoryAutoConfiguration.class,
			LocalDevToolsAutoConfiguration.class, ThymeleafAutoConfiguration.class })
	public static class ConfigWithMockLiveReload {

		@Bean
		public LiveReloadServer liveReloadServer() {
			return mock(LiveReloadServer.class);
		}

	}

	@Configuration
	@Import({ ServletWebServerFactoryAutoConfiguration.class,
			LocalDevToolsAutoConfiguration.class, ResourceProperties.class })
	public static class WebResourcesConfig {

	}

	@Configuration
	public static class SessionRedisTemplateConfig {

		@Bean
		public RedisTemplate<Object, Object> sessionRedisTemplate() {
			RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
			redisTemplate.setConnectionFactory(mock(RedisConnectionFactory.class));
			return redisTemplate;
		}

	}

}
