/*
 * Copyright 2018, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.validate;

import com.google.common.collect.ImmutableList;
import com.google.protobuf.Message;
import com.google.protobuf.Timestamp;
import io.spine.option.IfInvalidOption;
import io.spine.option.OptionsProto;
import io.spine.option.Time;
import io.spine.option.TimeOption;

import java.util.List;

import static io.spine.base.Time.getCurrentTime;
import static io.spine.option.Time.FUTURE;
import static io.spine.option.Time.TIME_UNDEFINED;
import static io.spine.protobuf.AnyPacker.pack;
import static io.spine.validate.Validate.isDefault;

/**
 * Validates fields of type {@link Message}.
 *
 * @author Alexander Litus
 */
class MessageFieldValidator extends FieldValidator<Message> {

    private final TimeOption timeConstraint;
    private final boolean fieldIsTimestamp;

    /**
     * Creates a new validator instance.
     *
     * @param fieldContext the context of the field to validate
     * @param fieldValues  values to validate
     * @param strict       if {@code true} the validator would assume that the field
     *                     is required even if the corresponding field option is not present
     */
    MessageFieldValidator(FieldContext fieldContext,
                          Object fieldValues,
                          boolean strict) {
        super(fieldContext, FieldValidator.<Message>toValueList(fieldValues), strict);
        this.timeConstraint = getFieldOption(OptionsProto.when);
        this.fieldIsTimestamp = isTimestamp();
    }

    @Override
    protected void validateOwnRules() {
        boolean validateFields = getValidateOption();
        if (validateFields) {
            validateFields();
        }
        if (fieldIsTimestamp) {
            validateTimestamps();
        }
    }

    @Override
    protected boolean isValueNotSet(Message value) {
        boolean result = isDefault(value);
        return result;
    }

    private boolean isTimestamp() {
        ImmutableList<Message> values = getValues();
        Message value = values.isEmpty()
                        ? null
                        : values.get(0);
        boolean result = value instanceof Timestamp;
        return result;
    }

    private void validateFields() {
        for (Message value : getValues()) {
            MessageValidator validator = MessageValidator.newInstance(getFieldContext());
            List<ConstraintViolation> violations = validator.validate(value);
            if (!violations.isEmpty()) {
                addViolation(newValidViolation(value, violations));
            }
        }
    }

    private void validateTimestamps() {
        Time when = timeConstraint.getIn();
        if (when == TIME_UNDEFINED) {
            return;
        }
        Timestamp now = getCurrentTime();
        for (Message value : getValues()) {
            Timestamp time = (Timestamp) value;
            if (isTimeInvalid(time, when, now)) {
                addViolation(newTimeViolation(time));
                return; // return because one error message is enough for the "time" option
            }
        }
    }

    /**
     * Checks the time.
     *
     * @param timeToCheck  a timestamp to check
     * @param whenExpected the time when the checked timestamp should be
     * @param now          the current moment
     * @return {@code true} if the time is valid according to {@code whenExpected} parameter,
     * {@code false} otherwise
     */
    private static boolean isTimeInvalid(Timestamp timeToCheck, Time whenExpected, Timestamp now) {
        boolean isValid = (whenExpected == FUTURE)
                          ? isLaterThan(timeToCheck, /*than*/ now)
                          : isLaterThan(now, /*than*/ timeToCheck);
        boolean isInvalid = !isValid;
        return isInvalid;
    }

    private static boolean isLaterThan(Timestamp t1, Timestamp t2) {
        int result = Long.compare(t1.getSeconds(), t2.getSeconds());
        result = (result == 0)
                 ? Integer.compare(t1.getNanos(), t2.getNanos())
                 : result;
        return result > 0;
    }

    private ConstraintViolation newTimeViolation(Timestamp fieldValue) {
        String msg = getErrorMsgFormat(timeConstraint, timeConstraint.getMsgFormat());
        String when = timeConstraint.getIn()
                                    .toString()
                                    .toLowerCase();
        ConstraintViolation violation = ConstraintViolation.newBuilder()
                                                           .setMsgFormat(msg)
                                                           .addParam(when)
                                                           .setFieldPath(getFieldPath())
                                                           .setFieldValue(pack(fieldValue))
                                                           .build();
        return violation;
    }

    private ConstraintViolation newValidViolation(Message fieldValue,
                                                  Iterable<ConstraintViolation> violations) {
        IfInvalidOption ifInvalid = ifInvalid();
        String msg = getErrorMsgFormat(ifInvalid, ifInvalid.getMsgFormat());
        ConstraintViolation violation = ConstraintViolation.newBuilder()
                                                           .setMsgFormat(msg)
                                                           .setFieldPath(getFieldPath())
                                                           .setFieldValue(pack(fieldValue))
                                                           .addAllViolation(violations)
                                                           .build();
        return violation;
    }
}

@@ -2609,16 +2609,54 @@
 Option()
+ && !isValueNotSet(getValues().get(0))
 ;%0A      

Add fix for the failing test case