/*
 * Copyright 2002-2017 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;

/**
 * {@code TestPropertySourceAttributes} encapsulates the attributes declared
 * via {@link TestPropertySource @TestPropertySource}.
 *
 * <p>In addition to encapsulating declared attributes,
 * {@code TestPropertySourceAttributes} also enforces configuration rules
 * and detects default properties files.
 *
 * @author Sam Brannen
 * @since 4.1
 * @see TestPropertySource
 * @see MergedTestPropertySources
 */
class TestPropertySourceAttributes {

	private static final Log logger = LogFactory.getLog(TestPropertySourceAttributes.class);

	private final Class<?> declaringClass;

	private final String[] locations;

	private final boolean inheritLocations;

	private final String[] properties;

	private final boolean inheritProperties;


	/**
	 * Create a new {@code TestPropertySourceAttributes} instance for the
	 * supplied {@link TestPropertySource @TestPropertySource} annotation and
	 * the {@linkplain Class test class} that declared it, enforcing
	 * configuration rules and detecting a default properties file if
	 * necessary.
	 * @param declaringClass the class that declared {@code @TestPropertySource}
	 * @param testPropertySource the annotation from which to retrieve the attributes
	 * @since 4.2
	 */
	TestPropertySourceAttributes(Class<?> declaringClass, TestPropertySource testPropertySource) {
		this(declaringClass, testPropertySource.locations(), testPropertySource.inheritLocations(),
			testPropertySource.properties(), testPropertySource.inheritProperties());
	}

	private TestPropertySourceAttributes(Class<?> declaringClass, String[] locations, boolean inheritLocations,
			String[] properties, boolean inheritProperties) {

		Assert.notNull(declaringClass, "declaringClass must not be null");
		if (ObjectUtils.isEmpty(locations) && ObjectUtils.isEmpty(properties)) {
			locations = new String[] { detectDefaultPropertiesFile(declaringClass) };
		}
		this.declaringClass = declaringClass;
		this.locations = locations;
		this.inheritLocations = inheritLocations;
		this.properties = properties;
		this.inheritProperties = inheritProperties;
	}


	/**
	 * Get the {@linkplain Class class} that declared {@code @TestPropertySource}.
	 * @return the declaring class; never {@code null}
	 */
	Class<?> getDeclaringClass() {
		return this.declaringClass;
	}

	/**
	 * Get the resource locations that were declared via {@code @TestPropertySource}.
	 * <p>Note: The returned value may represent a <em>detected default</em>
	 * that does not match the original value declared via {@code @TestPropertySource}.
	 * @return the resource locations; potentially <em>empty</em>
	 * @see TestPropertySource#value
	 * @see TestPropertySource#locations
	 */
	String[] getLocations() {
		return this.locations;
	}

	/**
	 * Get the {@code inheritLocations} flag that was declared via {@code @TestPropertySource}.
	 * @return the {@code inheritLocations} flag
	 * @see TestPropertySource#inheritLocations
	 */
	boolean isInheritLocations() {
		return this.inheritLocations;
	}

	/**
	 * Get the inlined properties that were declared via {@code @TestPropertySource}.
	 * @return the inlined properties; potentially {@code null} or <em>empty</em>
	 * @see TestPropertySource#properties
	 */
	@Nullable
	String[] getProperties() {
		return this.properties;
	}

	/**
	 * Get the {@code inheritProperties} flag that was declared via {@code @TestPropertySource}.
	 * @return the {@code inheritProperties} flag
	 * @see TestPropertySource#inheritProperties
	 */
	boolean isInheritProperties() {
		return this.inheritProperties;
	}

	/**
	 * Provide a String representation of the {@code @TestPropertySource}
	 * attributes and declaring class.
	 */
	@Override
	public String toString() {
		return new ToStringCreator(this)//
		.append("declaringClass", declaringClass.getName())//
		.append("locations", ObjectUtils.nullSafeToString(locations))//
		.append("inheritLocations", inheritLocations)//
		.append("properties", ObjectUtils.nullSafeToString(properties))//
		.append("inheritProperties", inheritProperties)//
		.toString();
	}


	/**
	 * Detect a default properties file for the supplied class, as specified
	 * in the class-level Javadoc for {@link TestPropertySource}.
	 */
	private static String detectDefaultPropertiesFile(Class<?> testClass) {
		String resourcePath = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".properties";
		String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath;
		ClassPathResource classPathResource = new ClassPathResource(resourcePath);

		if (classPathResource.exists()) {
			if (logger.isInfoEnabled()) {
				logger.info(String.format("Detected default properties file \"%s\" for test class [%s]",
					prefixedResourcePath, testClass.getName()));
			}
			return prefixedResourcePath;
		}
		else {
			String msg = String.format("Could not detect default properties file for test [%s]: " +
					"%s does not exist. Either declare the 'locations' or 'properties' attributes " +
					"of @TestPropertySource or make the default properties file available.", testClass.getName(),
					classPathResource);
			logger.error(msg);
			throw new IllegalStateException(msg);
		}
	}

}
