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.spec.impl;
  
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.Set;
  import java.util.TreeSet;
  import java.util.function.Predicate;
  import java.util.stream.Collectors;
  
  import org.apache.causeway.applib.Identifier;
  import org.apache.causeway.applib.annotation.Domain;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.collections._Lists;
  import org.apache.causeway.commons.internal.functions._Predicates;
  import org.apache.causeway.commons.internal.reflection._Annotations;
  import org.apache.causeway.commons.internal.reflection._ClassCache;
  import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
  import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
  import org.apache.causeway.commons.semantics.AccessorSemantics;
  import org.apache.causeway.commons.internal.reflection._Reflect;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MemberSupportPrefix;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.ObjectSupportMethod;
  import org.apache.causeway.core.metamodel.commons.MethodUtil;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
  import org.apache.causeway.core.metamodel.facets.FacetedMethod;
  import org.apache.causeway.core.metamodel.facets.HasFacetedMethod;
  import org.apache.causeway.core.metamodel.facets.ImperativeFacet;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
  import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
  import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
  
  import org.jspecify.annotations.NonNull;
  
  /**
   * @since 2.0
   * @see org.apache.causeway.applib.annotation.Domain.Include
   */
  class ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution
  extends MetaModelValidatorAbstract {
  
      private final _ClassCache classCache;
  
      ValidatorDomainIncludeAnnotationEnforcesMetamodelContribution(final MetaModelContext mmc) {
          super(mmc, spec->((spec instanceof ObjectSpecificationDefault)
              && !spec.isAbstract()
              && !spec.getBeanSort().isManagedBeanNotContributing()
              && !spec.isValue()));
          this.classCache = _ClassCache.getInstance();
      }
  
      @Override
      public void validateObjectEnter(final ObjectSpecification spec) {
  
          final Class<?> type = spec.getCorrespondingClass();
  
          // methods picked up by the framework
          var memberMethods = new TreeSet<ResolvedMethod>(ResolvedMethod::methodCompare);
          var supportMethods = new TreeSet<ResolvedMethod>(ResolvedMethod::methodCompare);
  
          spec
              .streamAnyActions(MixedIn.EXCLUDED)
              .map(HasFacetedMethod.class::cast)
              .map(HasFacetedMethod::getFacetedMethod)
              .map(FacetedMethod::methodFacade)
              .map(MethodFacade::asMethodForIntrospection)
              .forEach(memberMethods::add);
  
          spec
              .streamAssociations(MixedIn.EXCLUDED)
              .map(HasFacetedMethod.class::cast)
              .map(HasFacetedMethod::getFacetedMethod)
              .map(FacetedMethod::methodFacade)
              .map(MethodFacade::asMethodForIntrospection)
              .forEach(memberMethods::add);
  
          spec
              .streamFacetHolders()
              .flatMap(FacetHolder::streamFacetRankings)
              .map(facetRanking->facetRanking.getWinnerNonEvent(facetRanking.facetType()))
              .flatMap(Optional::stream)
              .filter(ImperativeFacet.class::isInstance)
              .map(ImperativeFacet.class::cast)
              .map(ImperativeFacet::getMethods)
              .flatMap(Can::stream)
              .map(MethodFacade::asMethodForIntrospection)
              .forEach(supportMethods::add);
  
          var methodsIntendedToBeIncludedButNotPickedUp = classCache
              // methods intended to be included with the meta-model but missing
              .streamDeclaredMethodsHaving(
                      type,
                      "domain-include",
                      method->
                          _Annotations.synthesize(method.method(), Domain.Include.class).isPresent())
              // filter away those that are recognized
              .filter(Predicate.not(memberMethods::contains))
              .filter(Predicate.not(supportMethods::contains))
              // filter away classic getters, that shadow record components
              .filter(spec.getCorrespondingClass().isRecord()
                  ? Predicate.not(AccessorSemantics::isGetter)
                  : _Predicates.alwaysTrue())
              .collect(Collectors.toCollection(HashSet::new));
  
          // find reasons about why these are not recognized
          methodsIntendedToBeIncludedButNotPickedUp.stream()
              .forEach(notPickedUpMethod->{
                  var unmetConstraints =
                      unmetConstraints((ObjectSpecificationDefault) spec, notPickedUpMethod)
                          .stream()
                          .collect(Collectors.joining("; "));
  
                  ValidationFailure.raiseFormatted(spec,
                          MessageTemplate.UNSATISFIED_DOMAIN_INCLUDE_SEMANTICS
                              .builder()
                              .addVariable("type", spec.getFeatureIdentifier().className())
                              .addVariable("member", _Reflect.methodToShortString(notPickedUpMethod.method()))
                              .addVariable("unmetConstraints", unmetConstraints)
                              .buildMessage());
              });
  
          validateOrphanedSupportingMethod(
                  spec, supportMethods, memberMethods, methodsIntendedToBeIncludedButNotPickedUp);
      }
  
      // -- HELPER - VALIDATION LOGIC
  
      private List<String> unmetConstraints(
              final ObjectSpecificationDefault spec,
              final ResolvedMethod method) {
  
          //var type = spec.getCorrespondingClass();
          var unmetContraints = _Lists.<String>newArrayList();
  
          if(!spec.getIntrospectionPolicy().getEncapsulationPolicy().isEncapsulatedMembersSupported()
                  && !MethodUtil.isPublic(method)) {
              unmetContraints.add("method must be 'public'");
              return unmetContraints; // don't check any further
          }
  
          // find any inherited methods that have Domain.Include semantics
          var inheritedMethodsWithDomainIncludeSemantics =
              _Reflect.streamInheritedMethods(method.method())
              .filter(m->!Objects.equals(method.toString(), m.toString())) // exclude self
              .filter(m->_Annotations.synthesize(m, Domain.Include.class).isPresent())
              .collect(Collectors.toSet());
  
          if(!inheritedMethodsWithDomainIncludeSemantics.isEmpty()) {
              unmetContraints.add("inherited method(s) having conflicting domain-include semantics: "
                      + inheritedMethodsWithDomainIncludeSemantics);
              return unmetContraints; // don't check any further
          }
  
          // fallback message
          unmetContraints.add("conflicting domain-include semantics, orphaned support method, "
                  + "misspelled prefix or unsupported method signature");
          return unmetContraints;
      }
  
      private static void validateOrphanedSupportingMethod(
              final @NonNull ObjectSpecification spec,
              final @NonNull Set<ResolvedMethod> supportMethods,
              final @NonNull Set<ResolvedMethod> memberMethods,
              final @NonNull Set<ResolvedMethod> alreadyReported) {
  
          if(spec.isAbstract()
                  || spec.getBeanSort().isManagedBeanNotContributing()
                  || spec.isValue()
                  || spec.getIntrospectionPolicy()
                      .getSupportMethodAnnotationPolicy()
                      .isSupportMethodAnnotationsRequired()) {
              return; // ignore
          }
  
          var potentialOrphans = spec instanceof ObjectSpecificationDefault specDefault
              ? specDefault.getPotentialOrphans()
              : Collections.<ResolvedMethod>emptySet();
          if(potentialOrphans.isEmpty()) return; // nothing to do
  
          // find reasons why these are not recognized
          potentialOrphans.stream()
              .filter(Predicate.not(alreadyReported::contains))
              .filter(Predicate.not(memberMethods::contains))
              .filter(Predicate.not(supportMethods::contains))
              .filter(m->matchesSupportMethodNamingConvention(m.name()))
              .forEach(orphanedMethod->{
  
                  var methodIdentifier = Identifier
                          .methodIdentifier(spec.getFeatureIdentifier().logicalType(), orphanedMethod);
  
                  ValidationFailure.raise(
                          spec,
                          ProgrammingModelConstants.MessageTemplate.ORPHANED_METHOD
                              .builder()
                              .addVariablesFor(methodIdentifier)
                              .buildMessage());
              });
  
          potentialOrphans.clear(); // no longer needed
      }
  
      private static boolean matchesSupportMethodNamingConvention(final String methodName) {
          for(var objectSupportMethod : ObjectSupportMethod.values()) {
              if(objectSupportMethod.getMethodNames().anyMatch(name->name.equals(methodName))) return true;
          }
          for(var memberSupportPrefix : MemberSupportPrefix.values()) {
              if(memberSupportPrefix.getMethodNamePrefixes().anyMatch(prefix->methodName.startsWith(prefix))) return true;
          }
          return false;
      }
  
  }
  