/*
 * Copyright 2012-2020 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
 *
 *      https://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.cloudnativebuildpack.build;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;

import org.springframework.boot.cloudnativebuildpack.docker.DockerApi;
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi;
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi;
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi;
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent;
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference;
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
import org.springframework.util.FileCopyUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

/**
 * Tests for {@link Lifecycle}.
 *
 * @author Phillip Webb
 */
class LifecycleTests {

	private TestPrintStream out;

	private DockerApi docker;

	private Lifecycle lifecycle;

	private Map<String, ContainerConfig> configs = new LinkedHashMap<>();

	private Map<String, ContainerContent> content = new LinkedHashMap<>();

	@BeforeEach
	void setup() throws Exception {
		this.out = new TestPrintStream();
		this.docker = mockDockerApi();
		BuildRequest request = getTestRequest();
		this.lifecycle = createLifecycle(request);
	}

	private DockerApi mockDockerApi() {
		DockerApi docker = mock(DockerApi.class);
		ImageApi imageApi = mock(ImageApi.class);
		ContainerApi containerApi = mock(ContainerApi.class);
		VolumeApi volumeApi = mock(VolumeApi.class);
		given(docker.image()).willReturn(imageApi);
		given(docker.container()).willReturn(containerApi);
		given(docker.volume()).willReturn(volumeApi);
		return docker;
	}

	@Test
	void executeExecutesPhases() throws Exception {
		given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
		given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
		this.lifecycle.execute();
		assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
		assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json"));
		assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json"));
		assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
		assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json"));
		assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.json"));
		assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
	}

	@Test
	void executeOnlyUploadsContentOnce() throws Exception {
		given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
		given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
		this.lifecycle.execute();
		assertThat(this.content).hasSize(1);
	}

	@Test
	void executeWhenAleadyRunThrowsException() throws Exception {
		given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
		given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
		this.lifecycle.execute();
		assertThatIllegalStateException().isThrownBy(this.lifecycle::execute)
				.withMessage("Lifecycle has already been executed");
	}

	@Test
	void executeWhenCleanCacheClearsCache() throws Exception {
		given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
		given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
		BuildRequest request = getTestRequest().withCleanCache(true);
		createLifecycle(request).execute();
		VolumeName name = VolumeName.of("pack-cache-b35197ac41ea.build");
		verify(this.docker.volume()).delete(name, true);
	}

	@Test
	void closeClearsVolumes() throws Exception {
		this.lifecycle.close();
		verify(this.docker.volume()).delete(VolumeName.of("pack-layers-aaaaaaaaaa"), true);
		verify(this.docker.volume()).delete(VolumeName.of("pack-app-aaaaaaaaaa"), true);
	}

	private BuildRequest getTestRequest() {
		TarArchive content = mock(TarArchive.class);
		ImageReference name = ImageReference.of("my-application");
		BuildRequest request = BuildRequest.of(name, (owner) -> content);
		return request;
	}

	private Lifecycle createLifecycle(BuildRequest request) throws IOException {
		EphemeralBuilder builder = mockEphemeralBuilder();
		return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"),
				builder);
	}

	private EphemeralBuilder mockEphemeralBuilder() throws IOException {
		EphemeralBuilder builder = mock(EphemeralBuilder.class);
		byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("builder-metadata.json"));
		BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8));
		given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder"));
		given(builder.getBuilderMetadata()).willReturn(metadata);
		return builder;
	}

	private Answer<ContainerReference> answerWithGeneratedContainerId() {
		return (invocation) -> {
			ContainerConfig config = invocation.getArgument(0, ContainerConfig.class);
			ArrayNode command = getCommand(config);
			String name = command.get(0).asText().substring(1).replaceAll("/", "-");
			this.configs.put(name, config);
			if (invocation.getArguments().length > 1) {
				this.content.put(name, invocation.getArgument(1, ContainerContent.class));
			}
			return ContainerReference.of(name);
		};
	}

	private ArrayNode getCommand(ContainerConfig config) throws JsonProcessingException, JsonMappingException {
		JsonNode node = SharedObjectMapper.get().readTree(config.toString());
		return (ArrayNode) node.at("/Cmd");
	}

	private void assertPhaseWasRun(String name, IOConsumer<ContainerConfig> configConsumer) throws IOException {
		ContainerReference containerReference = ContainerReference.of("lifecycle-" + name);
		verify(this.docker.container()).start(containerReference);
		verify(this.docker.container()).logs(eq(containerReference), any());
		verify(this.docker.container()).remove(containerReference, true);
		configConsumer.accept(this.configs.get(containerReference.toString()));
	}

	private IOConsumer<ContainerConfig> withExpectedConfig(String name) {
		return (config) -> {
			InputStream in = getClass().getResourceAsStream(name);
			String json = FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8));
			assertThat(config.toString()).isEqualToIgnoringWhitespace(json);
		};
	}

	static class TestLifecycle extends Lifecycle {

		TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReferece,
				EphemeralBuilder builder) {
			super(log, docker, request, runImageReferece, builder);
		}

		@Override
		protected VolumeName createRandomVolumeName(String prefix) {
			return VolumeName.of(prefix + "aaaaaaaaaa");
		}

	}

	static class TestPrintStream extends PrintStream {

		TestPrintStream() {
			super(new ByteArrayOutputStream());
		}

		@Override
		public String toString() {
			return this.out.toString();
		}

	}

}
