package org.reflections;

import org.junit.jupiter.api.Test;
import org.reflections.scanners.Scanners;
import org.reflections.util.AnnotationMergeCollector;
import org.reflections.util.QueryFunction;

import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.reflections.ReflectionUtils.*;
import static org.reflections.ReflectionsQueryTest.equalTo;
import static org.reflections.TestModel.*;
import static org.reflections.scanners.Scanners.MethodsAnnotated;
import static org.reflections.scanners.Scanners.TypesAnnotated;

public class ReflectionUtilsQueryTest {

	@Test
	public void testTypes() throws NoSuchMethodException {
		assertThat(
			get(SuperTypes.of(C3.class)),
			equalTo(C1.class, I2.class, I1.class));

		assertThat(
			get(SuperTypes.of(C3.class)
				.filter(withAnnotation(AI1.class))),
			equalTo(I1.class));

		assertThat(
			get(Interfaces.get(C1.class)),
			equalTo(I2.class));

		assertThat(
			get(Interfaces.of(C3.class)),
			equalTo(I2.class, I1.class));

		assertThat(
			get(SuperClass.of(C5.class)),
			equalTo(C3.class, C1.class));

		assertThat(
			get(Annotations.of(TestModel.C3.class)
				.map(Annotation::annotationType)),
			equalTo(
				Retention.class, Target.class, Documented.class, Inherited.class,
				AC1.class, AC1n.class, AC2.class, AI1.class, AI2.class, MAI1.class));

		assertThat(
			get(AnnotationTypes.of(C3.class)
				.filter(a -> !a.getName().startsWith("java."))),
			equalTo(
				AC1.class, AC1n.class, AC2.class, AI1.class, AI2.class, MAI1.class));

		assertThat(
			get(Annotations.of(C4.class.getDeclaredMethod("m4", String.class))
				.map(Annotation::annotationType)),
			equalTo());
	}

	@Test
	public void testMembers() throws NoSuchMethodException, NoSuchFieldException {
		assertThat(
			get(Methods.of(C4.class, withName("m4"))),
			equalTo(C4.class.getDeclaredMethod("m4", String.class)));

		assertThat(
			get(Methods.of(C4.class, withParameters(String.class))),
			equalTo(C4.class.getDeclaredMethod("m4", String.class)));

		assertThat(
			get(Methods.of(C4.class)
				.filter(withPattern("public.*.void .*"))
				.map(Method::getName)),
			equalTo("m1"));

		assertThat(
			get(Methods.of(C4.class, withAnyParameterAnnotation(AM1.class))),
			equalTo(C4.class.getDeclaredMethod("m4", String.class)));

		assertThat(
			get(Methods.of(Class.class)
				.filter(withReturnType(Method.class).and(withPublic()))
				.map(Method::getName)),
			equalTo("getMethod", "getDeclaredMethod", "getEnclosingMethod"));

		assertThat(
			get(Fields.of(C4.class, withAnnotation(AF1.class))),
			equalTo(C4.class.getDeclaredField("f1"),
				C4.class.getDeclaredField("f2")));

		AF1 af12 = new AF1() {
			public String value() { return "2"; }
			public Class<? extends Annotation> annotationType() { return AF1.class; }
		};
		assertThat(
			get(Fields.of(C4.class)
				.filter(withAnnotation(af12))),
			equalTo(C4.class.getDeclaredField("f2")));

		assertThat(
			get(Fields.of(C4.class)
				.filter(withTypeAssignableTo(String.class))),
			equalTo(C4.class.getDeclaredField("f1"),
				C4.class.getDeclaredField("f2"),
				C4.class.getDeclaredField("f3")));

		assertThat(
			get(Constructors.of(C4.class)
				.filter(withParametersCount(0))),
			equalTo(C4.class.getDeclaredConstructor()));
	}

	@Test
	public void nestedQuery() {
		Set<Class<? extends Annotation>> annotations =
			get(AnnotationTypes.of(
					Methods.of(C4.class))
				.filter(withNamePrefix("org.reflections")));

		assertThat(annotations,
			equalTo(AM1.class));
	}

