content: /*
   *  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.viewer.wicket.ui.test.components.scalars;
  
  import java.io.Serializable;
  import java.math.BigDecimal;
  import java.util.Locale;
  import java.util.Objects;
  
  import org.apache.wicket.util.convert.ConversionException;
  import org.assertj.core.util.Arrays;
  import org.mockito.Mockito;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertNotNull;
  import static org.junit.jupiter.api.Assertions.assertNull;
  import static org.junit.jupiter.api.Assertions.assertThrows;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  
  import org.apache.causeway.applib.Identifier;
  import org.apache.causeway.applib.clock.VirtualClock;
  import org.apache.causeway.applib.id.LogicalType;
  import org.apache.causeway.applib.locale.UserLocale;
  import org.apache.causeway.applib.services.clock.ClockService;
  import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
  import org.apache.causeway.applib.services.iactnlayer.InteractionService;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsAbstract;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
  import org.apache.causeway.commons.functional.ThrowingRunnable;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.commons.internal.base._Strings;
  import org.apache.causeway.core.config.CausewayConfiguration;
  import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting;
  import org.apache.causeway.core.metamodel.commons.ViewOrEditMode;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
  import org.apache.causeway.core.security._testing.InteractionService_forTesting;
  import org.apache.causeway.viewer.wicket.model.value.ConverterBasedOnValueSemantics;
  
  import lombok.AllArgsConstructor;
  import org.jspecify.annotations.NonNull;
  
  public class ConverterTester<T extends Serializable> {
  
      private final Locale LOCALE_NOT_USED = null;
  
      private final InteractionService interactionService;
      private final MetaModelContext mmc;
      private Scenario scenario;
  
      @AllArgsConstructor
      private class Scenario {
          Locale locale;
          ConverterBasedOnValueSemantics<T> converter;
      }
  
      public ConverterTester(final Class<T> valueType, final ValueSemanticsAbstract<T> valueSemantics,
              final Object ...additionalSingletons) {
          this(valueType, valueSemantics, VirtualClock.frozenTestClock(), additionalSingletons);
      }
  
      public ConverterTester(
              final @NonNull Class<T> valueType,
              final @NonNull ValueSemanticsAbstract<T> valueSemantics,
              final @NonNull VirtualClock virtualClock,
              final Object ...additionalSingletons) {
  
          mmc = MetaModelContext_forTesting.builder()
                  .valueSemantic(valueSemantics)
                  .singleton(new ClockService(null) {
                      @Override public VirtualClock getClock() {
                          return virtualClock;
                      }
                  })
                  .singletons(Arrays.asList(additionalSingletons))
                  .interactionService(interactionService = new InteractionService_forTesting())
                  .memberExecutor(Mockito.mock(MemberExecutorService.class))
                  .build();
  
          mmc.getServiceInjector().injectServicesInto(valueSemantics);
  
          // pre-requisites for testing
          var identifier = Identifier.classIdentifier(LogicalType.fqcn(valueType));
          var reg = mmc.getServiceRegistry().lookupServiceElseFail(ValueSemanticsResolver.class);
          assertNotNull(reg.selectValueSemantics(identifier, valueType));
          assertTrue(reg.selectValueSemantics(identifier, valueType).isNotEmpty());
          assertNotNull(mmc.getServiceRegistry().lookupServiceElseFail(InteractionService.class));
          assertNotNull(mmc.getInteractionService());
      }
  
      public void setScenario(
              final @NonNull Locale locale,
              final @NonNull ConverterBasedOnValueSemantics<T> converter) {
          this.scenario = new Scenario(locale, converter);
      }
  
      public ConverterBasedOnValueSemantics<T> converterForProperty(
              final Class<?> type,
              final String propertyId,
              final ViewOrEditMode representation) {
          var customerSpec = mmc.getSpecificationLoader().specForTypeElseFail(type);
          var prop = customerSpec.getPropertyElseFail("value");
          var propType = (Class<T>) prop.getElementType().getCorrespondingClass();
          return new ConverterBasedOnValueSemantics<T>(propType, prop, representation);
      }
  
      public void runWithInteraction(final @NonNull ThrowingRunnable runnable) {
  
          Objects.requireNonNull(scenario, "must select a scenario before using this method");
  
          interactionService.run(
                  InteractionContext.builder().locale(UserLocale.valueOf(scenario.locale)).build(),
                  runnable);
      }
  
      public void assertRoundtrip(
              final @NonNull T value,
              final @NonNull String valueAsText) {
          assertRoundtrip(value, valueAsText, valueAsText);
      }
  
      public CausewayConfiguration.ValueTypes.BigDecimal getConfigurationForBigDecimalValueType() {
          return mmc.getConfiguration().getValueTypes().getBigDecimal();
      }
  
      /**
       * @param value - non-null
       * @param valueAsText - parser input
       * @param expectedText - reversed parser output
       */
      public void assertRoundtrip(
              final @NonNull T value,
              final @NonNull String valueAsText,
              final @NonNull String expectedText) {
          runWithInteraction(()->{
  
              var parsedValue = scenario.converter.convertToObject(valueAsText, LOCALE_NOT_USED);
  
              if(value instanceof BigDecimal) {
                  _Assert.assertNumberEquals(
                          (BigDecimal)value, (BigDecimal)parsedValue);
              } else if(value instanceof java.util.Date) {
                  assertTemporalEquals(
                          (java.util.Date)value, (java.util.Date)parsedValue);
              } else {
                  assertEquals(
                          value, parsedValue);
              }
  
              assertEquals(
                      expectedText, scenario.converter.convertToString(value, LOCALE_NOT_USED));
          });
      }
  
      public void assertHandlesEmpty() {
          runWithInteraction(()->{
              assertNull(scenario.converter.convertToObject(null, LOCALE_NOT_USED));
              assertNull(scenario.converter.convertToObject("", LOCALE_NOT_USED));
              assertTrue(_Strings.isEmpty(scenario.converter.convertToString(null, LOCALE_NOT_USED)));
          });
      }
  
      public void assertConversionFailure(
              final @NonNull String valueAsInvalidText,
              final @NonNull String expectedMessage) {
  
          runWithInteraction(()->{
              var ex = assertThrows(ConversionException.class, //TODO find a more generic exception type
                      ()->scenario.converter.convertToObject(valueAsInvalidText, LOCALE_NOT_USED));
              assertEquals(expectedMessage, ex.getMessage());
          });
      }
  
      // -- HELPER
  
      @SuppressWarnings("deprecation")
      private void assertTemporalEquals(final java.util.Date a, final java.util.Date b) {
          assertEquals(a.getDay(), b.getDay());
          assertEquals(a.getMonth(), b.getMonth());
          assertEquals(a.getYear(), b.getYear());
          assertEquals(a.getTime(), b.getTime());
      }
  
  }
  