/*
 * Hibernate Validator, declare and validate application constraints
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
 */
package org.hibernate.validator.test.internal.engine.valuehandling;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Type;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.Payload;
import javax.validation.UnexpectedTypeException;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.constraints.Future;
import javax.validation.constraints.Min;
import javax.validation.constraints.Null;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import org.hibernate.validator.internal.util.TypeResolutionHelper;
import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper;
import org.hibernate.validator.testutil.TestForIssue;
import org.hibernate.validator.testutils.ValidatorUtil;
import org.hibernate.validator.testutils.constraints.NotBlankTypeUse;
import org.hibernate.validator.valuehandling.UnwrapValidatedValue;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectConstraintTypes;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectPropertyPaths;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNumberOfViolations;

/**
 * Test the various scenarios for explicit and implicit unwrapping of values.
 *
 * @author Hardy Ferentschik
 */
@TestForIssue(jiraKey = "HV-925")
@SuppressWarnings("unused")
public class UnwrapModesTest {
	private Validator validatorWithUnwrapper;
	private Validator validatorWithoutUnwrapper;

	@BeforeClass
	public void setupValidator() {
		validatorWithoutUnwrapper = ValidatorUtil.getValidator();

		validatorWithUnwrapper = ValidatorUtil.getConfiguration()
				.addValidatedValueHandler( new ValueHolderUnwrapper() )
				.buildValidatorFactory()
				.getValidator();
	}

	@Test(expectedExceptions = UnexpectedTypeException.class, expectedExceptionsMessageRegExp = "HV000030.*")
	public void no_constraint_validator_for_wrapped_and_unwrapped_value_throws_exception() {
		validatorWithoutUnwrapper.validate( new Foo() );
	}

	@Test(expectedExceptions = UnexpectedTypeException.class, expectedExceptionsMessageRegExp = "HV000030.*")
	public void no_constraint_validator_for_unwrapped_value_throws_exception() {
		validatorWithUnwrapper.validate( new Fubar() );
	}

	@Test(expectedExceptions = ValidationException.class, expectedExceptionsMessageRegExp = "HV000182.*")
	public void missing_value_unwrapper_throws_exception() {
		validatorWithoutUnwrapper.validate( new Foobar() );
	}

	@Test
	public void unwrapped_values_get_validated() {
		Set<ConstraintViolation<Bar>> constraintViolations = validatorWithUnwrapper.validate( new Bar() );
		assertNumberOfViolations( constraintViolations, 2 );
		assertCorrectPropertyPaths( constraintViolations, "integerHolder", "stringHolder" );
		assertCorrectConstraintTypes( constraintViolations, Min.class, NotBlankTypeUse.class );
	}

	@Test
	public void validate_wrapper_itself_if_there_is_no_unwrapper_and_no_validator_for_wrapped_value() {
		Set<ConstraintViolation<Qux>> constraintViolations = validatorWithoutUnwrapper.validate( new Qux() );
		assertNumberOfViolations( constraintViolations, 1 );
		assertCorrectPropertyPaths( constraintViolations, "integerHolder" );
		assertCorrectConstraintTypes( constraintViolations, ValueHolderConstraint.class );
	}

	@Test
	public void validate_wrapper_itself_if_there_is_unwrapper_but_only_constraint_validator_for_wrapper() {
		Set<ConstraintViolation<Qux>> constraintViolations = validatorWithUnwrapper.validate( new Qux() );
		assertNumberOfViolations( constraintViolations, 1 );
		assertCorrectPropertyPaths( constraintViolations, "integerHolder" );
		assertCorrectConstraintTypes( constraintViolations, ValueHolderConstraint.class );

		// execute validation twice to ensure that the handling for this case is not subjective to caching (see HV-976)
		constraintViolations = validatorWithUnwrapper.validate( new Qux() );
		assertNumberOfViolations( constraintViolations, 1 );
		assertCorrectPropertyPaths( constraintViolations, "integerHolder" );
		assertCorrectConstraintTypes( constraintViolations, ValueHolderConstraint.class );
	}

	@Test
	public void validate_wrapper_itself_if_there_is_no_unwrapper() {
		Set<ConstraintViolation<Baz>> constraintViolations = validatorWithoutUnwrapper.validate( new Baz() );
		assertNumberOfViolations( constraintViolations, 1 );
		assertCorrectPropertyPaths( constraintViolations, "integerHolder" );
		assertCorrectConstraintTypes( constraintViolations, Null.class );
	}

	@Test(expectedExceptions = UnexpectedTypeException.class, expectedExceptionsMessageRegExp = "HV000186.*")
	public void constraint_declaration_exception_if_there_are_validators_for_wrapper_and_wrapped_value() {
		validatorWithUnwrapper.validate( new Baz() );
	}

	public class Foo {
		// no constraint validator defined for @DummyConstraint and no unwrapper for ValueHolder
		@DummyConstraint
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );
	}

	public class Fubar {
		// @UnwrapValidatedValue annotation optional
		@Future // there is no future validator for integers!
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );
	}

	public class Foobar {
		@UnwrapValidatedValue
		@Min(10)
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );
	}

	public class Bar {
		@Min(10)
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );

		private ValueHolder<@NotBlankTypeUse String> stringHolder = new ValueHolder<>( "" );
	}

	public class Baz {
		@Null
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );
	}

	public class Qux {
		@ValueHolderConstraint
		private ValueHolder<Integer> integerHolder = new ValueHolder<>( 5 );
	}

	class ValueHolder<T> {

		ValueHolder(T value) {
			this.value = value;
		}

		private T value;

		public T getValue() {
			return value;
		}
	}

	@Documented
	@Constraint(validatedBy = {})
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
	@Retention(RUNTIME)
	public @interface DummyConstraint {
		String message() default "dummy constraint";

		Class<?>[] groups() default {};

		Class<? extends Payload>[] payload() default {};
	}

	@Documented
	@Constraint(validatedBy = { ValueHandlerConstraintValidator.class })
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
	@Retention(RUNTIME)
	public @interface ValueHolderConstraint {
		String message() default "value holder constraint";

		Class<?>[] groups() default {};

		Class<? extends Payload>[] payload() default {};
	}

	public static class ValueHandlerConstraintValidator
			implements ConstraintValidator<ValueHolderConstraint, ValueHolder> {

		@Override
		public void initialize(ValueHolderConstraint constraintAnnotation) {

		}

		@Override
		public boolean isValid(ValueHolder value, ConstraintValidatorContext context) {
			return false;
		}
	}

	class ValueHolderUnwrapper extends ValidatedValueUnwrapper<ValueHolder> {
		TypeResolver typeResolver = new TypeResolutionHelper().getTypeResolver();

		@Override
		public Object handleValidatedValue(ValueHolder valueHolder) {
			return valueHolder.getValue();
		}

		@Override
		public Type getValidatedValueType(Type valueType) {
			ResolvedType resolvedType = typeResolver.resolve( valueType );
			return resolvedType.typeParametersFor( ValueHolder.class ).get( 0 ).getErasedType();
		}
	}

}
