package com.github.jenspiegsa.wiremockextension;

import static com.github.jenspiegsa.wiremockextension.ManagedWireMockServer.with;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
import static com.github.tomakehurst.wiremock.client.WireMock.notFound;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.util.Collections.unmodifiableList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.then;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.VerificationException;
import com.github.tomakehurst.wiremock.core.Options;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;

/**
 * @author Jens Piegsa
 */
@DisplayName("WireMock Extension")
@ExtendWith(WireMockExtension.class)
@WireMockSettings(failOnUnmatchedRequests = false)
class WireMockExtensionTest {

	private static final Logger log = Logger.getLogger(WireMockExtensionTest.class.getSimpleName());

	@Nested
	class DefaultConfiguration {

		@InjectServer
		WireMockServer serverMock;

		@Test
		@DisplayName("should start and inject server.")
		void shouldInjectServer() {
			assertThat(serverMock).isNotNull();
			assertThat(serverMock.isRunning()).describedAs("server expected to be running.").isTrue();
		}

		@Test
		@DisplayName("should return status ok.")
		void shouldReturnStatusOk() {
			givenThat(get("/").willReturn(ok()));
			final int port = serverMock.port();
			assertThat(port).isEqualTo(8080);
			final SampleClient sampleClient = new SampleClient("http://localhost:8080/");
			assertThat(sampleClient.isOk()).isTrue();
			sampleClient.close();
		}
	}

	@Nested
	class CustomConfiguration {

		@ConfigureWireMock
		private Options options = wireMockConfig().dynamicPort();

		@InjectServer
		private WireMockServer serverMock;

		@Test
		@DisplayName("should start and inject server.")
		void shouldInjectServer() {
			assertThat(serverMock).isNotNull();
			assertThat(serverMock.isRunning()).describedAs("server expected to be running.").isTrue();
		}

		@Test
		@DisplayName("should return status ok.")
		void shouldReturnStatusOk() {
			assertThat(serverMock.isRunning()).isTrue();
			final int port = serverMock.port();
			log.info(() -> "port: " + port);

			givenThat(get("/").willReturn(ok()));

			final SampleClient client = new SampleClient("http://localhost:" + port + "/");
			log.info(() -> "target: " + client.target.getUri());
			assertThat(client.isOk()).isTrue();
			client.close();
		}
	}

	@Nested
	@DisplayName("Test instance preparation")
	class TestInstancePreparation {

		@Test
		@DisplayName("should raise ExtensionConfigurationException when multiple configs are present.")
		void shouldRaiseExceptionWhenMultipleConfigsArePresent() {

			// given
			final Class<?> testClass = InvalidMultipleOptionsTestCase.class;

			// when
			final TestResults results = launchTests(testClass);

			// then
			then(results.getSummary().getTestsFailedCount())
					.describedAs("test instance preparation expected to fail")
					.isEqualTo(1L);
			then(results.getThrowables()).hasSize(1);
			then(results.getThrowables().get(0))
					.isInstanceOf(ExtensionConfigurationException.class)
					.hasMessageContaining("@ConfigureWireMock");
		}
		
		@Test
		@DisplayName("should raise VerificationException on unmatched request when failOnUnmatchedRequest is true.")
		void shouldRaiseExceptionOnUnmatchedRequestIfConfiguredSo() {

			// given
			final Class<?> testClass = FailOnUnmatchedRequestsTestCase.class;

			// when
			final TestResults results = launchTests(testClass);

			then(results.getSummary().getTestsFailedCount())
					.describedAs("test execution expected to fail")
					.isEqualTo(1L);
			then(results.getThrowables()).hasSize(1);
			then(results.getThrowables().get(0))
					.isInstanceOf(VerificationException.class)
					.hasMessageContaining("unmatched");
		}

		@Test
		@DisplayName("should raise VerificationException on near missed request when failOnUnmatchedRequest is true.")
		void shouldRaiseExceptionOnNearMissedRequestIfConfiguredSo() {

			// given
			final Class<?> testClass = FailOnNearMissedRequestsTestCase.class;

			// when
			final TestResults results = launchTests(testClass);

			then(results.getSummary().getTestsFailedCount())
					.describedAs("test execution expected to fail")
					.isEqualTo(1L);
			then(results.getThrowables()).hasSize(1);
			then(results.getThrowables().get(0))
					.isInstanceOf(VerificationException.class)
					.hasMessageContaining("unmatched")
					.hasMessageContaining("Closest");
		}

