/*
 * Copyright 2002-2016 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.test.context.support;

import java.util.Map;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.test.context.TestPropertySource;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.context.support.TestPropertySourceUtils.*;

/**
 * Unit tests for {@link TestPropertySourceUtils}.
 *
 * @author Sam Brannen
 * @since 4.1
 */
public class TestPropertySourceUtilsTests {

	private static final String[] EMPTY_STRING_ARRAY = new String[0];

	private static final String[] KEY_VALUE_PAIR = new String[] {"key = value"};

	private static final String[] FOO_LOCATIONS = new String[] {"classpath:/foo.properties"};


	@Rule
	public ExpectedException expectedException = ExpectedException.none();


	@Test
	public void emptyAnnotation() {
		expectedException.expect(IllegalStateException.class);
		expectedException.expectMessage(startsWith("Could not detect default properties file for test"));
		expectedException.expectMessage(containsString("EmptyPropertySources.properties"));
		buildMergedTestPropertySources(EmptyPropertySources.class);
	}

	@Test
	public void extendedEmptyAnnotation() {
		expectedException.expect(IllegalStateException.class);
		expectedException.expectMessage(startsWith("Could not detect default properties file for test"));
		expectedException.expectMessage(containsString("ExtendedEmptyPropertySources.properties"));
		buildMergedTestPropertySources(ExtendedEmptyPropertySources.class);
	}

	@Test
	public void value() {
		assertMergedTestPropertySources(ValuePropertySources.class, asArray("classpath:/value.xml"),
				EMPTY_STRING_ARRAY);
	}

	@Test
	public void locationsAndValueAttributes() {
		expectedException.expect(AnnotationConfigurationException.class);
		buildMergedTestPropertySources(LocationsAndValuePropertySources.class);
	}

	@Test
	public void locationsAndProperties() {
		assertMergedTestPropertySources(LocationsAndPropertiesPropertySources.class,
				asArray("classpath:/foo1.xml", "classpath:/foo2.xml"), asArray("k1a=v1a", "k1b: v1b"));
	}

	@Test
	public void inheritedLocationsAndProperties() {
		assertMergedTestPropertySources(InheritedPropertySources.class,
				asArray("classpath:/foo1.xml", "classpath:/foo2.xml"), asArray("k1a=v1a", "k1b: v1b"));
	}

	@Test
	public void extendedLocationsAndProperties() {
		assertMergedTestPropertySources(ExtendedPropertySources.class,
				asArray("classpath:/foo1.xml", "classpath:/foo2.xml", "classpath:/bar1.xml", "classpath:/bar2.xml"),
				asArray("k1a=v1a", "k1b: v1b", "k2a v2a", "k2b: v2b"));
	}

	@Test
	public void overriddenLocations() {
		assertMergedTestPropertySources(OverriddenLocationsPropertySources.class,
				asArray("classpath:/baz.properties"), asArray("k1a=v1a", "k1b: v1b", "key = value"));
	}

	@Test
	public void overriddenProperties() {
		assertMergedTestPropertySources(OverriddenPropertiesPropertySources.class,
				asArray("classpath:/foo1.xml", "classpath:/foo2.xml", "classpath:/baz.properties"), KEY_VALUE_PAIR);
	}

	@Test
	public void overriddenLocationsAndProperties() {
		assertMergedTestPropertySources(OverriddenLocationsAndPropertiesPropertySources.class,
				asArray("classpath:/baz.properties"), KEY_VALUE_PAIR);
	}


