package architecturetests;

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.JavaClasses;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Optional;
import java.util.Set;

@SuppressWarnings("unused")
public final class ArchUnitHelper {

    static final String PACKAGE_PREFIX         = "de.accso.flexinale";
    static final String PACKAGE_PREFIX_TOCHECK = "de.accso.flexinale";
    static final JavaClasses allJavaClassesToCheck =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages(PACKAGE_PREFIX_TOCHECK);

    static DescribedPredicate<JavaClass> isFlexinaleClass =
            new DescribedPredicate<>("any Flexinale class") {
                @Override
                public boolean test(JavaClass clazz) {
                    return clazz.getPackageName().startsWith(PACKAGE_PREFIX);
                }
            };

    @SuppressWarnings("rawtypes")
    static final DescribedPredicate<JavaAnnotation> isEntityAnnotation =
            new DescribedPredicate<>("a Entity annotation") {
                @Override
                public boolean test(final JavaAnnotation anno) {
                    return anno.getType().getName().contains("jakarta.persistence.Entity");
                }
            };

    static ArchCondition<JavaClass> hasFieldOfType(String fieldName, Class<?> fieldType) {
        return hasFieldOfType(fieldName, fieldType, true);
    }

    static ArchCondition<JavaClass> hasFieldOfType(String fieldName, Class<?> fieldType,
                                                   boolean includeDerivedFieldsFromSuperClasses) {
        return new ArchCondition<>("a field type check") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                // check all fields (or all field including the derived onces, from superclass / interfaces)
                Optional<JavaField> optionalField =
                        (includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields())
                                .stream()
                                // and retrieve the field with the given name to check
                                .filter(field -> fieldName.equals(field.getName()))
                                .findFirst();

                // if there is not such a field with the given name ...
                if (optionalField.isEmpty()) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a field %s", clazz.getName(), fieldName);
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
                else
                // if there is a field with the given name, but it is not of the given type ...
                if (!(optionalField.get().getRawType().isEquivalentTo(fieldType))) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a field %s of type %s",
                            clazz.getName(), fieldName, fieldType.getName());
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
            }
        };
    }

    static ArchCondition<JavaClass> hasFieldsWithSuffixOfType(String fieldNameSuffix, Class<?> fieldType) {
        return hasFieldsWithSuffixOfType(fieldNameSuffix, fieldType, true);
    }

    static ArchCondition<JavaClass> hasFieldsWithSuffixOfType(String fieldNameSuffix, Class<?> fieldType,
                                                              boolean includeDerivedFieldsFromSuperClasses) {
        return new ArchCondition<>("a field type check") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                // check all fields (or all field including the derived onces, from superclass / interfaces)
                List<JavaField> fields =
                        (includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields())
                                .stream()
                                // and retrieve the field with the given name to check
                                .filter(field -> field.getName().endsWith(fieldNameSuffix))
                                .toList();

                // if there are fields with the given name suffix, but it is not of the given type ...
                for (JavaField field: fields) {
                    if (!field.getRawType().isEquivalentTo(fieldType)) {
                        // ... then add message to ArchConditions result
                        String message = String.format("Class %s has a field %s but it is not of type %s",
                                clazz.getName(), field.getName(), fieldType.getName());
                        events.add(SimpleConditionEvent.violated(clazz, message));
                    }
                }
            }
        };
    }

    static ArchCondition<JavaClass> hasFieldPublicFinal(String fieldName) {
        return hasFieldPublicFinal(fieldName, true);
    }

    static ArchCondition<JavaClass> hasFieldPublicFinal(String fieldName,
                                                        boolean includeDerivedFieldsFromSuperClasses) {
        return hasFieldModifiers(fieldName, includeDerivedFieldsFromSuperClasses, Set.of(JavaModifier.PUBLIC, JavaModifier.FINAL));
    }

    static ArchCondition<JavaClass> hasFieldPrivateFinal(String fieldName) {
        return hasFieldPrivateFinal(fieldName, true);
    }

    static ArchCondition<JavaClass> hasFieldPrivateFinal(String fieldName,
                                                         boolean includeDerivedFieldsFromSuperClasses) {
        return hasFieldModifiers(fieldName, includeDerivedFieldsFromSuperClasses, Set.of(JavaModifier.PRIVATE, JavaModifier.FINAL));
    }

    private static ArchCondition<JavaClass> hasFieldModifiers(String fieldName,
                                                              boolean includeDerivedFieldsFromSuperClasses,
                                                              Set<JavaModifier> modifiers) {
        return new ArchCondition<>("a field check for public final") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                // check all fields (or all field including the derived onces, from superclass / interfaces)
                Optional<JavaField> optionalField =
                        (includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields())
                                .stream()
                                // and retrieve the field with the given name to check
                                .filter(field -> fieldName.equals(field.getName()))
                                .findFirst();

                // if there is not such a field with the given name ...
                if (optionalField.isEmpty()) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a field %s", clazz.getName(), fieldName);
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
                else
                // if there is a field with the given name, but it does not have the given modifiers
                if (!(optionalField.get().getModifiers().containsAll(modifiers))) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a final field %s with modifiers %s",
                            clazz.getName(), fieldName, modifiers);
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
            }
        };
    }

    static ArchCondition<JavaClass> hasFieldAnnotatedWithType(String fieldName, Class<? extends Annotation> annotationType) {
        return hasFieldAnnotatedWithType(fieldName, annotationType, true);
    }

    static ArchCondition<JavaClass> hasFieldAnnotatedWithType(String fieldName, Class<? extends Annotation> annotationType,
                                                              boolean includeDerivedFieldsFromSuperClasses) {
        return new ArchCondition<>("a field annotation check") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                // check all fields (or all field including the derived onces, from superclass / interfaces)
                Optional<JavaField> optionalField =
                        (includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields())
                        .stream()
                        // and retrieve the field with the given name to check
                        .filter(field -> fieldName.equals(field.getName()))
                        .findFirst();

                // if there is not such a field with the given name ...
                if (optionalField.isEmpty()) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a field %s", clazz.getName(), fieldName);
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
                else
                // if there is a field with the given name, but it is not annotated with the annotation type ...
                if (!(optionalField.get().isAnnotatedWith(annotationType))) {
                    // ... then add message to ArchConditions result
                    String message = String.format("Class %s does not have a field %s annotated with type %s",
                            clazz.getName(), fieldName, annotationType.getName());
                    events.add(SimpleConditionEvent.violated(clazz, message));
                }
            }
        };
    }
}