	@Test
	public void addQuery() {
		Set<Class<? extends Annotation>> annotations =
			get(AnnotationTypes.of(C1.class)
				.add(AnnotationTypes.of(C2.class)));

		assertThat(annotations,
			equalTo(
				Retention.class, Target.class, Documented.class, Inherited.class,
				AC1.class, AC2.class, AC1n.class, AI2.class, AI1.class, MAI1.class));
	}

	@Test
	public void singleQuery() {
		QueryFunction<Store, Class<?>> single =
			QueryFunction.single(CombinedTestModel.Impl.class);
		assertThat(single.apply(null),
			equalTo(CombinedTestModel.Impl.class));

		QueryFunction<Store, Class<?>> second =
			single.add(
				QueryFunction.single(CombinedTestModel.Controller.class));
		assertThat(second.apply(null),
			equalTo(CombinedTestModel.Impl.class, CombinedTestModel.Controller.class));
	}

	@Test
	public void getAllQuery() {
		QueryFunction<Store, Class<?>> single =
			QueryFunction.single(CombinedTestModel.Impl.class);

		QueryFunction<Store, Class<?>> allIncluding =
			single.add(
				single.getAll(SuperTypes::get));
		assertThat(allIncluding.apply(null),
			equalTo(CombinedTestModel.Impl.class, CombinedTestModel.Abstract.class, CombinedTestModel.Controller.class));
	}

	@Test
	public void flatMapQuery() throws NoSuchMethodException {
		Set<Method> query =
			get(Annotations.of(
					Methods.of(CombinedTestModel.Impl.class))
				.flatMap(annotation ->
					Methods.of(annotation.annotationType())));

		Set<Method> query1 =
			get(AnnotationTypes.of(Methods.of(CombinedTestModel.Impl.class)).flatMap(Methods::of));

		assertThat(query,
			equalTo(
				CombinedTestModel.Post.class.getDeclaredMethod("value"),
				CombinedTestModel.Requests.class.getDeclaredMethod("value"),
				CombinedTestModel.Get.class.getDeclaredMethod("value")));

		assertEquals(query, query1);
	}

	@Test
	public void annotationToMap() {
		Set<Map<String, Object>> valueMaps =
			get(Annotations.of(
					Methods.of(CombinedTestModel.Impl.class))
				.filter(withNamePrefix("org.reflections"))
				.map(ReflectionUtils::toMap));

		// todo proper assert
		Set<String> collect = valueMaps.stream().map(Object::toString).sorted().collect(Collectors.toCollection(LinkedHashSet::new));
		assertThat(collect,
			equalTo(
				"{value=/get}",
				"{value=/post}",
				"{value=[" +
					"{method=PUT, value=/another}, " +
					"{method=PATCH, value=/another}]}"
			));
	}

	@Test
	public void mergedAnnotations() {
		Class<CombinedTestModel.Request> metaAnnotation = CombinedTestModel.Request.class;

		Reflections reflections = new Reflections(metaAnnotation, Scanners.values());

		Set<Class<?>> metaAnnotations =
			reflections.get(TypesAnnotated.getAllIncluding(metaAnnotation.getName()).asClass());

		QueryFunction<Store, CombinedTestModel.Request> mergedAnnotations =
			MethodsAnnotated.with(metaAnnotations)
				.as(Method.class)
				.map(method ->
					get(Annotations.of(method.getDeclaringClass())
						.add(Annotations.of(method))
						.filter(a -> metaAnnotations.contains(a.annotationType())))
						.stream()
						.collect(new AnnotationMergeCollector(method)))
				.map(map -> ReflectionUtils.toAnnotation(map, metaAnnotation));

		assertThat(
			reflections.get(mergedAnnotations.map(CombinedTestModel.Request::value)),
			equalTo("/base/post", "/base/get"));

		assertThat(
			reflections.get(mergedAnnotations.map(CombinedTestModel.Request::method)),
			equalTo("Post", "Get"));
	}
}