		@Test
		@DisplayName("should tolerate unmatched request when failOnUnmatchedRequest is false.")
		void shouldTolerateUnmatchedRequestIfConfiguredSo() {

			// given
			final Class<?> testClass = TolerateUnmatchedRequestsTestCase.class;

			// when
			final TestResults results = launchTests(testClass);

			then(results.getSummary().getTestsFailedCount())
					.describedAs("test execution expected not to fail")
					.isEqualTo(0L);
			then(results.getThrowables()).isEmpty();
		}

		private TestResults launchTests(final Class<?> testClass) {
			final Launcher launcher = LauncherFactory.create();
			final TestResults results = new TestResults();
			launcher.execute(request().selectors(selectClass(testClass)).build(), results);
			return results;
		}
	}

	@Nested
	@DisplayName("Multiple servers")
	class MultipleServers {

		@Managed WireMockServer s1 = with(wireMockConfig().dynamicPort());
		@Managed WireMockServer s2 = with(wireMockConfig().dynamicPort());
		@Managed WireMockServer s3 = with(wireMockConfig().dynamicPort());

		@Test
		@DisplayName("should work.")
		void shouldWork() {
			s1.stubFor(get("/a").willReturn(ok()));
			s2.stubFor(get("/b").willReturn(notFound()));
			s3.stubFor(get("/c").willReturn(ok()));
			then(new SampleClient("http://localhost:" + s1.port() + "/a").isOk()).isTrue();
			then(new SampleClient("http://localhost:" + s2.port() + "/b").isOk()).isFalse();
			then(new SampleClient("http://localhost:" + s3.port() + "/c").isOk()).isTrue();
		}
	}

	static class InvalidMultipleOptionsTestCase extends TestBase {
		@ConfigureWireMock Options o1 = wireMockConfig().dynamicPort();
		@ConfigureWireMock Options o2 = wireMockConfig().dynamicPort();
	}

	@WireMockSettings(failOnUnmatchedRequests = true)
	static class FailOnUnmatchedRequestsTestCase extends TestBase {

		@InjectServer WireMockServer server;
		@ConfigureWireMock Options options = wireMockConfig().dynamicPort();

		@Test
		@SuppressWarnings("JUnitTestMethodWithNoAssertions")
		void shouldFailWhenUnmatchedRequestOccurs() {
			new SampleClient("http://localhost:" + server.port() + "/").isOk();
		}
	}

	@WireMockSettings(failOnUnmatchedRequests = true)
	static class FailOnNearMissedRequestsTestCase extends TestBase {

		@InjectServer WireMockServer server;
		@ConfigureWireMock Options options = wireMockConfig().dynamicPort();

		@Test
		@SuppressWarnings("JUnitTestMethodWithNoAssertions")
		void shouldFailWhenNearMissedRequestOccurs() {
			givenThat(get("/a").willReturn(ok()));
			new SampleClient("http://localhost:" + server.port() + "/").isOk();
		}
	}

	@WireMockSettings(failOnUnmatchedRequests = false)
	static class TolerateUnmatchedRequestsTestCase extends TestBase {

		@InjectServer WireMockServer server;
		@ConfigureWireMock Options options = wireMockConfig().dynamicPort();

		@Test
		@SuppressWarnings("JUnitTestMethodWithNoAssertions")
		void shouldIgnoreUnmatchedRequests() {
			new SampleClient("http://localhost:" + server.port() + "/").isOk();
		}
	}

	@Nested
	@DisplayName("Nested test classes")
	class NestedTestClasses {

		@Managed WireMockServer serverMock = with(wireMockConfig().dynamicPort());

		@Nested
		@DisplayName("accessing parent field")
		class AccessingParentField {

			@Test
			@DisplayName("should work.")
			void shouldWork() {
				serverMock.stubFor(get("/z").willReturn(ok()));
				then(new SampleClient("http://localhost:" + serverMock.port() + "/z").isOk()).isTrue();
			}
		}
	}


	@ExtendWith(WireMockExtension.class)
	private static abstract class TestBase {
		@Test
		@SuppressWarnings("JUnitTestMethodWithNoAssertions")
		void testNothing() {
		}
	}

	private static class TestResults extends SummaryGeneratingListener {

		private final List<Throwable> throwables = new ArrayList<>();

		@Override
		public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
			super.executionFinished(testIdentifier, testExecutionResult);
			testExecutionResult.getThrowable().ifPresent(throwables::add);
		}

		public List<Throwable> getThrowables() {
			return unmodifiableList(throwables);
		}
	}
}