/*
 *  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.archtestsupport.applib.classrules;

import java.util.Objects;

import javax.jdo.annotations.Discriminator;
import javax.jdo.annotations.NotPersistent;
import javax.jdo.annotations.PersistenceCapable;

import jakarta.inject.Inject;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaEnumConstant;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;

import static com.tngtech.archunit.base.DescribedPredicate.not;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;

import org.apache.causeway.applib.annotation.DomainObject;

import lombok.experimental.UtilityClass;

/**
 * A library of architecture tests to ensure coding conventions are followed for classes annotated with
 * the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation.
 *
 * @since 2.0 {@index}
 */
@UtilityClass
public class ArchitectureJdoRules {

    public static ArchRule every_logicalTypeName_and_jdo_discriminator_must_be_same() {
        return classes()
                .that().areAnnotatedWith(DomainObject.class)
                .and(new DescribedPredicate<>("have a logicalTypeName") {
                    @Override
                    public boolean test(final JavaClass javaClass) {
                        return _LogicalNaming.hasExplicitLogicalName(javaClass);
                    }
                })
                .and(new DescribedPredicate<>("have a @Discriminator") {
                    @Override
                    public boolean test(final JavaClass javaClass) {
                        var discriminatorIfAny = javaClass.tryGetAnnotationOfType(Discriminator.class);
                        return discriminatorIfAny.isPresent();
                    }
                })
                .should(new ArchCondition<>("be the same") {
                    @Override
                    public void check(final JavaClass javaClass, final ConditionEvents conditionEvents) {
                        var logicalTypeName = _LogicalNaming.logicalNameFor(javaClass);
                        var discriminatorValue = javaClass.getAnnotationOfType(Discriminator.class).value();
                        if (!Objects.equals(logicalTypeName, discriminatorValue)) {
                            conditionEvents.add(
                                    new SimpleConditionEvent(javaClass, false,
                                    String.format("@DomainObject(logicalTypeName = '%s') vs @Discriminator('%s')",
                                            logicalTypeName, discriminatorValue)));
                        }
                    }
                });
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * must also be annotated with the Apache Causeway {@link DomainObject} annotation specifying that its
     * {@link DomainObject#nature() nature} is an {@link org.apache.causeway.applib.annotation.Nature#ENTITY entity}.
     */
    public static ArchRule every_jdo_PersistenceCapable_must_be_annotated_with_DomainObject_nature_of_ENTITY() {
        return classes()
                .that().areAnnotatedWith(javax.jdo.annotations.PersistenceCapable.class)
                .should().beAnnotatedWith(CommonPredicates.DomainObject_nature_ENTITY());
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * must also be annotated with the JDO {@link javax.jdo.annotations.Version} annotation (in support of optimistic
     * locking checks).
     *
     * <p>
     *     The rule does <i>not</i> apply to any entities that are subtype entities where there
     *     is a supertype entity.
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_must_be_annotated_with_Version() {
        return classes()
                .that().areAnnotatedWith(javax.jdo.annotations.PersistenceCapable.class)
                .and(not(areSubtypeEntities()))
                .should().beAnnotatedWith(javax.jdo.annotations.Version.class);
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * must also be annotated with the Apache Causeway {@link jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter} annotation
     * with a value of {@link org.apache.causeway.applib.jaxb.PersistentEntityAdapter}<code>.class</code>.
     *
     * <p>
     * Tnis is so that entities can be transparently referenced from XML-style view models.
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_must_be_annotated_as_XmlJavaAdapter_PersistentEntityAdapter() {
        return classes()
                .that().areAnnotatedWith(javax.jdo.annotations.PersistenceCapable.class)
                .should().beAnnotatedWith(CommonPredicates.XmlJavaTypeAdapter_value_PersistentEntityAdapter());
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * must also implement {@link Comparable}.
     *
     * <p>
     * This is so that entities have a natural ordering and can safely be added to parented collections of type
     * {@link java.util.SortedSet}.
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_must_implement_Comparable() {
        return classes()
                .that().areAnnotatedWith(javax.jdo.annotations.PersistenceCapable.class)
                .should().implement(Comparable.class);
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * and is not a subtype entity, must also be annotated with the JDO {@link javax.jdo.annotations.Uniques} or
     * {@link javax.jdo.annotations.Unique} constraints.
     *
     * <p>
     * This is so that entities will have an alternative business key in addition to the system-defined surrogate
     * key.
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_must_be_annotated_as_Uniques_or_Unique() {
        return classes()
                .that().areAnnotatedWith(javax.jdo.annotations.PersistenceCapable.class)
                .and(not(areSubtypeEntities()))
                .should().beAnnotatedWith(javax.jdo.annotations.Uniques.class)
                .orShould().beAnnotatedWith(javax.jdo.annotations.Unique.class);
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable} annotation
     * must also be annotated with the JDO {@link javax.jdo.annotations.Uniques} or {@link javax.jdo.annotations.Unique}
     * constraints.
     *
     * <p>
     * This is so that entities will have an alternative business key in addition to the system-defined surrogate
     * key.
     * </p>
     *
     * <p>
     *     The rule does <i>not</i> apply to any entities that are subtype entities where there
     *     is a supertype entity.
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_with_DATASTORE_identityType_must_be_annotated_as_DataStoreIdentity() {
        return classes()
                .that().areAnnotatedWith(PersistenceCapable_with_DATASTORE_identityType())
                .and(not(areSubtypeEntities()))
                .should().beAnnotatedWith(javax.jdo.annotations.DatastoreIdentity.class);
    }

    /**
     * This rule requires that classes annotated with the JDO {@link javax.jdo.annotations.PersistenceCapable}
     * annotation must have the {@link javax.jdo.annotations.PersistenceCapable#schema() schema}  attribute set.
     *
     * <p>
     * This is so that entity tables are organised into an appropriate structure (ideally mirroring that of the
     * entities).
     * </p>
     */
    public static ArchRule every_jdo_PersistenceCapable_must_have_schema() {
        return classes()
                .that().areAnnotatedWith(PersistenceCapable.class)
                .should().beAnnotatedWith(PersistenceCapable_schema());
    }

    static DescribedPredicate<JavaAnnotation<?>> PersistenceCapable_schema() {
        return new DescribedPredicate<>("@PersistenceCapable(schema=...)") {
            @Override
            public boolean test(final JavaAnnotation<?> javaAnnotation) {
                if (!javaAnnotation.getRawType().isAssignableTo(PersistenceCapable.class)) {
                    return false;
                }
                var properties = javaAnnotation.getProperties();
                var schema = properties.get("schema");
                return schema instanceof String &&
                        ((String) schema).length() > 0;
            }
        };
    }

    static DescribedPredicate<JavaAnnotation<?>> PersistenceCapable_with_DATASTORE_identityType() {
        return new DescribedPredicate<>("@PersistenceCapable(identityType=DATASTORE)") {
            @Override
            public boolean test(final JavaAnnotation<?> javaAnnotation) {
                if (!javaAnnotation.getRawType().isAssignableTo(PersistenceCapable.class)) {
                    return false;
                }
                var properties = javaAnnotation.getProperties();
                var identityType = properties.get("identityType");
                return identityType instanceof JavaEnumConstant &&
                        identityType.toString().equals("IdentityType.DATASTORE");
            }
        };
    }

    /**
     * This rule requires that injected fields in classes annotated with the JDO {@link PersistenceCapable} annotation
     * must also be annotated with JDO {@link javax.jdo.annotations.NotPersistent} annotation.
     *
     * <p>
     * The rationale here is that injected services are managed by the runtime and are not/cannot be persisted.
     * </p>
     *
     * <p>
     * In fact, JDO is tolerant to such fields and will not trip up. So another more pragmatic reason is that, if
     * using JDO type-safe queries, then the injected fields will not appear incorrectly in the generated Q classes.
     * </p>
     */
    public static ArchRule every_injected_field_of_jdo_PersistenceCapable_must_be_annotated_with_NotPersistent() {
        return fields().that()
                .areDeclaredInClassesThat(areEntities())
                .and().areAnnotatedWith(Inject.class)
                .should().beAnnotatedWith(NotPersistent.class)
                ;
    }

    static DescribedPredicate<JavaClass> areEntities() {
        return new DescribedPredicate<>("are entities") {
            @Override
            public boolean test(final JavaClass input) {
                return input.isAnnotatedWith(PersistenceCapable.class);
            }
        };
    }

    static DescribedPredicate<? super JavaClass> areSubtypeEntities() {
        return new DescribedPredicate<>("are subtype entities ") {
            @Override
            public boolean test(final JavaClass input) {
                var superclassIfAny = input.getSuperclass();
                if (!superclassIfAny.isPresent()) {
                    return false;
                }
                var superType = superclassIfAny.get();
                var superClass = superType.toErasure();
                var persistenceCapableIfAny = superClass
                        .tryGetAnnotationOfType(PersistenceCapable.class);
                return persistenceCapableIfAny.isPresent();
            }
        };
    }

}
