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 de.accso.flexinale.FlexinaleModulith1OnionApplication;
import de.accso.flexinale.shared_kernel.DoNotCheckInArchitectureTests;
import org.springframework.boot.test.context.SpringBootTest;

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);
                }
            };

    static final DescribedPredicate<JavaClass> isFlexinaleTestClassAnnotatedWithDoNotCheckAnnotation =
            new DescribedPredicate<>("any Flexinale class annotated with DoNotCheckInArchitectureTests") {
                @Override
                public boolean test(JavaClass clazz) {
                    return clazz.isAnnotatedWith(DoNotCheckInArchitectureTests.class);
                }
            };

    static final DescribedPredicate<JavaClass> isAnyClass =
            new DescribedPredicate<>("any class (to be ignored always)") {
                @Override
                public boolean test(JavaClass clazz) {
                    return true;
                }
            };

    static final DescribedPredicate<JavaClass> isSpringBootTestClass =
            new DescribedPredicate<>("a Flexinale Spring Boot test class") {
                @Override
                public boolean test(JavaClass clazz) {
                    return clazz.isAnnotatedWith(SpringBootTest.class);
                }
            };

    static final DescribedPredicate<JavaClass> isSpringBootApplicationMainClass =
            new DescribedPredicate<>("the Flexinale Spring Boot application main class") {
                @Override
                public boolean test(JavaClass clazz) {
                    return clazz.getFullName().equals(FlexinaleModulith1OnionApplication.class.getName());
                }
            };

    static final DescribedPredicate<JavaClass> isSpringClass =
            new DescribedPredicate<>("a Spring class") {
                @Override
                public boolean test(JavaClass clazz) {
                    return clazz.getPackageName().contains("spring");
                }
            };

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

    @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));
                }
            }
        };
    }

    static ArchCondition<JavaClass> hasNoFieldsOfAnyType(Set<Class<?>> disallowedFieldTypes, Set<String> fieldsToBeFilteredOut) {
        return hasNoFieldsOfAnyType(disallowedFieldTypes, fieldsToBeFilteredOut, true);
    }

    static ArchCondition<JavaClass> hasNoFieldsOfAnyType(Set<Class<?>> disallowedFieldTypes, Set<String> fieldsToBeFilteredOut,
                                                         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)
                Set<JavaField> fields = includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields();

                // check all retrieved fields of clazz
                for (JavaField field: fields) {
                    // but do not check the fields explicitely filtered out or annotated
                    if (! (   (field.isAnnotatedWith(DoNotCheckInArchitectureTests.class))
                            || (fieldsToBeFilteredOut.contains(field.getName()))))
                    {
                        // for any given disallowed field type
                        for (Class<?> fieldType : disallowedFieldTypes) {
                            // check if the field from the clazz is of this disallowed type ...
                            if (field.getRawType().isEquivalentTo(fieldType)) {
                                // ... and if not, add message to ArchConditions result
                                String message = String.format("Class %s has a field %s of type %s",
                                        clazz.getName(), field.getName(), fieldType.getSimpleName());
                                events.add(SimpleConditionEvent.violated(clazz, message));
                            }
                        }
                    }
                }
            }
        };
    }

    static ArchCondition<JavaClass> hasOnlyFieldsImplementing(Set<Class<?>> interfaceTypes,
                                                              Set<String> fieldsToBeFilteredOut, String fieldSuffixToBeFilteredOut) {
        return hasOnlyFieldsImplementing(interfaceTypes, fieldsToBeFilteredOut, fieldSuffixToBeFilteredOut, true);
    }

    static ArchCondition<JavaClass> hasOnlyFieldsImplementing(Set<Class<?>> interfaceTypes,
                                                              Set<String> fieldsToBeFilteredOut, String fieldSuffixToBeFilteredOut,
                                                              boolean includeDerivedFieldsFromSuperClasses) {
        return new ArchCondition<>("a field implementing interfaces check") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                // check all fields (or all field including the derived onces, from superclass / interfaces)
                Set<JavaField> fields = includeDerivedFieldsFromSuperClasses ? clazz.getAllFields() : clazz.getFields();

                // check all retrieved fields of clazz
                for (JavaField field: fields) {
                    // but do not check the fields explicitely filtered out or annotated
                    if (! (   (field.isAnnotatedWith(DoNotCheckInArchitectureTests.class))
                           || (fieldsToBeFilteredOut.contains(field.getName()))
                           || (field.getName().endsWith(fieldSuffixToBeFilteredOut))))
                    {
                        // for any given interface type
                        for (Class<?> interfaceType : interfaceTypes) {
                            // check if the field from the clazz does implement the interface type ...
                            if (!field.getRawType().getAllRawInterfaces()
                                    .stream()
                                    .map(interfaceClazz -> interfaceClazz.reflect())
                                    .toList()
                                    .contains(interfaceType))
                            {
                                // ... and if not, add message to ArchConditions result
                                String message = String.format("Class %s has a field %s not implementing type %s but has type %s",
                                        clazz.getName(), field.getName(), interfaceType.getSimpleName(), field.getRawType().reflect().getSimpleName());
                                events.add(SimpleConditionEvent.violated(clazz, message));
                            }
                        }
                    }
                }
            }
        };
    }
}