	@Test
	public void addPropertiesFilesToEnvironmentWithNullContext() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("must not be null");
		addPropertiesFilesToEnvironment((ConfigurableApplicationContext) null, FOO_LOCATIONS);
	}

	@Test
	public void addPropertiesFilesToEnvironmentWithContextAndNullLocations() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("must not be null");
		addPropertiesFilesToEnvironment(mock(ConfigurableApplicationContext.class), (String[]) null);
	}

	@Test
	public void addPropertiesFilesToEnvironmentWithNullEnvironment() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("must not be null");
		addPropertiesFilesToEnvironment((ConfigurableEnvironment) null, mock(ResourceLoader.class), FOO_LOCATIONS);
	}

	@Test
	public void addPropertiesFilesToEnvironmentWithEnvironmentAndNullLocations() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("must not be null");
		addPropertiesFilesToEnvironment(new MockEnvironment(), mock(ResourceLoader.class), (String[]) null);
	}

	@Test
	public void addPropertiesFilesToEnvironmentWithSinglePropertyFromVirtualFile() {
		ConfigurableEnvironment environment = new MockEnvironment();

		MutablePropertySources propertySources = environment.getPropertySources();
		propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
		assertEquals(0, propertySources.size());

		String pair = "key = value";
		ByteArrayResource resource = new ByteArrayResource(pair.getBytes(), "from inlined property: " + pair);
		ResourceLoader resourceLoader = mock(ResourceLoader.class);
		when(resourceLoader.getResource(anyString())).thenReturn(resource);

		addPropertiesFilesToEnvironment(environment, resourceLoader, FOO_LOCATIONS);
		assertEquals(1, propertySources.size());
		assertEquals("value", environment.getProperty("key"));
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithNullContext() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("context");
		addInlinedPropertiesToEnvironment((ConfigurableApplicationContext) null, KEY_VALUE_PAIR);
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithContextAndNullInlinedProperties() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("inlined");
		addInlinedPropertiesToEnvironment(mock(ConfigurableApplicationContext.class), (String[]) null);
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithNullEnvironment() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("environment");
		addInlinedPropertiesToEnvironment((ConfigurableEnvironment) null, KEY_VALUE_PAIR);
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithEnvironmentAndNullInlinedProperties() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("inlined");
		addInlinedPropertiesToEnvironment(new MockEnvironment(), (String[]) null);
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithMalformedUnicodeInValue() {
		expectedException.expect(IllegalStateException.class);
		expectedException.expectMessage("Failed to load test environment property");
		addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("key = \\uZZZZ"));
	}

	@Test
	public void addInlinedPropertiesToEnvironmentWithMultipleKeyValuePairsInSingleInlinedProperty() {
		expectedException.expect(IllegalStateException.class);
		expectedException.expectMessage("Failed to load exactly one test environment property");
		addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("a=b\nx=y"));
	}

	@Test
	@SuppressWarnings("rawtypes")
	public void addInlinedPropertiesToEnvironmentWithEmptyProperty() {
		ConfigurableEnvironment environment = new MockEnvironment();
		MutablePropertySources propertySources = environment.getPropertySources();
		propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
		assertEquals(0, propertySources.size());
		addInlinedPropertiesToEnvironment(environment, asArray("  "));
		assertEquals(1, propertySources.size());
		assertEquals(0, ((Map) propertySources.iterator().next().getSource()).size());
	}

	@Test
	public void convertInlinedPropertiesToMapWithNullInlinedProperties() {
		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("inlined");
		convertInlinedPropertiesToMap((String[]) null);
	}


	private static void assertMergedTestPropertySources(Class<?> testClass, String[] expectedLocations,
			String[] expectedProperties) {

		MergedTestPropertySources mergedPropertySources = buildMergedTestPropertySources(testClass);
		assertNotNull(mergedPropertySources);
		assertArrayEquals(expectedLocations, mergedPropertySources.getLocations());
		assertArrayEquals(expectedProperties, mergedPropertySources.getProperties());
	}


	@SafeVarargs
	private static <T> T[] asArray(T... arr) {
		return arr;
	}


	@TestPropertySource
	static class EmptyPropertySources {
	}

	@TestPropertySource
	static class ExtendedEmptyPropertySources extends EmptyPropertySources {
	}

	@TestPropertySource(locations = "/foo", value = "/bar")
	static class LocationsAndValuePropertySources {
	}

	@TestPropertySource("/value.xml")
	static class ValuePropertySources {
	}

	@TestPropertySource(locations = { "/foo1.xml", "/foo2.xml" }, properties = { "k1a=v1a", "k1b: v1b" })
	static class LocationsAndPropertiesPropertySources {
	}

	static class InheritedPropertySources extends LocationsAndPropertiesPropertySources {
	}

	@TestPropertySource(locations = { "/bar1.xml", "/bar2.xml" }, properties = { "k2a v2a", "k2b: v2b" })
	static class ExtendedPropertySources extends LocationsAndPropertiesPropertySources {
	}

	@TestPropertySource(locations = "/baz.properties", properties = "key = value", inheritLocations = false)
	static class OverriddenLocationsPropertySources extends LocationsAndPropertiesPropertySources {
	}

	@TestPropertySource(locations = "/baz.properties", properties = "key = value", inheritProperties = false)
	static class OverriddenPropertiesPropertySources extends LocationsAndPropertiesPropertySources {
	}

	@TestPropertySource(locations = "/baz.properties", properties = "key = value", inheritLocations = false, inheritProperties = false)
	static class OverriddenLocationsAndPropertiesPropertySources extends LocationsAndPropertiesPropertySources {
	}

}