/*
 * Copyright 2002-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.cloud.servicebroker.autoconfigure.web;

import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.support.TestPropertySourceUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.junit.jupiter.api.Assertions.assertThrows;

class ServiceBrokerPropertiesBindingTest {

	private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

	@AfterEach
	void closeContext() {
		this.context.close();
	}

	@Test
	void bindMinimumValidProperties() {
		this.context.register(ServiceBrokerPropertiesConfiguration.class);
		TestPropertySourceUtils.addPropertiesFilesToEnvironment(this.context, "classpath:catalog-minimal.properties");
		validateMinimumCatalog();
	}

	@Test
	void bindMinimumValidYaml() throws Exception {
		this.context.register(ServiceBrokerPropertiesConfiguration.class);
		Resource resource = context.getResource("classpath:catalog-minimal.yml");
		YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
		List<PropertySource<?>> properties = sourceLoader.load("catalog", resource);
		context.getEnvironment().getPropertySources().addFirst(properties.get(0));
		validateMinimumCatalog();
	}

	private void validateMinimumCatalog() {
		this.context.refresh();
		ServiceBrokerProperties properties = this.context.getBean(ServiceBrokerProperties.class);
		Catalog catalog = properties.getCatalog();
		assertThat(catalog).isNotNull();
		assertThat(catalog.getServices()).hasSize(1);
		assertThat(catalog.getServices().get(0).getId()).isEqualTo("service-one-id");
		assertThat(catalog.getServices().get(0).getName()).isEqualTo("Service One");
		assertThat(catalog.getServices().get(0).getDescription()).isEqualTo("Description for Service One");
		assertThat(catalog.getServices().get(0).getPlans()).hasSize(1);
		assertThat(catalog.getServices().get(0).getPlans().get(0).getId()).isEqualTo("plan-one-id");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getName()).isEqualTo("Plan One");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getDescription())
				.isEqualTo("Description for Plan One");

		//Mandatory fields should have a default set when unspecified in configuration
		assertThat(catalog.getServices().get(0).isBindable()).isNotNull();
		//Optional unspecified fields should not have a default set.
		assertThat(catalog.getServices().get(0).getPlans().get(0).isBindable()).isNull();
		assertThat(catalog.getServices().get(0).getPlans().get(0).isFree()).isNull();
		assertThat(catalog.getServices().get(0).getPlans().get(0).isPlanUpdateable()).isNull();
		assertThat(catalog.getServices().get(0).getPlans().get(0).getMaximumPollingDuration()).isNull();
		assertThat(catalog.getServices().get(0).isInstancesRetrievable()).isNull();
		assertThat(catalog.getServices().get(0).isBindingsRetrievable()).isNull();
		assertThat(catalog.getServices().get(0).isPlanUpdateable()).isNull();
	}

	@Test
	void bindInvalidProperties() {
		this.context.register(ServiceBrokerPropertiesConfiguration.class);
		TestPropertyValues.of("spring.cloud.openservicebroker.catalog.services[0].id:service-one-id")
				.applyTo(this.context);
		assertThrows(ConfigurationPropertiesBindException.class, this.context::refresh);
	}

	@Test
	void bindFullValidYaml() throws Exception {
		this.context.register(ServiceBrokerPropertiesConfiguration.class);
		Resource resource = context.getResource("classpath:catalog-full.yml");
		YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
		List<PropertySource<?>> properties = sourceLoader.load("catalog", resource);
		context.getEnvironment().getPropertySources().addFirst(properties.get(0));
		validateFullCatalog();
	}

	@Test
	void bindFullValidProperties() {
		this.context.register(ServiceBrokerPropertiesConfiguration.class);
		TestPropertySourceUtils.addPropertiesFilesToEnvironment(this.context, "classpath:catalog-full.properties");
		validateFullCatalog();
	}

	private void validateFullCatalog() {
		this.context.refresh();
		ServiceBrokerProperties properties = this.context.getBean(ServiceBrokerProperties.class);
		Catalog catalog = properties.getCatalog();
		assertThat(catalog).isNotNull();
		validateServiceOne(catalog);
		validateServiceOneMetadata(catalog);
		validateServiceOnePlans(catalog);
		validateServiceTwo(catalog);
	}

	private void validateServiceOne(Catalog catalog) {
		assertThat(catalog.getServices()).hasSize(2);
		assertThat(catalog.getServices().get(0).getId()).isEqualTo("service-one-id");
		assertThat(catalog.getServices().get(0).getName()).isEqualTo("Service One");
		assertThat(catalog.getServices().get(0).getDescription()).isEqualTo("Description for Service One");
		assertThat(catalog.getServices().get(0).isBindable()).isTrue();
		assertThat(catalog.getServices().get(0).isBindingsRetrievable()).isTrue();
		assertThat(catalog.getServices().get(0).isInstancesRetrievable()).isTrue();
		assertThat(catalog.getServices().get(0).isPlanUpdateable()).isTrue();
		assertThat(catalog.getServices().get(0).getRequires()).containsOnly("syslog_drain", "route_forwarding");
		assertThat(catalog.getServices().get(0).getTags()).containsOnly("tag1", "tag2");
		assertThat(catalog.getServices().get(0).getDashboardClient().getId()).isEqualTo("dashboard-id");
		assertThat(catalog.getServices().get(0).getDashboardClient().getSecret()).isEqualTo("dashboard-secret");
		assertThat(catalog.getServices().get(0).getDashboardClient().getRedirectUri())
				.isEqualTo("dashboard-redirect-uri");
	}

	private void validateServiceOneMetadata(Catalog catalog) {
		assertThat(catalog.getServices().get(0).getMetadata().getDisplayName()).isEqualTo("service display name");
		assertThat(catalog.getServices().get(0).getMetadata().getImageUrl()).isEqualTo("image-uri");
		assertThat(catalog.getServices().get(0).getMetadata().getLongDescription())
				.isEqualTo("service long description");
		assertThat(catalog.getServices().get(0).getMetadata().getProviderDisplayName())
				.isEqualTo("service provider display name");
		assertThat(catalog.getServices().get(0).getMetadata().getDocumentationUrl())
				.isEqualTo("service-documentation-url");
		assertThat(catalog.getServices().get(0).getMetadata().getSupportUrl()).isEqualTo("service-support-url");

		Object licenses = catalog.getServices().get(0).getMetadata().getProperties().get("licenses");
		assertThat(licenses).isInstanceOf(Map.class);
		@SuppressWarnings("unchecked")
		Map<String, Object> licenseMap = (Map<String, Object>) licenses;
		assertThat(licenseMap).containsOnly(entry("0", "license1"), entry("1", "license2"));

		Object features = catalog.getServices().get(0).getMetadata().getProperties().get("features");
		assertThat(features).isInstanceOf(Map.class);
		@SuppressWarnings("unchecked")
		Map<String, Object> featuresMap = (Map<String, Object>) features;
		assertThat(featuresMap).containsOnly(entry("0", "hosting"), entry("1", "scaling"));

		assertThat(catalog.getServices().get(0).getMetadata().getProperties()).contains(entry("key1", "value1"),
				entry("key2", "value2"));
	}

	private void validateServiceOnePlans(Catalog catalog) {
		assertThat(catalog.getServices().get(0).getPlans()).hasSize(2);
		assertThat(catalog.getServices().get(0).getPlans().get(0).getId()).isEqualTo("plan-one-id");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getName()).isEqualTo("Plan One");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getDescription())
				.isEqualTo("Description for Plan One");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getMaintenanceInfo().getVersion()).isEqualTo("1.0.1");
		assertThat(catalog.getServices().get(0).getPlans().get(0).getMaintenanceInfo().getDescription())
				.isEqualTo("Description for maintenance info");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getId()).isEqualTo("plan-two-id");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getName()).isEqualTo("Plan Two");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getDescription())
				.isEqualTo("Description for Plan Two");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getProperties())
				.containsOnly(entry("key1", "value1"), entry("key2", "value2"));
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getCosts()).hasSize(1);
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getCosts().get(0).getUnit())
				.isEqualTo("MONTHLY");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getCosts().get(0).getAmount())
				.contains(entry("usd", 649.0));
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getDisplayName())
				.isEqualTo("sample display name");
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMetadata().getBullets())
				.containsExactlyInAnyOrder("bullet1", "bullet2");
		assertThat(catalog.getServices().get(0).getPlans().get(1).isBindable()).isTrue();
		assertThat(catalog.getServices().get(0).getPlans().get(1).isFree()).isTrue();
		assertThat(catalog.getServices().get(0).getPlans().get(1).isPlanUpdateable()).isTrue();
		assertThat(catalog.getServices().get(0).getPlans().get(1).getSchemas().getServiceInstance().getCreate()
				.getParameters())
				.contains(entry("$schema", "http://json-schema.org/draft-04/schema#"),
						entry("type", "string"));
		Object enumMap = catalog.getServices().get(0).getPlans().get(1).getSchemas().getServiceInstance().getCreate()
				.getParameters().get("enum");
		assertThat(enumMap).isInstanceOf(Map.class);
		@SuppressWarnings("unchecked")
		Map<String, Object> castedMap = (Map<String, Object>) enumMap;
		assertThat(castedMap).containsOnly(entry("0", "one"), entry("1", "two"), entry("2", "three"));
		assertThat(catalog.getServices().get(0).getPlans().get(1).getSchemas().getServiceInstance().getUpdate()
				.getParameters())
				.containsOnly(entry("$schema", "http://json-schema.org/draft-04/schema#"), entry("type", "object"));
		assertThat(catalog.getServices().get(0).getPlans().get(1).getSchemas().getServiceBinding().getCreate()
				.getParameters())
				.containsOnly(entry("$schema", "http://json-schema.org/draft-04/schema#"), entry("type", "object"));
		assertThat(catalog.getServices().get(0).getPlans().get(1).getMaximumPollingDuration()).isEqualTo(120);
	}

	private void validateServiceTwo(Catalog catalog) {
		assertThat(catalog.getServices().get(1).getId()).isEqualTo("service-two-id");
		assertThat(catalog.getServices().get(1).getName()).isEqualTo("Service Two");
		assertThat(catalog.getServices().get(1).getDescription()).isEqualTo("Description for Service Two");
		assertThat(catalog.getServices().get(1).getPlans()).hasSize(1);
		assertThat(catalog.getServices().get(1).getPlans().get(0).getId()).isEqualTo("plan-one-id");
		assertThat(catalog.getServices().get(1).getPlans().get(0).getName()).isEqualTo("Plan One");
		assertThat(catalog.getServices().get(1).getPlans().get(0).getDescription())
				.isEqualTo("Description for Plan One");
	}

	@Configuration
	@EnableConfigurationProperties(ServiceBrokerProperties.class)
	protected static class ServiceBrokerPropertiesConfiguration {

	}

}