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.testing.fixtures.applib.modules;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.function.BiConsumer;
  import java.util.stream.Collectors;
  
  import jakarta.annotation.Priority;
  import jakarta.inject.Inject;
  import jakarta.inject.Named;
  
  import org.springframework.beans.factory.annotation.Qualifier;
  import org.springframework.context.ConfigurableApplicationContext;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.context.annotation.Import;
  import org.springframework.context.event.ContextRefreshedEvent;
  import org.springframework.context.event.EventListener;
  import org.jspecify.annotations.Nullable;
  import org.springframework.stereotype.Service;
  
  import org.apache.causeway.applib.annotation.PriorityPrecedence;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.commons.internal.reflection._Reflect;
  import org.apache.causeway.core.runtimeservices.spring.BeanDescriptor;
  import org.apache.causeway.core.runtimeservices.spring.ContextBeans;
  import org.apache.causeway.core.runtimeservices.spring.SpringBeansService;
  import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
  
  import lombok.Data;
  import org.jspecify.annotations.NonNull;
  import lombok.RequiredArgsConstructor;
  import lombok.extern.log4j.Log4j2;
  
  /**
   * @since 2.x {@index}
   */
  @Service
  @Named("causeway.testing.fixtures.ModuleWithFixturesService")
  @Priority(PriorityPrecedence.MIDPOINT)
  @Qualifier("Default")
  @Log4j2
  @RequiredArgsConstructor(onConstructor_ = {@Inject})
  public class ModuleWithFixturesService {
  
      private final SpringBeansService springBeansService;
  
      public FixtureScript getRefDataSetupFixture() {
          return new FixtureScript() {
              @Override
              protected void execute(final ExecutionContext executionContext) {
                  final List<ModuleWithFixturesService.ModuleWithFixturesDescriptor> descriptors = modules();
                  executionContext.executeChildren(this,
                          descriptors.stream()
                                  .map(ModuleWithFixturesService.ModuleWithFixturesDescriptor::getModule)
                                  .map(ModuleWithFixtures::getRefDataSetupFixture));
              }
          };
      }
  
      public FixtureScript getTeardownFixture() {
          return new FixtureScript() {
              @Override
              protected void execute(final ExecutionContext executionContext) {
                  final List<ModuleWithFixturesService.ModuleWithFixturesDescriptor> descriptors = modules();
                  Collections.reverse(descriptors);
                  executionContext.executeChildren(this,
                          descriptors.stream()
                                  .map(ModuleWithFixturesService.ModuleWithFixturesDescriptor::getModule)
                                  .map(ModuleWithFixtures::getTeardownFixture));
              }
  
          };
      }
  
      public List<ModuleWithFixturesDescriptor> modules() {
          var modules = modulesWithin(springBeansService.beansByContextId().entrySet());
          return sequenced(modules);
      }
  
      static List<ModuleWithFixturesDescriptor> modulesWithin(final Iterable<Map.Entry<String, ContextBeans>> beans) {
  
          var descriptors = new ArrayList<ModuleWithFixturesDescriptor>();
          for (Map.Entry<String, ContextBeans> contextEntry : beans) {
              final String contextId = contextEntry.getKey();
              final ContextBeans contextBeans = contextEntry.getValue();
              final ConfigurableApplicationContext context = contextBeans.context();
  
              final Map<String, ModuleWithFixtures> modulesByBeanName = context.getBeansOfType(ModuleWithFixtures.class);
              final Map<String, Object> configurationBeansByBeanName = context.getBeansWithAnnotation(Configuration.class);
              final Map<String, Object> beansAnnotatedWithImportByBeanName = context.getBeansWithAnnotation(Import.class);
  
              for (Map.Entry<String, BeanDescriptor> beanEntry : contextBeans.beans().entrySet()) {
                  final String beanName = beanEntry.getKey();
  
                  final ModuleWithFixtures module = modulesByBeanName.get(beanName);
                  if(module != null) {
  
                      final Object annotatedWithImport = beansAnnotatedWithImportByBeanName.get(beanName);
  
                      final Optional<Import> importIfAny = annotatedWithImport != null
                              && configurationBeansByBeanName.get(beanName) != null
                                  ? Optional.ofNullable(_Reflect.getAnnotation(annotatedWithImport.getClass(), Import.class))
                                  : Optional.empty();
  
                      final Map<String, ModuleWithFixtures> importedModulesByBeanName =
                          importIfAny
                              .map(importAnnot->{
                                  final Map<String, ModuleWithFixtures> map = new LinkedHashMap<>();
                                  visitImportAnnotation(importAnnot, ModuleWithFixtures.class, context, map::put);
                                  return map;
                              })
                              .orElseGet(Collections::emptyMap);
  
                      var descriptor = new ModuleWithFixturesDescriptor(contextId, beanName, module, importedModulesByBeanName);
                      descriptors.add(descriptor);
                  }
              }
          }
          return descriptors;
      }
  
      static <T> void visitImportAnnotation(
              final @Nullable Import importAnnot,
              final @NonNull Class<T> requiredType,
              final @NonNull ConfigurableApplicationContext springContext,
              final @NonNull BiConsumer<String, T> visitor) {
  
          if(importAnnot==null) return;
          final Class<?>[] importedClasses = importAnnot.value();
  
          _NullSafe.stream(importedClasses)
          .filter(type->requiredType.isAssignableFrom(type))
          .forEach(importedClass -> {
              final Map<String, ?> importedBeansOfType = springContext.getBeansOfType(importedClass);
              importedBeansOfType.forEach((name, entryValue) -> {
                  final Collection<?> beanCollection = entryValue instanceof Collection
                          ? (Collection<?>) entryValue
                          : Collections.singletonList(entryValue);
  
                  beanCollection.stream()
                          .filter(requiredType::isInstance)
                          .map(requiredType::cast)
                          .forEach(mod -> visitor.accept(name, mod));
              });
          });
      }
  
      static List<ModuleWithFixturesDescriptor> sequenced(final List<ModuleWithFixturesDescriptor> modules) {
          var remaining = new ArrayList<>(modules);
          var sequenced = new ArrayList<ModuleWithFixturesDescriptor>();
  
          var moduleByName = new LinkedHashMap<String, ModuleWithFixturesDescriptor>();
          modules.forEach(module -> {
              moduleByName.put(module.getBeanName(), module);
          });
  
          while(!remaining.isEmpty()) {
              ModuleWithFixturesDescriptor added = addNextModule(sequenced, remaining, moduleByName);
              if (added == null) {
                  throw new IllegalStateException(String.format(
                          "Unable to determine next module.\nfound = %s\nremaining = %s",
                          beanNamesOf(sequenced), beanNamesOf(remaining)));
              }
              remaining.remove(added);
          }
  
          return sequenced;
      }
  
      static List<String> beanNamesOf(final ArrayList<ModuleWithFixturesDescriptor> result) {
          return result.stream().map(ModuleWithFixturesDescriptor::getBeanName).collect(Collectors.toList());
      }
  
      static ModuleWithFixturesDescriptor addNextModule(
              final List<ModuleWithFixturesDescriptor> result,
              final List<ModuleWithFixturesDescriptor> remaining,
              final LinkedHashMap<String, ModuleWithFixturesDescriptor> moduleByName) {
  
          for (ModuleWithFixturesDescriptor module : remaining) {
              var numDependenciesNotYetEncountered =
                      module.getDependenciesByName().keySet().stream()
                              .map(moduleByName::get)
                              .filter(dependency -> !result.contains(dependency)) // ignore if already known about
                              .count();
              if(numDependenciesNotYetEncountered == 0) {
                  result.add(module);
                  return module;
              }
          }
          return null;
      }
  
      @EventListener(ContextRefreshedEvent.class)
      public void onContextRefreshed(final ContextRefreshedEvent event) {
          log.info("onContextRefreshed");
          for (final ModuleWithFixturesDescriptor descriptor : modules()) {
              log.info(descriptor);
          }
      }
  
      @Data
      public static class ModuleWithFixturesDescriptor {
          private final String contextId;
          private final String beanName;
          private final ModuleWithFixtures module;
          private final Map<String, ModuleWithFixtures> dependenciesByName;
  
          @Override
          public String toString() {
              return "ModuleWithFixturesDescriptor{" +
                      "contextId='" + contextId + '\'' +
                      ", beanName='" + beanName + '\'' +
                      ", dependenciesByName=" + dependenciesByName.keySet() +
                      '}';
          }
      }
  
  }
  