/*
 * 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
 *
 *      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.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.util.Iterator;
import java.util.Set;

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

import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*;

/**
 * Unit tests that verify support for getting and finding all composed, repeatable
 * annotations on a single annotated element.
 *
 * <p>See <a href="https://jira.spring.io/browse/SPR-13973">SPR-13973</a>.
 *
 * @author Sam Brannen
 * @since 4.3
 * @see AnnotatedElementUtils#getMergedRepeatableAnnotations
 * @see AnnotatedElementUtils#findMergedRepeatableAnnotations
 * @see AnnotatedElementUtilsTests
 * @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests
 */
public class ComposedRepeatableAnnotationsTests {

	@Rule
	public final ExpectedException exception = ExpectedException.none();


	@Test
	public void getNonRepeatableAnnotation() {
		expectNonRepeatableAnnotation();
		getMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
	}

	@Test
	public void getInvalidRepeatableAnnotationContainerMissingValueAttribute() {
		expectContainerMissingValueAttribute();
		getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
	}

	@Test
	public void getInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
		expectContainerWithNonArrayValueAttribute();
		getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
	}

	@Test
	public void getInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
		expectContainerWithArrayValueAttributeButWrongComponentType();
		getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
			ContainerWithArrayValueAttributeButWrongComponentType.class);
	}

	@Test
	public void getRepeatableAnnotationsOnClass() {
		assertGetRepeatableAnnotations(RepeatableClass.class);
	}

	@Test
	public void getRepeatableAnnotationsOnSuperclass() {
		assertGetRepeatableAnnotations(SubRepeatableClass.class);
	}

	@Test
	public void getComposedRepeatableAnnotationsOnClass() {
		assertGetRepeatableAnnotations(ComposedRepeatableClass.class);
	}

	@Test
	public void getComposedRepeatableAnnotationsMixedWithContainerOnClass() {
		assertGetRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
	}

	@Test
	public void getComposedContainerForRepeatableAnnotationsOnClass() {
		assertGetRepeatableAnnotations(ComposedContainerClass.class);
	}

	@Test
	public void getNoninheritedComposedRepeatableAnnotationsOnClass() {
		Class<?> element = NoninheritedRepeatableClass.class;
		Set<Noninherited> annotations = getMergedRepeatableAnnotations(element, Noninherited.class);
		assertNoninheritedRepeatableAnnotations(annotations);
	}

	@Test
	public void getNoninheritedComposedRepeatableAnnotationsOnSuperclass() {
		Class<?> element = SubNoninheritedRepeatableClass.class;
		Set<Noninherited> annotations = getMergedRepeatableAnnotations(element, Noninherited.class);
		assertNotNull(annotations);
		assertEquals(0, annotations.size());
	}

	@Test
	public void findNonRepeatableAnnotation() {
		expectNonRepeatableAnnotation();
		findMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
	}

	@Test
	public void findInvalidRepeatableAnnotationContainerMissingValueAttribute() {
		expectContainerMissingValueAttribute();
		findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
	}

	@Test
	public void findInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
		expectContainerWithNonArrayValueAttribute();
		findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
	}

	@Test
	public void findInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
		expectContainerWithArrayValueAttributeButWrongComponentType();
		findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
			ContainerWithArrayValueAttributeButWrongComponentType.class);
	}

	@Test
	public void findRepeatableAnnotationsOnClass() {
		assertFindRepeatableAnnotations(RepeatableClass.class);
	}

	@Test
	public void findRepeatableAnnotationsOnSuperclass() {
		assertFindRepeatableAnnotations(SubRepeatableClass.class);
	}

	@Test
	public void findComposedRepeatableAnnotationsOnClass() {
		assertFindRepeatableAnnotations(ComposedRepeatableClass.class);
	}

	@Test
	public void findComposedRepeatableAnnotationsMixedWithContainerOnClass() {
		assertFindRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
	}

	@Test
	public void findNoninheritedComposedRepeatableAnnotationsOnClass() {
		Class<?> element = NoninheritedRepeatableClass.class;
		Set<Noninherited> annotations = findMergedRepeatableAnnotations(element, Noninherited.class);
		assertNoninheritedRepeatableAnnotations(annotations);
	}

	@Test
	public void findNoninheritedComposedRepeatableAnnotationsOnSuperclass() {
		Class<?> element = SubNoninheritedRepeatableClass.class;
		Set<Noninherited> annotations = findMergedRepeatableAnnotations(element, Noninherited.class);
		assertNoninheritedRepeatableAnnotations(annotations);
	}

	@Test
	public void findComposedContainerForRepeatableAnnotationsOnClass() {
		assertFindRepeatableAnnotations(ComposedContainerClass.class);
	}

	private void expectNonRepeatableAnnotation() {
		exception.expect(IllegalArgumentException.class);
		exception.expectMessage(startsWith("Annotation type must be a repeatable annotation"));
		exception.expectMessage(containsString("failed to resolve container type for"));
		exception.expectMessage(containsString(NonRepeatable.class.getName()));
	}

	private void expectContainerMissingValueAttribute() {
		exception.expect(AnnotationConfigurationException.class);
		exception.expectMessage(startsWith("Invalid declaration of container type"));
		exception.expectMessage(containsString(ContainerMissingValueAttribute.class.getName()));
		exception.expectMessage(containsString("for repeatable annotation"));
		exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
		exception.expectCause(isA(NoSuchMethodException.class));
	}

	private void expectContainerWithNonArrayValueAttribute() {
		exception.expect(AnnotationConfigurationException.class);
		exception.expectMessage(startsWith("Container type"));
		exception.expectMessage(containsString(ContainerWithNonArrayValueAttribute.class.getName()));
		exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
		exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
	}

	private void expectContainerWithArrayValueAttributeButWrongComponentType() {
		exception.expect(AnnotationConfigurationException.class);
		exception.expectMessage(startsWith("Container type"));
		exception.expectMessage(containsString(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()));
		exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
		exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
	}

	private void assertGetRepeatableAnnotations(AnnotatedElement element) {
		assertNotNull(element);

		Set<PeteRepeat> peteRepeats = getMergedRepeatableAnnotations(element, PeteRepeat.class);
		assertNotNull(peteRepeats);
		assertEquals(3, peteRepeats.size());

		Iterator<PeteRepeat> iterator = peteRepeats.iterator();
		assertEquals("A", iterator.next().value());
		assertEquals("B", iterator.next().value());
		assertEquals("C", iterator.next().value());
	}

	private void assertFindRepeatableAnnotations(AnnotatedElement element) {
		assertNotNull(element);

		Set<PeteRepeat> peteRepeats = findMergedRepeatableAnnotations(element, PeteRepeat.class);
		assertNotNull(peteRepeats);
		assertEquals(3, peteRepeats.size());

		Iterator<PeteRepeat> iterator = peteRepeats.iterator();
		assertEquals("A", iterator.next().value());
		assertEquals("B", iterator.next().value());
		assertEquals("C", iterator.next().value());
	}

	private void assertNoninheritedRepeatableAnnotations(Set<Noninherited> annotations) {
		assertNotNull(annotations);
		assertEquals(3, annotations.size());

		Iterator<Noninherited> iterator = annotations.iterator();
		assertEquals("A", iterator.next().value());
		assertEquals("B", iterator.next().value());
		assertEquals("C", iterator.next().value());
	}


	// -------------------------------------------------------------------------

	@Retention(RetentionPolicy.RUNTIME)
	@interface NonRepeatable {
	}

	@Retention(RetentionPolicy.RUNTIME)
	@interface ContainerMissingValueAttribute {
		// InvalidRepeatable[] value();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@interface ContainerWithNonArrayValueAttribute {

		InvalidRepeatable value();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@interface ContainerWithArrayValueAttributeButWrongComponentType {

		String[] value();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@interface InvalidRepeatable {
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Inherited
	@interface PeteRepeats {

		PeteRepeat[] value();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Inherited
	@Repeatable(PeteRepeats.class)
	@interface PeteRepeat {

		String value();
	}

	@PeteRepeat("shadowed")
	@Target({ ElementType.METHOD, ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	@Inherited
	@interface ForPetesSake {

		@AliasFor(annotation = PeteRepeat.class)
		String value();
	}

	@PeteRepeat("shadowed")
	@Target({ ElementType.METHOD, ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	@Inherited
	@interface ForTheLoveOfFoo {

		@AliasFor(annotation = PeteRepeat.class)
		String value();
	}

	@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
	@Target({ ElementType.METHOD, ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	@Inherited
	@interface ComposedContainer {
	}

	@PeteRepeat("A")
	@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
	static class RepeatableClass {
	}

	static class SubRepeatableClass extends RepeatableClass {
	}

	@ForPetesSake("B")
	@ForTheLoveOfFoo("C")
	@PeteRepeat("A")
	static class ComposedRepeatableClass {
	}

	@ForPetesSake("C")
	@PeteRepeats(@PeteRepeat("A"))
	@PeteRepeat("B")
	static class ComposedRepeatableMixedWithContainerClass {
	}

	@PeteRepeat("A")
	@ComposedContainer
	static class ComposedContainerClass {
	}

	@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@interface Noninheriteds {

		Noninherited[] value();
	}

	@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@Repeatable(Noninheriteds.class)
	@interface Noninherited {

		@AliasFor("name")
		String value() default "";

		@AliasFor("value")
		String name() default "";
	}

	@Noninherited(name = "shadowed")
	@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@interface ComposedNoninherited {

		@AliasFor(annotation = Noninherited.class)
		String name() default "";
	}

	@ComposedNoninherited(name = "C")
	@Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") })
	static class NoninheritedRepeatableClass {
	}

	static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass {
	}

}
