/*
 * Copyright 2013-2023 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.data.repository.core.support;

import static org.assertj.core.api.Assertions.*;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadataUnitTests.DummyGenericRepositorySupport;

/**
 * Unit tests for {@link DefaultRepositoryInformation}.
 *
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Mark Paluch
 * @author Johannes Englmeier
 */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class DefaultRepositoryInformationUnitTests {

	@SuppressWarnings("rawtypes") static final Class<DummyGenericRepositorySupport> REPOSITORY = DummyGenericRepositorySupport.class;

	@Mock FooRepositoryCustom customImplementation;

	@Test
	void discoversRepositoryBaseClassMethod() throws Exception {

		var method = FooRepository.class.getMethod("findById", Integer.class);
		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		var information = new DefaultRepositoryInformation(metadata, REPOSITORY,
				RepositoryComposition.empty().withMethodLookup(MethodLookups.forRepositoryTypes(metadata)));

		var reference = information.getTargetClassMethod(method);
		assertThat(reference.getDeclaringClass()).isEqualTo(REPOSITORY);
		assertThat(reference.getName()).isEqualTo("findById");
	}

	@Test
	void discoveresNonRepositoryBaseClassMethod() throws Exception {

		var method = FooRepository.class.getMethod("findById", Long.class);

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		var information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty().withMethodLookup(MethodLookups.forRepositoryTypes(metadata)));

		assertThat(information.getTargetClassMethod(method)).isEqualTo(method);
	}

	@Test
	void discoversCustomlyImplementedCrudMethod() throws SecurityException, NoSuchMethodException {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.just(customImplementation));

		var source = FooRepositoryCustom.class.getMethod("save", User.class);
		var expected = customImplementation.getClass().getMethod("save", User.class);

		assertThat(information.getTargetClassMethod(source)).isEqualTo(expected);
	}

	@Test
	void considersIntermediateMethodsAsFinderMethods() {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		assertThat(information.hasCustomMethod()).isFalse();
	}

	@Test // DATACMNS-151
	void doesNotConsiderManuallyDefinedSaveMethodAQueryMethod() {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CompletePageableAndSortingRepository.class,
				RepositoryComposition.empty());

		assertThat(information.getQueryMethods()).isEmpty();
	}

	@Test // DATACMNS-151
	void doesNotConsiderRedeclaredSaveMethodAQueryMethod() throws Exception {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		var saveMethod = BaseRepository.class.getMethod("save", Object.class);
		var deleteMethod = BaseRepository.class.getMethod("delete", Object.class);

		Iterable<Method> queryMethods = information.getQueryMethods();

		assertThat(queryMethods).doesNotContain(saveMethod, deleteMethod);
	}

	@Test
	void onlyReturnsMostConcreteQueryMethod() throws Exception {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		var intermediateMethod = BaseRepository.class.getMethod("genericMethodToOverride", String.class);
		var concreteMethod = ConcreteRepository.class.getMethod("genericMethodToOverride", String.class);

		Iterable<Method> queryMethods = information.getQueryMethods();

		assertThat(queryMethods).contains(concreteMethod);
		assertThat(queryMethods).doesNotContain(intermediateMethod);
	}

	@Test // DATACMNS-193
	void detectsQueryMethodCorrectly() {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		var queryMethod = getMethodFrom(ConcreteRepository.class, "findBySomethingDifferent");

		assertThat(information.getQueryMethods()).contains(queryMethod);
		assertThat(information.isQueryMethod(queryMethod)).isTrue();
	}

	@Test // DATACMNS-364
	void ignoresCrudMethodsAnnotatedWithQuery() throws Exception {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(ConcreteRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		var method = BaseRepository.class.getMethod("findById", Object.class);

		assertThat(information.getQueryMethods()).contains(method);
	}

	@Test // DATACMNS-385
	void findsTargetSaveForIterableIfEntityImplementsIterable() throws Exception {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(BossRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		var method = BossRepository.class.getMethod("saveAll", Iterable.class);
		var reference = CrudRepository.class.getMethod("saveAll", Iterable.class);

		assertThat(information.getTargetClassMethod(method)).isEqualTo(reference);
	}

	@Test // DATACMNS-441
	void getQueryShouldNotReturnAnyBridgeMethods() {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomDefaultRepositoryMethodsRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.empty());

		assertThat(information.getQueryMethods()).allMatch(method -> !method.isBridge());
	}

	@Test // DATACMNS-854
	void discoversCustomlyImplementedCrudMethodWithGenerics() throws SecurityException, NoSuchMethodException {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.just(customImplementation));

		var source = FooRepositoryCustom.class.getMethod("exists", Object.class);
		var expected = customImplementation.getClass().getMethod("exists", Object.class);

		assertThat(information.getTargetClassMethod(source)).isEqualTo(expected);
	}

	@Test // DATACMNS-912
	void discoversCustomlyImplementedCrudMethodWithGenericParameters() throws Exception {

		var customImplementation = new GenericsSaveRepositoryImpl();
		RepositoryMetadata metadata = new DefaultRepositoryMetadata(GenericsSaveRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, RepositoryFactorySupport.class,
				RepositoryComposition.just(customImplementation).withMethodLookup(MethodLookups.forRepositoryTypes(metadata)));

		var customBaseRepositoryMethod = GenericsSaveRepository.class.getMethod("save", Object.class);
		assertThat(information.isCustomMethod(customBaseRepositoryMethod)).isTrue();
	}

	@Test // DATACMNS-939
	void ignoresStaticMethod() throws SecurityException, NoSuchMethodException {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.just(customImplementation));

		var method = FooRepository.class.getMethod("staticMethod");

		assertThat(information.getQueryMethods()).doesNotContain(method);
	}

	@Test // DATACMNS-939
	void ignoresDefaultMethod() throws SecurityException, NoSuchMethodException {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(FooRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class,
				RepositoryComposition.just(customImplementation));

		var method = FooRepository.class.getMethod("defaultMethod");

		assertThat(information.getQueryMethods()).doesNotContain(method);
	}

	@Test // DATACMNS-943
	void usesCorrectSaveOverload() throws Exception {

		RepositoryMetadata metadata = new DefaultRepositoryMetadata(DummyRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, DummyRepositoryImpl.class,
				RepositoryComposition.empty());

		var method = DummyRepository.class.getMethod("saveAll", Iterable.class);

		assertThat(information.getTargetClassMethod(method))
				.isEqualTo(DummyRepositoryImpl.class.getMethod("saveAll", Iterable.class));
	}

	@Test // DATACMNS-1008, DATACMNS-912, DATACMNS-854
	void discoversCustomlyImplementedCrudMethodWithoutGenericParameters() throws Exception {

		var customImplementation = new SimpleSaveRepositoryImpl();
		RepositoryMetadata metadata = new DefaultRepositoryMetadata(SimpleSaveRepository.class);
		RepositoryInformation information = new DefaultRepositoryInformation(metadata, RepositoryFactorySupport.class,
				RepositoryComposition.just(customImplementation).withMethodLookup(MethodLookups.forRepositoryTypes(metadata)));

		var customBaseRepositoryMethod = SimpleSaveRepository.class.getMethod("save", Object.class);
		assertThat(information.isCustomMethod(customBaseRepositoryMethod)).isTrue();
	}

	private static Method getMethodFrom(Class<?> type, String name) {

		return Arrays.stream(type.getMethods())//
				.filter(method -> method.getName().equals(name))//
				.findFirst()//
				.orElseThrow(() -> new IllegalStateException("No method found with name ".concat(name)));
	}

	@Target(ElementType.METHOD)
	@Retention(RetentionPolicy.RUNTIME)
	@QueryAnnotation
	@interface MyQuery {
	}

	interface FooRepository extends CrudRepository<User, Integer>, FooRepositoryCustom {

		// Redeclared method
		Optional<User> findById(Integer primaryKey);

		// Not a redeclared method
		User findById(Long primaryKey);

		static void staticMethod() {}

		default void defaultMethod() {}
	}

	interface FooSuperInterfaceWithGenerics<T> {
		boolean exists(T id);
	}

	interface FooRepositoryCustom extends FooSuperInterfaceWithGenerics<User> {
		User save(User user);
	}

	@SuppressWarnings("unused")
	private class User {

		private String firstname;

		String getAddress() {

			return null;
		}
	}

	static class Boss implements Iterable<User> {

		@Override
		public Iterator<User> iterator() {
			return Collections.<User> emptySet().iterator();
		}
	}

	interface BaseRepository<S, ID> extends CrudRepository<S, ID> {

		S findBySomething(String something);

		S genericMethodToOverride(String something);

		<K extends S> K save(K entity);

		void delete(S entity);

		@MyQuery
		Optional<S> findById(ID id);
	}

	interface ConcreteRepository extends BaseRepository<User, Integer> {

		User findBySomethingDifferent(String somethingDifferent);

		User genericMethodToOverride(String something);
	}

	interface ReadOnlyRepository<T, ID> extends Repository<T, ID> {

		T findById(ID id);

		Iterable<T> findAll();

		Page<T> findAll(Pageable pageable);

		List<T> findAll(Sort sort);

		boolean existsById(ID id);

		long count();
	}

	interface CustomRepository extends ReadOnlyRepository<Object, Long> {

		Object save(Object object);
	}

	interface BossRepository extends CrudRepository<Boss, Long> {}

	interface CustomDefaultRepositoryMethodsRepository extends CrudRepository<User, Integer> {

		@MyQuery
		List<User> findAll();
	}

	// DATACMNS-854, DATACMNS-912

	interface GenericsSaveRepository extends CrudRepository<Sample, Long> {}

	static class GenericsSaveRepositoryImpl {

		public <T extends Sample> T save(T entity) {
			return entity;
		}
	}

	static class Sample {}

	interface DummyRepository extends CrudRepository<User, Integer> {

		@Override
		<S extends User> List<S> saveAll(Iterable<S> entites);
	}

	static class DummyRepositoryImpl<T, ID> implements CrudRepository<T, ID> {

		private CrudRepository<T, ID> delegate;

		public <S extends T> S save(S entity) {
			return this.delegate.save(entity);
		}

		public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
			return this.delegate.saveAll(entities);
		}

		public Optional<T> findById(ID id) {
			return this.delegate.findById(id);
		}

		public boolean existsById(ID id) {
			return this.delegate.existsById(id);
		}

		public Iterable<T> findAll() {
			return this.delegate.findAll();
		}

		public Iterable<T> findAllById(Iterable<ID> ids) {
			return this.delegate.findAllById(ids);
		}

		public long count() {
			return this.delegate.count();
		}

		public void deleteById(ID id) {
			this.delegate.deleteById(id);
		}

		public void delete(T entity) {
			this.delegate.delete(entity);
		}

		public void deleteAllById(Iterable<? extends ID> ids) {
			this.delegate.deleteAllById(ids);
		}

		public void deleteAll(Iterable<? extends T> entities) {
			this.delegate.deleteAll(entities);
		}

		public void deleteAll() {
			this.delegate.deleteAll();
		}
	}

	// DATACMNS-1008, DATACMNS-854, DATACMNS-912

	interface SimpleSaveRepository extends CrudRepository<Sample, Long> {}

	static class SimpleSaveRepositoryImpl {

		public Sample save(Sample entity) {
			return entity;
		}
	}

	interface CompletePageableAndSortingRepository<T, ID> extends CrudRepository<T, ID>, PagingAndSortingRepository<T, ID> {
	}

}
