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._testing;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.function.BiFunction;
  import java.util.function.Consumer;
  import java.util.function.Function;
  import java.util.stream.Stream;
  
  import static java.util.Objects.requireNonNull;
  
  import org.springframework.core.env.AbstractEnvironment;
  import org.springframework.util.ClassUtils;
  
  import org.apache.causeway.applib.layout.grid.Grid;
  import org.apache.causeway.applib.services.factory.FactoryService;
  import org.apache.causeway.applib.services.grid.GridLoaderService;
  import org.apache.causeway.applib.services.grid.GridMarshallerService;
  import org.apache.causeway.applib.services.grid.GridService;
  import org.apache.causeway.applib.services.i18n.TranslationService;
  import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
  import org.apache.causeway.applib.services.iactnlayer.InteractionService;
  import org.apache.causeway.applib.services.inject.ServiceInjector;
  import org.apache.causeway.applib.services.jaxb.JaxbService;
  import org.apache.causeway.applib.services.layout.LayoutService;
  import org.apache.causeway.applib.services.menu.MenuBarsService;
  import org.apache.causeway.applib.services.message.MessageService;
  import org.apache.causeway.applib.services.placeholder.PlaceholderRenderService;
  import org.apache.causeway.applib.services.registry.ServiceRegistry;
  import org.apache.causeway.applib.services.repository.RepositoryService;
  import org.apache.causeway.applib.services.title.TitleService;
  import org.apache.causeway.applib.services.wrapper.WrapperFactory;
  import org.apache.causeway.applib.services.xactn.TransactionService;
  import org.apache.causeway.applib.services.xactn.TransactionState;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsAbstract;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._Lazy;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.commons.internal.collections._Maps;
  import org.apache.causeway.commons.internal.collections._Sets;
  import org.apache.causeway.commons.internal.collections._Streams;
  import org.apache.causeway.commons.internal.ioc._SingletonBeanProvider;
  import org.apache.causeway.core.config.CausewayConfiguration;
  import org.apache.causeway.core.config.beans.CausewayBeanFactoryPostProcessor;
  import org.apache.causeway.core.config.beans.CausewayBeanTypeClassifier;
  import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry;
  import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
  import org.apache.causeway.core.config.viewer.web.WebAppContextPath;
  import org.apache.causeway.core.metamodel.commons.ClassUtil;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
  import org.apache.causeway.core.metamodel.facetapi.MetaModelRefiner;
  import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIconService;
  import org.apache.causeway.core.metamodel.facets.object.value.annotcfg.ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
  import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
  import org.apache.causeway.core.metamodel.progmodel.ProgrammingModelAbstract;
  import org.apache.causeway.core.metamodel.progmodel.ProgrammingModelInitFilter;
  import org.apache.causeway.core.metamodel.progmodel.ProgrammingModelInitFilterDefault;
  import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorDefault;
  import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorForCollections;
  import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry;
  import org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
  import org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault;
  import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault;
  import org.apache.causeway.core.metamodel.services.grid.bootstrap.GridMarshallerServiceBootstrap;
  import org.apache.causeway.core.metamodel.services.grid.bootstrap.GridSystemServiceBootstrap;
  import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
  import org.apache.causeway.core.metamodel.services.layout.LayoutServiceDefault;
  import org.apache.causeway.core.metamodel.services.message.MessageServiceNoop;
  import org.apache.causeway.core.metamodel.services.title.TitleServiceDefault;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.core.metamodel.spec.impl.CausewayModuleCoreMetamodelConfigurationDefault;
  import org.apache.causeway.core.metamodel.spec.impl._JUnitSupport;
  import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
  import org.apache.causeway.core.metamodel.valuesemantics.BigDecimalValueSemantics;
  import org.apache.causeway.core.metamodel.valuesemantics.TreePathValueSemantics;
  import org.apache.causeway.core.metamodel.valuesemantics.URLValueSemantics;
  import org.apache.causeway.core.metamodel.valuesemantics.UUIDValueSemantics;
  import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
  import org.apache.causeway.core.security.authentication.manager.AuthenticationManager;
  import org.apache.causeway.core.security.authorization.manager.AuthorizationManager;
  
  import lombok.Builder;
  import lombok.Getter;
  import org.jspecify.annotations.NonNull;
  import lombok.Singular;
  
  @Builder @Getter
  public final class MetaModelContext_forTesting
  extends MetaModelContext {
  
      public static MetaModelContext_forTestingBuilder builder() {
          return new MetaModelContext_forTestingBuilder() {
              @Override
              public MetaModelContext_forTesting build() {
                  var mmc = super.build();
                  MetaModelContext.setOrReplace(mmc);
                  return mmc;
              }
          };
      }
  
      public static MetaModelContext buildDefault() {
          return MetaModelContext_forTesting.builder()
          .build();
      }
  
      private ServiceInjector serviceInjector;
      private ServiceRegistry serviceRegistry;
  
      @Builder.Default
      private MetamodelEventService metamodelEventService =
          new MetamodelEventService(event->{
              System.out.printf("MetaModelContext_forTesting (logs event to console): %s%n", event);
          });
  
      @Builder.Default
      private CausewaySystemEnvironment systemEnvironment = newCausewaySystemEnvironment();
  
      @Builder.Default
      private CausewayConfiguration configuration = newCausewayConfiguration();
  
      @Builder.Default
      private ClassSubstitutorRegistry classSubstitutorRegistry =
          new ClassSubstitutorRegistry(List.of(
                  //new ClassSubstitutorForDomainObjects(),
                  new ClassSubstitutorForCollections(),
                  new ClassSubstitutorDefault()
                  ));
  
      /**
       * Whether to enable all post-processors, that are registered with the {@link ProgrammingModel}.
       * <p>
       * default: false
       */
      @Builder.Default
      private boolean enablePostprocessors = false;
  
      private ObjectManager objectManager;
  
      private WrapperFactory wrapperFactory;
  
      private SpecificationLoader specificationLoader;
  
      @Builder.Default
      Can<Function<MetaModelContext, MetaModelRefiner>> refiners = Can.empty();
  
      @Builder.Default
      private BiFunction<MetaModelContext, Can<MetaModelRefiner>, ProgrammingModel> programmingModelFactory =
          (mmc, refiners)->new CausewayModuleCoreMetamodelConfigurationDefault()
              .programmingModel(mmc, refiners.toList(), ProgrammingModelInitFilter.noop());
  
      private InteractionService interactionService;
  
      private TranslationService translationService;
  
      private InteractionContext authentication;
  
      private AuthorizationManager authorizationManager;
  
      private AuthenticationManager authenticationManager;
  
      @Builder.Default
      private TitleService titleService = new TitleServiceDefault(null, null);
  
      private ObjectIconService objectIconService;
  
      private RepositoryService repositoryService;
  
      private FactoryService factoryService;
  
      private MemberExecutorService memberExecutor;
  
      private TransactionService transactionService;
  
      private TransactionState transactionState;
  
      private CausewayBeanTypeClassifier causewayBeanTypeClassifier;
  
      private CausewayBeanTypeRegistry causewayBeanTypeRegistry;
  
      @Builder.Default
      private PlaceholderRenderService placeholderRenderService = PlaceholderRenderService.fallback();
  
      //private Map<String, ManagedObject> serviceAdaptersById;
  
      @Singular
      private List<Object> singletons;
  
      @Singular
      private List<ValueSemanticsProvider<?>> valueSemantics;
  
      @Singular
      private List<_SingletonBeanProvider> singletonProviders;
  
      // -- SERVICE SUPPORT
  
      @Override
      public Stream<ManagedObject> streamServiceAdapters() {
          return objectAdaptersForBeansOfKnownSort.get().values().stream();
      }
  
      @Override
      public ManagedObject lookupServiceAdapterById(final String serviceId) {
          return objectAdaptersForBeansOfKnownSort.get().get(serviceId);
      }
  
      // -- LOOKUP
  
      private Stream<Object> streamSingletons() {
  
          var fields = Stream.of(
                  getConfiguration(),
                  getObjectManager(),
                  getWrapperFactory(),
                  getCausewayBeanTypeClassifier(),
                  getCausewayBeanTypeRegistry(),
                  systemEnvironment,
                  classSubstitutorRegistry,
                  serviceInjector,
                  serviceRegistry,
                  metamodelEventService,
                  messageService,
  //                specificationLoader,
                  interactionService,
                  getTranslationService(),
                  authentication,
                  authorizationManager,
                  authenticationManager,
                  titleService,
                  repositoryService,
                  transactionService,
                  transactionState,
                  getMemberExecutor(),
                  getValueSemanticsResolver(),
                  new BigDecimalValueSemantics(),
                  new URLValueSemantics(),
                  new UUIDValueSemantics(),
                  new TreePathValueSemantics(),
                  this);
  
          return Stream.concat(
                  fields,
                  getSingletons().stream())
                  .filter(_NullSafe::isPresent);
      }
  
      Stream<_SingletonBeanProvider> streamBeanAdapters() {
          return _Streams.concat(
                  streamSingletons().map(_SingletonBeanProvider::forTesting),
                  singletonProviders.stream(),
                  discoveredServices.stream(),
                  Stream.of(
                      // support for lazy bean providers,
                      _SingletonBeanProvider.forTestingLazy(GridLoaderService.class, this::getGridLoaderService),
                      _SingletonBeanProvider.forTestingLazy(GridService.class, this::getGridService),
                      _SingletonBeanProvider.forTestingLazy(JaxbService.class, this::getJaxbService),
                      _SingletonBeanProvider.forTestingLazy(MenuBarsService.class, this::getMenuBarsService),
                      _SingletonBeanProvider.forTestingLazy(LayoutService.class, this::getLayoutService),
                      _SingletonBeanProvider.forTestingLazy(SpecificationLoader.class, this::getSpecificationLoader)
                  )
                  );
      }
  
      private static CausewaySystemEnvironment newCausewaySystemEnvironment() {
          var env = new CausewaySystemEnvironment();
          env.setUnitTesting(true);
          return env;
      }
  
      private static CausewayConfiguration newCausewayConfiguration() {
          var properties = _Maps.<String, String>newHashMap();
          var config = CausewayConfiguration.builder().environment(new AbstractEnvironment() {
              @Override
              public String getProperty(final String key) {
                  return properties.get(key);
              }
          }).build();
          return config;
      }
  
      @Override
      public ServiceRegistry getServiceRegistry() {
          if(serviceRegistry==null) {
              serviceRegistry = new ServiceRegistry_forTesting(this);
          }
          return serviceRegistry;
      }
  
      @Override
      public ServiceInjector getServiceInjector() {
          if(serviceInjector==null) {
              serviceInjector = new ServiceInjector_forTesting(this);
          }
          return serviceInjector;
      }
  
      @Override
      public FactoryService getFactoryService() {
          if(factoryService==null) {
              factoryService = new FactoryService_forTesting(this);
          }
          return factoryService;
      }
  
      @Override
      public TranslationService getTranslationService() {
          if(translationService==null) {
              translationService = new TranslationService_forTesting();
          }
          return translationService;
      }
  
      private ValueSemanticsResolver valueSemanticsResolver;
      private ValueSemanticsResolver getValueSemanticsResolver(){
          if(valueSemanticsResolver==null) {
              valueSemanticsResolver = new ValueSemanticsResolverDefault(valueSemantics,
                      getTranslationService());
          }
          return valueSemanticsResolver;
      }
  
      private final CausewayBeanFactoryPostProcessor causewayBeanFactoryPostProcessorForSpring =
              new CausewayBeanFactoryPostProcessor();
  
      public CausewayBeanTypeClassifier getCausewayBeanTypeClassifier() {
          if(causewayBeanTypeClassifier==null) {
              causewayBeanTypeClassifier = causewayBeanFactoryPostProcessorForSpring.getCausewayBeanTypeClassifier();
          }
          return causewayBeanTypeClassifier;
      }
  
      public CausewayBeanTypeRegistry getCausewayBeanTypeRegistry() {
          if(causewayBeanTypeRegistry==null) {
              causewayBeanTypeRegistry = CausewayBeanTypeRegistry.empty();
          }
          return causewayBeanTypeRegistry;
      }
  
      private final _Lazy<ProgrammingModel> programmingModelRef =
              _Lazy.threadSafe(()->initProgrammingModel());
      public ProgrammingModel getProgrammingModel() {
          return programmingModelRef.get();
      }
      private final ProgrammingModel initProgrammingModel() {
          var metamodelRefiners = refiners.map(factory->factory.apply(this));
          var programmingModel = programmingModelFactory.apply(this, metamodelRefiners);
  
          ((ProgrammingModelAbstract)programmingModel).init(new ProgrammingModelInitFilterDefault());
          return programmingModel;
      }
  
      @Override
      public SpecificationLoader getSpecificationLoader() {
          if(specificationLoader==null) {
  
              var configuration = requireNonNull(getConfiguration());
              var environment = requireNonNull(getSystemEnvironment());
              var serviceRegistry = requireNonNull(getServiceRegistry());
              @SuppressWarnings("unused")
              var serviceInjector = requireNonNull(getServiceInjector());
              var programmingModel = requireNonNull(getProgrammingModel());
              var causewayBeanTypeClassifier = requireNonNull(getCausewayBeanTypeClassifier());
              var causewayBeanTypeRegistry = requireNonNull(getCausewayBeanTypeRegistry());
              var classSubstitutorRegistry = requireNonNull(getClassSubstitutorRegistry());
  
              specificationLoader = _JUnitSupport.specLoader(
                      configuration,
                      environment,
                      serviceRegistry,
                      programmingModel,
                      enablePostprocessors,
                      causewayBeanTypeClassifier,
                      causewayBeanTypeRegistry,
                      classSubstitutorRegistry);
  
          }
          return specificationLoader;
      }
  
      @Builder.Default
      private final MessageService messageService = new MessageServiceNoop();
  
      @Override
      public ManagedObject getHomePageAdapter() {
          // not supported
          return null;
      }
  
      @Override
      public ObjectManager getObjectManager() {
          if(objectManager==null) {
              objectManager = new ObjectManager(this);
          }
          return objectManager;
      }
  
      @Override
      public WrapperFactory getWrapperFactory() {
          if(wrapperFactory==null) {
              wrapperFactory = new WrapperFactory_forTesting();
          }
          return wrapperFactory;
      }
  
      @Override
      public WebAppContextPath getWebAppContextPath() {
          return new WebAppContextPath();
      }
  
      public void runWithConfigProperties(final Consumer<Map<String, String>> setup, final Runnable runnable) {
          var properties = _Maps.<String, String>newHashMap();
          setup.accept(properties);
  
          var currentConfigBackup = this.configuration;
          try {
  
              this.configuration = CausewayConfiguration.builder().environment(new AbstractEnvironment() {
                  @Override
                  public String getProperty(final String key) {
                      return properties.get(key);
                  }
              }).build();
  
              runnable.run();
          } finally {
              this.configuration = currentConfigBackup;
          }
  
      }
  
      // -- LAYOUT TESTING SUPPORT
  
      @Getter(lazy = true)
      private final JaxbService jaxbService = new JaxbService.Simple();
  
      @Getter(lazy = true)
      private final MenuBarsService menuBarsService = createMenuBarsService();
      private final MenuBarsService createMenuBarsService() {
          return getSingletonProviders().stream()
                  .filter(_SingletonBeanProvider.satisfying(MenuBarsService.class))
                  .findFirst()
                  .map(provider->(MenuBarsService)provider.getInstanceElseFail())
                  .orElseGet(MenuBarsService::forTesting);
      }
  
      @Getter(lazy = true)
      private final GridMarshallerService gridMarshallerService = createGridMarshallerService();
      //XXX lombok issue: won't compile if inlined
      private final GridMarshallerService<? extends Grid> createGridMarshallerService() {
          return new GridMarshallerServiceBootstrap(getJaxbService());
      }
  
      @Getter(lazy = true)
      private final GridLoaderService gridLoaderService = createGridLoaderService();
      //XXX lombok issue: won't compile if inlined
      private final GridLoaderService createGridLoaderService() {
          return new GridLoaderServiceDefault(getMessageService(), Can.of(new LayoutResourceLoaderDefault()), /*support reloading*/true);
      }
  
      @Getter(lazy = true)
      private final GridService gridService = createGridService();
      //XXX lombok issue: won't compile if inlined
      @SuppressWarnings("unchecked")
      private final GridService createGridService() {
          return new GridServiceDefault(
              getGridLoaderService(),
              getGridMarshallerService(),
              List.of(
                      new GridSystemServiceBootstrap(
                              this,
                              getTranslationService(),
                              getJaxbService(),
                              getMessageService(),
                              getSystemEnvironment(),
                              List.of())
                              .setMarshaller(getGridMarshallerService())
                      )); // support reloading
      }
  
      @Getter(lazy = true)
      private final LayoutService layoutService = createLayoutService();
      //XXX lombok issue: won't compile if inlined
      private final LayoutService createLayoutService() {
          return new LayoutServiceDefault(
                  getSpecificationLoader(),
                  getGridService(),
                  getMenuBarsService());
      }
  
      // -- SERVICE REGISTRY HELPER
  
      private final _Lazy<Map<String, ManagedObject>> objectAdaptersForBeansOfKnownSort =
              _Lazy.threadSafe(this::collectBeansOfKnownSort);
  
      public void clearRegisteredBeans() {
          objectAdaptersForBeansOfKnownSort.clear();
      }
  
      record ServiceInstance(
          ObjectSpecification specification,
          Object pojo) {
      }
  
      @Builder.Default
      private final Set<_SingletonBeanProvider> discoveredServices = _Sets.newHashSet();
  
      @SuppressWarnings({ "unchecked", "rawtypes" })
      private void registerAsService(final ServiceInstance serviceInstance) {
          var spec = serviceInstance.specification();
          discoveredServices.add(_SingletonBeanProvider.forTestingLazy(
                  spec.logicalTypeName(),
                  (Class)spec.getCorrespondingClass(),
                  serviceInstance::pojo));
      }
  
      private final Map<String, ManagedObject> collectBeansOfKnownSort() {
          var map = _Maps.<String, ManagedObject>newLinkedHashMap();
  
          // first pass: introspect them all
          var services = getServiceRegistry()
              .streamRegisteredBeans()
              .map(this::toServiceInstance)
              .map(op->op.orElse(null))
              .filter(_NullSafe::isPresent)
              .peek(this::registerAsService)
              .collect(Can.<ServiceInstance>toCan());
  
          // reload registered beans
          ((ServiceRegistry_forTesting)getServiceRegistry()).invalidateRegisteredBeans();
  
          // second pass: adapt service objects
          services.stream()
          .map(service->ManagedObject.service(service.specification, service.pojo))
          .forEach(serviceAdapter->
              map.put(serviceAdapter.objSpec().logicalTypeName(), serviceAdapter));
          return map;
      }
  
      private Optional<ServiceInstance> toServiceInstance(final _SingletonBeanProvider managedBeanAdapter) {
          var servicePojo = managedBeanAdapter.getInstanceElseFail();
  
          if(ProgrammingModelConstants.TypeVetoMarker.anyMatchOn(managedBeanAdapter.beanClass())) {
              return Optional.empty();
          }
          return getSpecificationLoader()
              .specForType(servicePojo.getClass())
              .map(serviceSpec->new ServiceInstance(serviceSpec, servicePojo));
      }
  
      // -- RECURSIVE INITIALIZATION FIX
  
      private final List<Runnable> postConstructRunnables = new ArrayList<>();
      public void registerPostconstruct(final @NonNull Runnable postConstructRunnable) {
          postConstructRunnables.add(postConstructRunnable);
      }
      public void runPostconstruct() {
          try {
              postConstructRunnables.stream()
              .forEach(Runnable::run);
          } finally {
              postConstructRunnables.clear();
          }
      }
  
      // -- VALUE SEMANTICS
  
      /**
       * Allows to register value-semantics.
       */
      public <T> MetaModelContext_forTesting withValueSemantics(final ValueSemanticsAbstract<T> valueSemantics) {
          var valueClass = valueSemantics.getCorrespondingClass();
          var valueSpec = getSpecificationLoader().loadSpecification(valueClass);
          ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory
              .installValueFacet(valueClass, Can.of(valueSemantics), valueSpec);
  
          if(ClassUtils.isPrimitiveWrapper(valueClass)) {
              var primitiveType = ClassUtil.unboxPrimitiveIfNecessary(valueClass);
              var primitiveTypeSpec = getSpecificationLoader().loadSpecification(primitiveType);
              ValueFacetForValueAnnotationOrAnyMatchingValueSemanticsFacetFactory
              .installValueFacet(valueClass, Can.of(valueSemantics), primitiveTypeSpec);
          }
          serviceInjector.injectServicesInto(valueSemantics);
          return this;
      }
  
  }
  