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

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import groovy.text.Template;
import groovy.text.TemplateEngine;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrint;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SpringBootHypermediaApplication.class, loader = SpringBootContextLoader.class)
@WebAppConfiguration
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
		"endpoints.health.sensitive=true", "endpoints.actuator.enabled=false",
		"management.security.enabled=false" })
@DirtiesContext
@AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR)
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
public class EndpointDocumentation {

	static final String RESTDOCS_OUTPUT_DIR = "target/generated-snippets";

	static final File LOG_FILE = new File("target/logs/spring.log");

	private static final Set<String> SKIPPED = Collections
			.<String>unmodifiableSet(new HashSet<>(
					Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents")));

	@Autowired
	private MvcEndpoints mvcEndpoints;

	@Autowired
	private TemplateEngine templates;

	@Autowired
	private MockMvc mockMvc;

	@BeforeClass
	public static void clearLog() {
		LOG_FILE.delete();
	}

	@Test
	public void logfile() throws Exception {
		this.mockMvc.perform(get("/application/logfile").accept(MediaType.TEXT_PLAIN))
				.andExpect(status().isOk()).andDo(document("logfile"));
	}

	@Test
	public void partialLogfile() throws Exception {
		FileCopyUtils.copy(getClass().getResourceAsStream("log.txt"),
				new FileOutputStream(LOG_FILE));
		this.mockMvc
				.perform(get("/application/logfile").accept(MediaType.TEXT_PLAIN)
						.header(HttpHeaders.RANGE, "bytes=0-1024"))
				.andExpect(status().isPartialContent())
				.andDo(document("partial-logfile"));
	}

	@Test
	public void singleLogger() throws Exception {
		this.mockMvc
				.perform(get("/application/loggers/org.springframework.boot")
						.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk()).andDo(document("single-logger"));
	}

	@Test
	public void setLogger() throws Exception {
		this.mockMvc
				.perform(post("/application/loggers/org.springframework.boot")
						.contentType(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON)
						.content("{\"configuredLevel\": \"DEBUG\"}"))
				.andExpect(status().isOk()).andDo(document("set-logger"));
	}

	@Test
	public void auditEvents() throws Exception {
		this.mockMvc
				.perform(get("/application/auditevents").param("after", "2016-11-01T10:00:00+0000")
						.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
				.andExpect(status().isOk()).andDo(document("auditevents"));
	}

	@Test
	public void auditEventsByPrincipal() throws Exception {
		this.mockMvc
				.perform(get("/application/auditevents").param("principal", "admin")
						.param("after", "2016-11-01T10:00:00+0000")
						.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
				.andExpect(status().isOk())
				.andDo(document("auditevents/filter-by-principal"));
	}

	@Test
	public void auditEventsByPrincipalAndType() throws Exception {
		this.mockMvc
				.perform(get("/application/auditevents").param("principal", "admin")
						.param("after", "2016-11-01T10:00:00+0000")
						.param("type", "AUTHENTICATION_SUCCESS")
						.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
				.andExpect(status().isOk())
				.andDo(document("auditevents/filter-by-principal-and-type"));
	}

	@Test
	public void endpoints() throws Exception {
		final File docs = new File("src/main/asciidoc");
		final Map<String, Object> model = new LinkedHashMap<>();
		final List<EndpointDoc> endpoints = new ArrayList<>();
		model.put("endpoints", endpoints);
		for (MvcEndpoint endpoint : getEndpoints()) {
			final String endpointPath = (StringUtils.hasText(endpoint.getPath())
					? endpoint.getPath() : "/");
			if (!SKIPPED.contains(endpointPath)) {
				String output = endpointPath.substring(1);
				output = output.length() > 0 ? output : "./";
				this.mockMvc
						.perform(get("/application" + endpointPath)
								.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
						.andExpect(status().isOk()).andDo(document(output))
						.andDo(new ResultHandler() {

							@Override
							public void handle(MvcResult mvcResult) throws Exception {
								EndpointDoc endpoint = new EndpointDoc(docs,
										endpointPath);
								endpoints.add(endpoint);
							}

						});
			}
		}
		File file = new File(RESTDOCS_OUTPUT_DIR + "/endpoints.adoc");
		file.getParentFile().mkdirs();
		PrintWriter writer = new PrintWriter(file, "UTF-8");
		try {
			Template template = this.templates.createTemplate(
					new File("src/restdoc/resources/templates/endpoints.adoc.tpl"));
			template.make(model).writeTo(writer);
		}
		finally {
			writer.close();
		}
	}

	private Collection<? extends MvcEndpoint> getEndpoints() {
		List<? extends MvcEndpoint> endpoints = new ArrayList<>(
				this.mvcEndpoints.getEndpoints());
		Collections.sort(endpoints, new Comparator<MvcEndpoint>() {
			@Override
			public int compare(MvcEndpoint o1, MvcEndpoint o2) {
				return o1.getPath().compareTo(o2.getPath());
			}
		});
		return endpoints;
	}

	public static class EndpointDoc {

		private String path;

		private String custom;

		private String title;

		public EndpointDoc(File rootDir, String path) {
			this.title = path;
			this.path = path.equals("/") ? "" : path;
			String custom = path.substring(1) + ".adoc";
			if (new File(rootDir, custom).exists()) {
				this.custom = custom;
			}
		}

		public String getTitle() {
			return this.title;
		}

		public String getPath() {
			return this.path;
		}

		public String getCustom() {
			return this.custom;
		}

	}

}
