/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2021  Delft University of Technology
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package test.labracore;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.mockito.stubbing.Answer;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public abstract class LabracoreApiMocker<T, ID> {
	/**
	 * Model mapper used when needed.
	 */
	protected final ModelMapper mapper = new ModelMapper();

	/**
	 * The saved data mapped by its id.
	 */
	public final Map<ID, T> data = new HashMap<>();

	/**
	 * Mocks the getById method for the controller api.
	 */
	public abstract void mock();

	/**
	 * The answer to a call that can be referred to a local data map.
	 *
	 * @param  map   The map to find data in.
	 * @param  <TID> The type of the id to find data by.
	 * @return       An answer representing a lookup in the map.
	 */
	protected <TID> Answer<Flux<T>> getAllByMap(Map<TID, T> map) {
		return invocation -> {
			List<TID> ids = invocation.getArgument(0);
			return Flux.fromIterable(ids == null ? List.of() : ids)
					.flatMap(id -> Mono.justOrEmpty(map.get(id)));
		};
	}

	/**
	 * The answer to a call that can be referred to a local data map.
	 *
	 * @param  map   The map to find data in.
	 * @param  clazz The class to map to in the end.
	 * @param  <TID> The type of the id to find data by.
	 * @param  <R>   The type to map the result to.
	 * @return       An answer representing a lookup in the map.
	 */
	protected <TID, R> Answer<Mono<R>> getOneByMap(Map<TID, T> map, Class<R> clazz) {
		return invocation -> getOneByMap(map).answer(invocation).map(dto -> mapper.map(dto, clazz));
	}

	/**
	 * The answer to a call that can be referred to a local data map.
	 *
	 * @param  map   The map to find data in.
	 * @param  <TID> The type of the id to find data by.
	 * @return       An answer representing a lookup in the map.
	 */
	protected <TID> Answer<Mono<T>> getOneByMap(Map<TID, T> map) {
		return invocation -> {
			TID id = invocation.getArgument(0);
			return Mono.just(Optional.ofNullable(map.get(id))
					.orElseThrow(() -> new HttpClientErrorException(HttpStatus.NOT_FOUND)));
		};
	}

	/**
	 * The answer to a get all-by-ids call.
	 */
	protected Answer<Flux<T>> getAllByIds = getAllByMap(data);

	/**
	 * The answer to a get all call.
	 */
	protected Answer<Flux<T>> getAll = invocation -> Flux.fromIterable(data.values());

	/**
	 * The answer to get all call using a conversion to a specific view.
	 *
	 * @param  toType The class to convert to before returning.
	 * @param  <R>    The type to convert to.
	 * @return        An answer to a get all call.
	 */
	protected <R> Answer<Flux<R>> getAll(Class<R> toType) {
		return invocation -> Flux.fromStream(data.values().stream().map(dto -> mapper.map(dto, toType)));
	}

	/**
	 * Gets the id of the data type from the data type.
	 *
	 * @param  t The data.
	 * @return   The id of the data.
	 */
	public abstract ID getId(T t);

	/**
	 * Saves a data object as if saved on the labracore api.
	 *
	 * @param t The data to save.
	 */
	public T save(T t) {
		data.put(getId(t), t);
		return t;
	}

	/**
	 * Gets an entry of this labracore mock api.
	 *
	 * @param  id The id of the entry.
	 * @return    An optional either containing the data or empty if none was registered.
	 */
	public Optional<T> getById(ID id) {
		return Optional.ofNullable(data.get(id));
	}
}
