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.core.metamodel.facets;
  
  import java.util.Optional;
  import java.util.function.BiConsumer;
  
  import org.junit.jupiter.api.AfterEach;
  import org.junit.jupiter.api.BeforeEach;
  import org.mockito.Mockito;
  
  import static org.junit.jupiter.api.Assertions.assertNotNull;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  
  import org.apache.causeway.applib.Identifier;
  import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy;
  import org.apache.causeway.applib.id.LogicalType;
  import org.apache.causeway.applib.services.i18n.TranslationService;
  import org.apache.causeway.applib.services.iactnlayer.InteractionService;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
  import org.apache.causeway.commons.internal.reflection._MethodFacades;
  import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting;
  import org.apache.causeway.core.metamodel._testing.MethodRemover_forTesting;
  import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
  import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
  import org.apache.causeway.core.metamodel.facetapi.FeatureType;
  import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessClassContext;
  import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessMethodContext;
  import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessParameterContext;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
  import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
  import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
  import org.apache.causeway.core.metamodel.spec.impl._JUnitSupport;
  import org.apache.causeway.core.metamodel.valuesemantics.IntValueSemantics;
  import org.apache.causeway.core.security.authentication.InteractionContextFactory;
  
  import lombok.Builder;
  import lombok.Getter;
  import org.jspecify.annotations.NonNull;
  
  public abstract class FacetFactoryTestAbstract
  implements HasMetaModelContext {
  
      // -- SCENARIO BUILDER
  
      @Builder
      public record ActionScenario(
              Class<?> declaringClass,
              String actionName,
              Optional<Class<?>> mixinClass) {
          public static ActionScenarioBuilder builder(final Class<?> declaringClass, final String actionName) {
              return new ActionScenario.ActionScenarioBuilder()
                      .mixinClass(Optional.empty())
                      .declaringClass(declaringClass)
                      .actionName(actionName);
          }
      }
  
      @Builder
      public record ParameterScenario(
              Class<?> declaringClass,
              String actionName,
              int paramIndex,
              Optional<Class<?>> mixinClass) {
          public static ParameterScenarioBuilder builder(final Class<?> declaringClass, final String actionName, final int paramIndex) {
              return new ParameterScenario.ParameterScenarioBuilder()
                      .mixinClass(Optional.empty())
                      .declaringClass(declaringClass)
                      .actionName(actionName)
                      .paramIndex(paramIndex);
          }
      }
  
      @Builder
      public record PropertyScenario(
              Class<?> declaringClass,
              String propertyName,
              Optional<Class<?>> mixinClass) {
          public static PropertyScenarioBuilder builder(final Class<?> declaringClass, final String propertyName) {
              return new PropertyScenario.PropertyScenarioBuilder()
                      .mixinClass(Optional.empty())
                      .declaringClass(declaringClass)
                      .propertyName(propertyName);
          }
      }
  
      @Builder
      public record CollectionScenario(
              Class<?> declaringClass,
              String collectionName,
              Optional<Class<?>> mixinClass) {
          public static CollectionScenarioBuilder builder(final Class<?> declaringClass, final String collectionName) {
              return new CollectionScenario.CollectionScenarioBuilder()
                      .mixinClass(Optional.empty())
                      .declaringClass(declaringClass)
                      .collectionName(collectionName);
          }
      }
  
      // --
  
      @Getter(onMethod_ = {@Override})
      private MetaModelContext metaModelContext;
  
      private MethodRemover_forTesting methodRemover;
  
      /**
       * Override, if a custom {@link MetaModelContext_forTesting} is required for certain tests.
       */
      protected MetaModelContext_forTesting setUpMmc(
              final MetaModelContext_forTesting.MetaModelContext_forTestingBuilder builder) {
          return builder.build();
      }
  
      @BeforeEach
      protected void setUpAll() {
  
          var mockTranslationService = Mockito.mock(TranslationService.class);
          var mockInteractionService = Mockito.mock(InteractionService.class);
          var mockMemberExecutorService = Mockito.mock(MemberExecutorService.class);
  
          var iaContext = InteractionContextFactory.testing();
  
          methodRemover = new MethodRemover_forTesting();
  
          metaModelContext = setUpMmc(MetaModelContext_forTesting.builder()
                  .translationService(mockTranslationService)
                  .interactionService(mockInteractionService)
                  .memberExecutor(mockMemberExecutorService)
                  .valueSemantic(new IntValueSemantics()));
  
          Mockito.when(mockInteractionService.currentInteractionContext()).thenReturn(Optional.of(iaContext));
      }
  
      @AfterEach
      protected void tearDownAll() {
          methodRemover = null;
      }
  
      @FunctionalInterface
      public static interface MemberScenarioConsumer {
          void accept(
                  ProcessMethodContext processMethodContext,
                  FacetHolder facetHolder,
                  FacetedMethod facetedMethod);
      }
      @FunctionalInterface
      protected static interface MixedInActionScenarioConsumer {
          void accept(
                  ProcessMethodContext processMethodContext,
                  ObjectSpecification mixeeSpec,
                  FacetedMethod facetedMethod,
                  ObjectAction mixedInAct);
      }
      @FunctionalInterface
      protected static interface MixedInPropertyScenarioConsumer {
          void accept(
                  ProcessMethodContext processMethodContext,
                  ObjectSpecification mixeeSpec,
                  FacetedMethod facetedMethod,
                  OneToOneAssociation mixedInProp);
      }
      @FunctionalInterface
      protected static interface MixedInCollectionScenarioConsumer {
          void accept(
                  ProcessMethodContext processMethodContext,
                  ObjectSpecification mixeeSpec,
                  FacetedMethod facetedMethod,
                  OneToManyAssociation mixedInColl);
      }
  
      @FunctionalInterface
      protected static interface ParameterScenarioConsumer {
          void accept(
                  ProcessParameterContext processParameterContext,
                  FacetHolder facetHolder,
                  FacetedMethod facetedMethod,
                  FacetedMethodParameter facetedMethodParameter);
      }
  
      /**
       * Action scenario.
       */
      protected void actionScenario(
              final Class<?> declaringClass, final String actionName, final MemberScenarioConsumer consumer) {
          actionScenario(ActionScenario.builder(declaringClass, actionName).build(), consumer);
      }
      /**
       * Custom Action scenario.
       */
      protected void actionScenario(
              final ActionScenario scenario, final MemberScenarioConsumer consumer) {
  
          var declaringClass = scenario.declaringClass();
          var memberId = scenario.actionName();
          var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, memberId);
          var paramTypes = actionMethod.paramTypes();
          var facetHolder = actionFacetHolder(declaringClass, memberId, paramTypes);
          var facetedMethod = FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, memberId, paramTypes);
          var processMethodContext = ProcessMethodContext
                  .forTesting(declaringClass, FeatureType.ACTION, actionMethod, methodRemover, facetedMethod);
  
          consumer.accept(processMethodContext, facetHolder, facetedMethod);
      }
      /**
       * MixedIn Action scenario.
       */
      protected void actionScenarioMixedIn(
              final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInActionScenarioConsumer consumer) {
          var scenario = ActionScenario.builder(mixeeClass, "unused")
                  .mixinClass(Optional.of(mixinClass))
                  .build();
          actionScenarioMixedIn(scenario, consumer);
      }
      protected void actionScenarioMixedIn(
              final ActionScenario scenario, final MixedInActionScenarioConsumer consumer) {
  
          var declaringClass = scenario.declaringClass();
  
          // get mixin main, assuming 'act'
          var mixinClass = scenario.mixinClass().orElseThrow();
          var mixedInMethod = _Utils.findMethodByNameOrFail(mixinClass, "act");
  
          var annotatedMethod = mixedInMethod;
          var facetedMethod = FacetedMethod.createForAction(getMetaModelContext(), mixinClass,
                  _MethodFacades.regular(annotatedMethod));
  
          var id = facetedMethod.getFeatureIdentifier();
          assertNotNull(id.className());
  
          var processMethodContext = new ProcessMethodContext(
                  mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.ACTION,
                  _MethodFacades.regular(annotatedMethod),
                  methodRemover, facetedMethod, true);
  
          final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
          final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
          final ObjectAction mixedInAct =
                  _JUnitSupport.mixedInActionforMixinMain(mixeeSpec, mixinSpec, "act", facetedMethod);
  
          consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInAct);
      }
  
      /**
       * Parameter scenario.
       */
      protected void parameterScenario(
              final Class<?> declaringClass, final String actionName, final int paramIndex, final ParameterScenarioConsumer consumer) {
          parameterScenario(ParameterScenario.builder(declaringClass, actionName, paramIndex).build(), consumer);
      }
      /**
       * Custom Parameter scenario.
       */
      protected void parameterScenario(
              final ParameterScenario scenario, final ParameterScenarioConsumer consumer) {
          _Assert.assertEquals(0, scenario.paramIndex(), ()->"not yet implemented otherwise");
  
          var declaringClass = scenario.declaringClass();
          var memberId = scenario.actionName();
          var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, memberId);
          var paramTypes = actionMethod.paramTypes();
          var facetHolder = actionFacetHolder(declaringClass, memberId, paramTypes);
          var facetedMethod = FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, memberId, paramTypes);
          var facetedMethodParameter =
                  actionMethod.isNoArg()
                  ? (FacetedMethodParameter)null
                  : new FacetedMethodParameter(getMetaModelContext(),
                      FeatureType.ACTION_PARAMETER_SINGULAR, facetedMethod.owningType(),
                      facetedMethod.methodFacade(), 0);
  
          var processParameterContext =
                  FacetFactory.ProcessParameterContext.forTesting(
                          declaringClass, IntrospectionPolicy.ANNOTATION_OPTIONAL, actionMethod, null, facetedMethodParameter);
  
          consumer.accept(processParameterContext, facetHolder, facetedMethod, facetedMethodParameter);
      }
  
      /**
       * Property scenario.
       */
      protected void propertyScenario(
              final Class<?> declaringClass, final String propertyName, final MemberScenarioConsumer consumer) {
          propertyScenario(PropertyScenario.builder(declaringClass, propertyName).build(), consumer);
      }
      /**
       * Custom Property scenario.
       */
      protected void propertyScenario(
              final PropertyScenario scenario, final MemberScenarioConsumer consumer) {
          var declaringClass = scenario.declaringClass();
          var memberId = scenario.propertyName();
  
          var facetHolder = propertyFacetHolder(declaringClass, memberId);
          var annotatedMethod = _Utils.findGetterOrFail(declaringClass, memberId);
          var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, annotatedMethod);
  
          var processMethodContext = ProcessMethodContext
                  .forTesting(declaringClass, FeatureType.PROPERTY, annotatedMethod, methodRemover, facetedMethod);
  
          consumer.accept(processMethodContext, facetHolder, facetedMethod);
      }
      /**
       * MixedIn Property scenario.
       */
      protected void propertyScenarioMixedIn(
              final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInPropertyScenarioConsumer consumer) {
          var scenario = PropertyScenario.builder(mixeeClass, "unused")
                  .mixinClass(Optional.of(mixinClass))
                  .build();
          propertyScenarioMixedIn(scenario, consumer);
      }
      protected void propertyScenarioMixedIn(
              final PropertyScenario scenario, final MixedInPropertyScenarioConsumer consumer) {
  
          var declaringClass = scenario.declaringClass();
  
          // get mixin main, assuming 'prop'
          var mixinClass = scenario.mixinClass().orElseThrow();
          var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, "prop");
  
          var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), mixinClass, annotatedMethod);
  
          var id = facetedMethod.getFeatureIdentifier();
          assertNotNull(id.className());
  
          var processMethodContext = new ProcessMethodContext(
                  mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.PROPERTY,
                  _MethodFacades.regular(annotatedMethod),
                  methodRemover, facetedMethod, true);
  
          final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
          final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
          final OneToOneAssociation mixedInProp =
                  _JUnitSupport.mixedInProp(mixeeSpec, mixinSpec, "prop", facetedMethod);
  
          consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInProp);
      }
  
      /**
       * Collection scenario.
       */
      protected void collectionScenario(
              final Class<?> declaringClass, final String collectionName, final MemberScenarioConsumer consumer) {
          collectionScenario(CollectionScenario.builder(declaringClass, collectionName).build(), consumer);
      }
      /**
       * Custom Collection scenario.
       */
      protected void collectionScenario(
              final CollectionScenario scenario, final MemberScenarioConsumer consumer) {
  
          var declaringClass = scenario.declaringClass();
          var memberId = scenario.collectionName();
          var facetHolder = collectionFacetHolder(declaringClass, memberId);
          var annotatedMethod = _Utils.findGetterOrFail(declaringClass, memberId);
          var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, annotatedMethod);
  
          var processMethodContext = ProcessMethodContext
                  .forTesting(declaringClass, FeatureType.COLLECTION, annotatedMethod, methodRemover, facetedMethod);
  
          consumer.accept(processMethodContext, facetHolder, facetedMethod);
      }
      /**
       * MixedIn Collection scenario.
       */
      protected void collectionScenarioMixedIn(
              final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInCollectionScenarioConsumer consumer) {
          var scenario = CollectionScenario.builder(mixeeClass, "unused")
                  .mixinClass(Optional.of(mixinClass))
                  .build();
          collectionScenarioMixedIn(scenario, consumer);
      }
      protected void collectionScenarioMixedIn(
              final CollectionScenario scenario, final MixedInCollectionScenarioConsumer consumer) {
  
          var declaringClass = scenario.declaringClass();
  
          // get mixin main, assuming 'coll'
          var mixinClass = scenario.mixinClass().orElseThrow();
          var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, "coll");
  
          var facetedMethod = FacetedMethod.createForCollection(getMetaModelContext(), mixinClass, annotatedMethod);
  
          var id = facetedMethod.getFeatureIdentifier();
          assertNotNull(id.className());
  
          var processMethodContext = new ProcessMethodContext(
                  mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.COLLECTION,
                  _MethodFacades.regular(annotatedMethod),
                  methodRemover, facetedMethod, true);
  
          final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
          final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
          final OneToManyAssociation mixedInColl =
                  _JUnitSupport.mixedInColl(mixeeSpec, mixinSpec, "coll", facetedMethod);
  
          consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInColl);
      }
  
      /**
       * DomainObject scenario.
       */
      protected void objectScenario(final Class<?> declaringClass, final BiConsumer<ProcessClassContext, FacetHolder> consumer) {
          var facetHolder = FacetHolder.simple(getMetaModelContext(),
                  Identifier.classIdentifier(LogicalType.fqcn(declaringClass)));
          var processClassContext = ProcessClassContext
                  .forTesting(declaringClass, methodRemover, facetHolder);
          consumer.accept(processClassContext, facetHolder);
      }
  
      // -- UTILITY
  
      protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, final String methodName, final Class<?>[] methodTypes) {
          return _Utils.findMethodExactOrFail(type, methodName, methodTypes);
      }
  
      protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, final String methodName) {
          return _Utils.findMethodExactOrFail(type, methodName);
      }
  
      protected static Optional<ResolvedMethod> findMethodExact(final Class<?> type, final String methodName) {
          return _Utils.findMethodExact(type, methodName);
      }
  
      private FacetHolder actionFacetHolder(final Class<?> declaringClass, final String memberId, final Class<?>[] paramTypes) {
          return FacetHolder.simple(getMetaModelContext(),
                  Identifier.actionIdentifier(LogicalType.fqcn(declaringClass), memberId, paramTypes));
      }
  
      private FacetHolder propertyFacetHolder(final Class<?> declaringClass, final String memberId) {
          return FacetHolder.simple(getMetaModelContext(),
                  Identifier.propertyIdentifier(LogicalType.fqcn(declaringClass), memberId));
      }
  
      private FacetHolder collectionFacetHolder(final Class<?> declaringClass, final String memberId) {
          return FacetHolder.simple(getMetaModelContext(),
                  Identifier.collectionIdentifier(LogicalType.fqcn(declaringClass), memberId));
      }
  
      // -- EXPECTATIONS
  
      protected final void assertNoMethodsRemoved() {
          assertTrue(methodRemover.removedMethodMethodCalls().isEmpty());
          assertTrue(methodRemover.removeMethodArgsCalls().isEmpty());
      }
  
      protected final void assertMethodWasRemoved(final ResolvedMethod method) {
          assertTrue(methodRemover.removedMethodMethodCalls().contains(method),
                  ()->String.format("method was not removed in test scenario: %s", method));
      }
  
      protected final void assertMethodWasRemoved(final Class<?> type, final String methodName) {
          assertMethodWasRemoved(findMethodExactOrFail(type, methodName));
      }
  
      protected final void assertMethodEqualsFirstIn(
              final @NonNull ResolvedMethod method,
              final @NonNull ImperativeFacet imperativeFacet) {
          _Utils.assertMethodEquals(method, imperativeFacet.getMethods().getFirstElseFail().asMethodElseFail());
      }
  
  }
  