/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *        http://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.apache.causeway.core.metamodel.facets.value.semantics;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.FormatStyle;

import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.causeway.applib.annotation.TimeZoneTranslation;
import org.apache.causeway.applib.annotation.ValueSemantics;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
import org.apache.causeway.core.metamodel.facets.FacetedMethod;
import org.apache.causeway.core.metamodel.facets.objectvalue.daterenderedadjust.DateRenderAdjustFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MaxFractionalDigitsFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MaxTotalDigitsFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MinFractionalDigitsFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MinIntegerDigitsFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.temporalformat.DateFormatStyleFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.temporalformat.TimeFormatStyleFacet;
import org.apache.causeway.core.metamodel.facets.objectvalue.temporalformat.TimeZoneTranslationFacet;

class ValueSemanticsAnnotationFacetFactoryTest
extends FacetFactoryTestAbstract {

    // -- MAX TOTAL DIGITS

    @Test
    void maxTotalPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(maxTotalDigits = 5)
            public BigDecimal getCost() { return null; }
        }
        propertyScenario(Order.class, "cost", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertMaxTotalDigits(facetedMethod, 5);
            assertDefaultMinIntegerDigits(facetedMethod);
            assertDefaultMaxFractionalDigits(facetedMethod);
            assertDefaultMinFractionalDigits(facetedMethod);
        });
    }

    @Test
    void maxTotalPickedUpOnActionParameter() {
        // given
        @SuppressWarnings("unused")
        class Order {
            public void updateCost(
                    @ValueSemantics(maxTotalDigits = 5)
                    final BigDecimal cost) { }
        }
        parameterScenario(Order.class, "updateCost", 0, (processParameterContext, facetHolder, facetedMethod, facetedMethodParameter) -> {
            // when
            newFacetFactory().processParams(processParameterContext);
            // then
            assertMaxTotalDigits(facetedMethodParameter, 5);
            assertDefaultMinIntegerDigits(facetedMethodParameter);
            assertDefaultMaxFractionalDigits(facetedMethodParameter);
            assertDefaultMinFractionalDigits(facetedMethodParameter);
        });
    }

    // -- MIN INTEGER DIGITS

    @Test
    void minIntegerPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(minIntegerDigits = 5)
            public BigDecimal getCost() { return null; }
        }
        propertyScenario(Order.class, "cost", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethod);
            assertMinIntegerDigits(facetedMethod, 5);
            assertDefaultMaxFractionalDigits(facetedMethod);
            assertDefaultMinFractionalDigits(facetedMethod);
        });
    }

    @Test
    void minIntegerPickedUpOnActionParameter() {
        // given
        @SuppressWarnings("unused")
        class Order {
            public void updateCost(
                    @ValueSemantics(minIntegerDigits = 5)
                    final BigDecimal cost) { }
        }
        parameterScenario(Order.class, "updateCost", 0, (processParameterContext, facetHolder, facetedMethod, facetedMethodParameter) -> {
            // when
            newFacetFactory().processParams(processParameterContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethodParameter);
            assertMinIntegerDigits(facetedMethodParameter, 5);
            assertDefaultMaxFractionalDigits(facetedMethodParameter);
            assertDefaultMinFractionalDigits(facetedMethodParameter);
        });
    }

    // -- MAX FRACTIONAL DIGITS

    @Test
    void maxFracionalPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(maxFractionalDigits = 5)
            public BigDecimal getCost() { return null; }
        }
        propertyScenario(Order.class, "cost", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethod);
            assertDefaultMinIntegerDigits(facetedMethod);
            assertMaxFractionalDigits(facetedMethod, 5);
            assertDefaultMinFractionalDigits(facetedMethod);
        });
    }

    @Test
    void maxFracionalPickedUpOnActionParameter() {
        // given
        @SuppressWarnings("unused")
        class Order {
            public void updateCost(
                    @ValueSemantics(maxFractionalDigits = 5)
                    final BigDecimal cost) { }
        }
        parameterScenario(Order.class, "updateCost", 0, (processParameterContext, facetHolder, facetedMethod, facetedMethodParameter) -> {
            // when
            newFacetFactory().processParams(processParameterContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethodParameter);
            assertDefaultMinIntegerDigits(facetedMethodParameter);
            assertMaxFractionalDigits(facetedMethodParameter, 5);
            assertDefaultMinFractionalDigits(facetedMethodParameter);
        });
    }

    // -- MIN FRACTIONAL DIGITS

    @Test
    void minFracionalPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(minFractionalDigits = 5)
            public BigDecimal getCost() { return null; }
        }
        propertyScenario(Order.class, "cost", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethod);
            assertDefaultMinIntegerDigits(facetedMethod);
            assertDefaultMaxFractionalDigits(facetedMethod);
            assertMinFractionalDigits(facetedMethod, 5);
        });
    }

    @Test
    void minFracionalPickedUpOnActionParameter() {
        // given
        @SuppressWarnings("unused")
        class Order {
            public void updateCost(
                    @ValueSemantics(minFractionalDigits = 5)
                    final BigDecimal cost) { }
        }
        parameterScenario(Order.class, "updateCost", 0, (processParameterContext, facetHolder, facetedMethod, facetedMethodParameter) -> {
            // when
            newFacetFactory().processParams(processParameterContext);
            // then
            assertDefaultMaxTotalDigits(facetedMethodParameter);
            assertDefaultMinIntegerDigits(facetedMethodParameter);
            assertDefaultMaxFractionalDigits(facetedMethodParameter);
            assertMinFractionalDigits(facetedMethodParameter, 5);
        });
    }

    // -- DIGITS ANNOTATION

    @Test
    void digitsAnnotationPickedUpOnProperty() {
        // given
        class Order {
            @jakarta.validation.constraints.Digits(integer=14, fraction=4)
            public BigDecimal getCost() { return null; }
        }
        propertyScenario(Order.class, "cost", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDigitsFacets(facetedMethod, 18, 4);
        });
    }

    @Test
    void digitsAnnotationPickedUpOnActionParameter() {
        // given
        @SuppressWarnings("unused")
        class Order {
            public void updateCost(
                    @jakarta.validation.constraints.Digits(integer=14, fraction=4)
                    final BigDecimal cost) { }
        }
        parameterScenario(Order.class, "updateCost", 0, (processParameterContext, facetHolder, facetedMethod, facetedMethodParameter) -> {
            // when
            newFacetFactory().processParams(processParameterContext);
            // then
            assertDigitsFacets(facetedMethodParameter, 18, 4);
        });
    }

    // -- CONSTRAINT MERGERS

    @Test
    void multipleAnnotationsMergedOnProperty() {
        // given
        class Order {

            @jakarta.validation.constraints.Digits(integer=14, fraction=4)
            @ValueSemantics(maxTotalDigits = 19)
            public BigDecimal maxTotalA() { return null; }

            @jakarta.validation.constraints.Digits(integer=14, fraction=5)
            @ValueSemantics(maxTotalDigits = 17)
            public BigDecimal maxTotalB() { return null; }

            @jakarta.validation.constraints.Digits(integer=14, fraction=4)
            @ValueSemantics(maxFractionalDigits = 5)
            public BigDecimal maxFracA() { return null; }

            @jakarta.validation.constraints.Digits(integer=14, fraction=5)
            @ValueSemantics(maxFractionalDigits = 4)
            public BigDecimal maxFracB() { return null; }

        }


        actionScenario(Order.class, "maxTotalA", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then - lowest bound wins
            assertMaxTotalDigits(facetedMethod, 18);
        });

        actionScenario(Order.class, "maxTotalB", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then - lowest bound wins
            assertMaxTotalDigits(facetedMethod, 17);
        });

        actionScenario(Order.class, "maxFracA", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then - lowest bound wins
            assertMaxFractionalDigits(facetedMethod, 4);
        });

        actionScenario(Order.class, "maxFracB", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then - lowest bound wins
            assertMaxFractionalDigits(facetedMethod, 4);
        });
    }

    // -- TEMPORAL FORMAT STYLE

    @Test
    void dateAdjustPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(dateRenderAdjustDays = ValueSemantics.AS_DAY_BEFORE)
            public LocalDateTime getDateTime() { return null; }
        }
        propertyScenario(Order.class, "dateTime", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDateRenderAdjustDays(facetedMethod, -1);
        });
    }

    @Test
    void timeZoneTranslationPickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(timeZoneTranslation = TimeZoneTranslation.NONE)
            public LocalDateTime getDateTimeA() { return null; }

            @ValueSemantics(timeZoneTranslation = TimeZoneTranslation.TO_LOCAL_TIMEZONE)
            public LocalDateTime getDateTimeB() { return null; }

        }
        propertyScenario(Order.class, "dateTimeA", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertTimeZoneTranslation(facetedMethod, TimeZoneTranslation.NONE);
        });
        propertyScenario(Order.class, "dateTimeB", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertTimeZoneTranslation(facetedMethod, TimeZoneTranslation.TO_LOCAL_TIMEZONE);
        });
    }

    @Test
    void dateFormatStylePickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(dateFormatStyle = FormatStyle.FULL)
            public LocalDateTime getDateTime() { return null; }
        }
        // when
        propertyScenario(Order.class, "dateTime", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertDateFormatStyle(facetedMethod, FormatStyle.FULL);
        });
    }

    @Test
    void timeFormatStylePickedUpOnProperty() {
        // given
        class Order {
            @ValueSemantics(timeFormatStyle = FormatStyle.FULL)
            public LocalDateTime getDateTime() { return null; }
        }
        propertyScenario(Order.class, "dateTime", (processMethodContext, facetHolder, facetedMethod) -> {
            // when
            newFacetFactory().process(processMethodContext);
            // then
            assertTimeFormatStyle(facetedMethod, FormatStyle.FULL);
        });
    }

    // -- HELPER

    ValueSemanticsAnnotationFacetFactory newFacetFactory() {
        return new ValueSemanticsAnnotationFacetFactory(getMetaModelContext());
    }

    private void assertDefaultMaxTotalDigits(final FacetHolder facetedMethod) {
        assertMaxTotalDigits(facetedMethod, 65);
    }

    private void assertDefaultMinIntegerDigits(final FacetHolder facetedMethod) {
        assertMinIntegerDigits(facetedMethod, 1);
    }

    private void assertDefaultMaxFractionalDigits(final FacetHolder facetedMethod) {
        assertMaxFractionalDigits(facetedMethod, 30);
    }

    private void assertDefaultMinFractionalDigits(final FacetHolder facetedMethod) {
        assertMinFractionalDigits(facetedMethod, 0);
    }

    private void assertMaxTotalDigits(
            final FacetHolder facetedMethod, final int maxTotalDigits) {
        final MaxTotalDigitsFacet facet = facetedMethod.getFacet(MaxTotalDigitsFacet.class);
        assertNotNull(facet);
        assertThat(facet.getMaxTotalDigits(), is(maxTotalDigits));
    }

    private void assertMinIntegerDigits(
            final FacetHolder facetedMethod, final int minIntegerDigits) {
        final MinIntegerDigitsFacet facet = facetedMethod.getFacet(MinIntegerDigitsFacet.class);
        assertNotNull(facet);
        assertThat(facet.getMinIntegerDigits(), is(minIntegerDigits));
    }

    private void assertMaxFractionalDigits(
            final FacetHolder facetedMethod, final int maxFractionalDigits) {
        final MaxFractionalDigitsFacet facet = facetedMethod.getFacet(MaxFractionalDigitsFacet.class);
        assertNotNull(facet);
        assertThat(facet.getMaxFractionalDigits(), is(maxFractionalDigits));
    }

    private void assertMinFractionalDigits(
            final FacetHolder facetedMethod, final int minFractionalDigits) {
        final MinFractionalDigitsFacet facet = facetedMethod.getFacet(MinFractionalDigitsFacet.class);
        assertNotNull(facet);
        assertThat(facet.getMinFractionalDigits(), is(minFractionalDigits));
    }

    private void assertDigitsFacets(
            final FacetHolder facetedMethod, final int maxTotalDigits, final int maxFractionalDigits) {
        if(maxTotalDigits>=0) {
            final MaxTotalDigitsFacet facet = facetedMethod.getFacet(MaxTotalDigitsFacet.class);
            assertNotNull(facet);
            assertTrue(facet instanceof MaxTotalDigitsFacetFromJavaxValidationDigitsAnnotation);
            assertThat(facet.getMaxTotalDigits(), is(maxTotalDigits));
        }

        if(maxFractionalDigits>=0) {
            final MaxFractionalDigitsFacet facet = facetedMethod.getFacet(MaxFractionalDigitsFacet.class);
            assertNotNull(facet);
            assertTrue(facet instanceof MaxFractionalDigitsFacetFromJavaxValidationDigitsAnnotation);
            assertThat(facet.getMaxFractionalDigits(), is(maxFractionalDigits));
        }
    }

    private void assertDateRenderAdjustDays(
            final FacetedMethod facetedMethod, final int adjustDays) {
        final DateRenderAdjustFacet facet = facetedMethod.getFacet(DateRenderAdjustFacet.class);
        assertNotNull(facet);
        assertThat(facet.getDateRenderAdjustDays(), is(adjustDays));
    }

    private void assertDateFormatStyle(
            final FacetedMethod facetedMethod, final FormatStyle formatStyle) {
        final DateFormatStyleFacet facet = facetedMethod.getFacet(DateFormatStyleFacet.class);
        assertNotNull(facet);
        assertThat(facet.getDateFormatStyle(), is(formatStyle));
    }

    private void assertTimeFormatStyle(
            final FacetedMethod facetedMethod, final FormatStyle formatStyle) {
        final TimeFormatStyleFacet facet = facetedMethod.getFacet(TimeFormatStyleFacet.class);
        assertNotNull(facet);
        assertThat(facet.getTimeFormatStyle(), is(formatStyle));
    }

    private void assertTimeZoneTranslation(
            final FacetedMethod facetedMethod, final TimeZoneTranslation timeZoneTranslation) {
        final TimeZoneTranslationFacet facet = facetedMethod.getFacet(TimeZoneTranslationFacet.class);
        assertNotNull(facet);
        assertThat(facet.getTimeZoneTranslation(), is(timeZoneTranslation));
    }

}
