package it.internal.org.springframework.content.rest.controllers;

import java.io.ByteArrayInputStream;
import java.util.Date;
import java.util.UUID;

import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration;
import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner;
import internal.org.springframework.content.rest.support.StoreConfig;
import internal.org.springframework.content.rest.support.TestStore;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.WritableResource;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;

import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach;
import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context;
import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe;
import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.FIt;
import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(Ginkgo4jSpringRunner.class)
@Ginkgo4jConfiguration(threads = 1)
@WebAppConfiguration
@ContextConfiguration(classes = { StoreConfig.class, DelegatingWebMvcConfiguration.class,
		RepositoryRestMvcConfiguration.class, RestConfiguration.class })
@Transactional
@ActiveProfiles("store")
public class StoreRestEndpointsIT {

	@Autowired
	TestStore store;

	@Autowired
	private WebApplicationContext context;

	private MockMvc mvc;

	private String path;
	private String request;

	private LastModifiedDate lastModifiedDate;

	{
		Describe("Store Rest Endpoints", () -> {
			BeforeEach(() -> {
				mvc = MockMvcBuilders.webAppContextSetup(context).build();
			});

			Context("given a root resource path", () -> {
				BeforeEach(() -> {
					path = "/" + UUID.randomUUID() + ".txt";
					request = "/teststore" + path;
				});
				Context("given a GET request to that path", () -> {
					It("should return 404", () -> {
						mvc.perform(get(request)).andExpect(status().isNotFound());
					});
				});
				Context("given a POST to that path with content", () -> {
					It("should set the content and return 201", () -> {

						String content = "New multi-part content";

						mvc.perform(multipart(request).file(new MockMultipartFile("file",
								"test-file.txt", "text/plain", content.getBytes())))
								.andExpect(status().isCreated());

						Resource r = store.getResource(path);
						assertThat(IOUtils.contentEquals(
								new ByteArrayInputStream("New multi-part content".getBytes()),
								r.getInputStream()), is(true));
						assertThat(r.contentLength(), equalTo(Long.valueOf(content.length())));
					});
				});
				Context("given a DELETE request to that path", () -> {
					It("should return a 404", () -> {
						mvc.perform(delete(request)).andExpect(status().isNotFound());
					});
				});
			});

			Context("given a root resource", () -> {
				BeforeEach(() -> {
					path = "/" + UUID.randomUUID() + ".txt";
					request = "/teststore" + path;
					Resource r = store.getResource(path);
					if (r instanceof WritableResource) {
						IOUtils.copy(
								new ByteArrayInputStream("Existing content".getBytes()),
								((WritableResource) r).getOutputStream());
					}

					lastModifiedDate.setMvc(mvc);
					lastModifiedDate.setUrl("/teststore" + path);
					lastModifiedDate.setLastModifiedDate(new Date(r.lastModified()));
					lastModifiedDate.setContent("Existing content");
				});
				It("should return the resource's content", () -> {
					MockHttpServletResponse response = mvc.perform(get(request))
							.andExpect(status().isOk()).andReturn().getResponse();

					assertThat(response, is(not(nullValue())));
					assertThat(response.getContentAsString(), is("Existing content"));

				});
				It("should return a byte range when requested", () -> {
					MockHttpServletResponse response = mvc
							.perform(get(request).header("range", "bytes=9-12"))
							.andExpect(status().isPartialContent()).andReturn()
							.getResponse();

					assertThat(response, is(not(nullValue())));
					assertThat(response.getContentAsString(), is("cont"));
				});
				It("should overwrite the resource's content", () -> {
					mvc.perform(put(request).content("New Existing content")
							.contentType("text/plain")).andExpect(status().isOk());

					Resource r = store.getResource(path);
					assertThat(IOUtils.contentEquals(
							new ByteArrayInputStream("New Existing content".getBytes()),
							r.getInputStream()), is(true));
				});
				Context("a POST to /{store}/{path} with multi-part form-data ", () -> {
					It("should overwrite the content and return 200", () -> {

						String content = "New multi-part content";

						mvc.perform(multipart(request).file(new MockMultipartFile("file",
								"tests-file.txt", "text/plain", content.getBytes())))
								.andExpect(status().isOk());

						Resource r = store.getResource(path);
						assertThat(IOUtils.contentEquals(
								new ByteArrayInputStream(
										"New multi-part content".getBytes()),
								r.getInputStream()), is(true));
						assertThat(r.contentLength(), equalTo(Long.valueOf(content.length())));
					});
				});
				It("should delete the resource", () -> {
					mvc.perform(delete(request)).andExpect(status().isNoContent());

					Resource r = store.getResource(path);
					assertThat(r.exists(), is(false));
				});

				lastModifiedDate = new LastModifiedDate();
			});

			Context("given a nested resource", () -> {
				BeforeEach(() -> {
					path = "/a/b/" + UUID.randomUUID() + ".txt";
					request = "/teststore" + path;
					Resource r = store.getResource(path);
					if (r instanceof WritableResource) {
						IOUtils.copy(
								new ByteArrayInputStream("Existing content".getBytes()),
								((WritableResource) r).getOutputStream());
					}
				});
				It("should return the resource's content", () -> {
					MockHttpServletResponse response = mvc.perform(get(request))
							.andExpect(status().isOk()).andReturn().getResponse();

					assertThat(response, is(not(nullValue())));
					assertThat(response.getContentAsString(), is("Existing content"));

				});
				It("should return a byte range when requested", () -> {
					MockHttpServletResponse response = mvc
							.perform(get(request).header("range", "bytes=9-12"))
							.andExpect(status().isPartialContent()).andReturn()
							.getResponse();

					assertThat(response, is(not(nullValue())));
					assertThat(response.getContentAsString(), is("cont"));
				});
				Context("given a typical browser request", () -> {
					It("should return the resource's content", () -> {
						MockHttpServletResponse response = mvc
								.perform(get(request).accept(new String[] { "text/html",
										"application/xhtml+xml", "application/xml;q=0.9",
										"image/webp", "image/apng", "*/*;q=0.8" }))
								.andExpect(status().isOk()).andReturn().getResponse();

						assertThat(response, is(not(nullValue())));
						assertThat(response.getContentAsString(), is("Existing content"));
					});
				});
				It("should overwrite the resource's content", () -> {
					mvc.perform(put(request).content("New Existing content")
							.contentType("text/plain")).andExpect(status().isOk());

					Resource r = store.getResource(path);
					assertThat(IOUtils.contentEquals(
							new ByteArrayInputStream("New Existing content".getBytes()),
							r.getInputStream()), is(true));
				});
				Context("a POST to /{store}/{path} with multi-part form-data ", () -> {
					It("should overwrite the content and return 200", () -> {

						String content = "New multi-part content";

						mvc.perform(multipart(request).file(new MockMultipartFile("file",
								"tests-file.txt", "text/plain", content.getBytes())))
								.andExpect(status().isOk());

						Resource r = store.getResource(path);
						assertThat(IOUtils.contentEquals(
								new ByteArrayInputStream(
										"New multi-part content".getBytes()),
								r.getInputStream()), is(true));
						assertThat(r.contentLength(), equalTo(Long.valueOf(content.length())));
					});
				});
				It("should delete the resource", () -> {
					mvc.perform(delete(request)).andExpect(status().isNoContent());

					Resource r = store.getResource(path);
					assertThat(r.exists(), is(false));
				});
			});
		});
	}

	@Test
	public void noop() {
	}
}
