package architecturetests;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.PackageMatcher;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.stream.Collectors;

import static architecturetests.ArchUnitHelper.PACKAGE_PREFIX;
import static architecturetests.ArchUnitHelper.allJavaClassesToCheck;
import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

public class ComponentDependenciesTest {

    @Test
    void test_component_dependencies_are_free_of_cycles() {
        PackageMatchingSliceIdentifierIgnoringDependenciesToApiContract sliceAssignment =
                new PackageMatchingSliceIdentifierIgnoringDependenciesToApiContract(PACKAGE_PREFIX + ".(*)..");

        // arrange, act, assert
        SlicesRuleDefinition.slices()
                .assignedFrom(sliceAssignment)
                .should()
                .beFreeOfCycles()
                .because("no cycles between components allowed")
                .check(allJavaClassesToCheck);
    }

    // ------------------------------------------------------------------------------------------------------------

    private record IncorrectDependencyFromTo(String fromClazzName, String toClazzName) {
        @Override
        public String toString() {
            return "%s -> %s".formatted(fromClazzName, toClazzName);
        }
    }

    @Test
    void test_besucherportal_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.besucherportal;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut,
                        FlexinaleComponent.security_api_contract,
                        FlexinaleComponent.backoffice_api_contract,
                        FlexinaleComponent.ticketing_api_contract,
                        FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_besucherportal_api_contract_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.besucherportal_api_contract;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut, FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_ticketing_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.ticketing;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut,
                        FlexinaleComponent.besucherportal_api_contract,
                        FlexinaleComponent.security_api_contract,
                        FlexinaleComponent.backoffice_api_contract,
                        FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_ticketing_api_contract_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.ticketing_api_contract;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut, FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_backoffice_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.backoffice;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut,
                        FlexinaleComponent.security_api_contract,
                        FlexinaleComponent.backoffice_api_contract,
                        FlexinaleComponent.ticketing_api_contract,
                        FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_security_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.security;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut,
                        FlexinaleComponent.security_api_contract,
                        FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_backoffice_api_contract_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.backoffice_api_contract;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut, FlexinaleComponent.common);

        // assert
        assertThat(incorrectDependencies)
                .withFailMessage("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies))
                .isEmpty();
    }

    @Test
    void test_common_has_only_allowed_dependencies_to_self() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.common;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanSelf(cut);

        // assert
        if (!incorrectDependencies.isEmpty()) {
            fail("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies));
        }
    }

    @Test
    void test_security_api_contract_has_only_allowed_dependencies_to_other_flexinale_components() {
        // arrange
        FlexinaleComponent cut = FlexinaleComponent.security_api_contract;

        // act
        Set<IncorrectDependencyFromTo> incorrectDependencies =
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(cut, FlexinaleComponent.common);

        // assert
        if (!incorrectDependencies.isEmpty()) {
            fail("incorrect dependencies found for component %s: %s".formatted(cut.name(), incorrectDependencies));
        }
    }

    private Set<IncorrectDependencyFromTo>
                getIncorrectDependenciesFromComponentClazzesOtherThanSelf(FlexinaleComponent fromComponent)
    {
        return getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(fromComponent);
    }

    private Set<IncorrectDependencyFromTo>
                getIncorrectDependenciesFromComponentClazzesOtherThanAllowed(FlexinaleComponent    fromComponent,
                                                                             FlexinaleComponent... toAllowedComponents)
    {
        Set<String> toAllowedPackages = Arrays.stream(toAllowedComponents)
                                              .map(component -> component.pkg)
                                              .collect(Collectors.toSet());
        // add self as allowed dependency
        toAllowedPackages.add(fromComponent.pkg);

        Set<IncorrectDependencyFromTo> allIncorrectDependenciesForComponent = new HashSet<>();

        // check all clazzes in the fromComponent
        JavaClasses fromComponentClazzes = new ClassFileImporter().importPackages(fromComponent.pkg);
        fromComponentClazzes.forEach(clazz -> {
            if (!clazz.getFullName().startsWith(fromComponent.pkg))
                throw new DeveloperMistakeException("wrong clazz filter implemented");

            // for any clazz of the fromComponent: check its outgoing dependencies and keep the wrong ones
            HashSet<IncorrectDependencyFromTo> incorrectDependenciesForClazz =
                    clazz.getDirectDependenciesFromSelf().stream()
                    .filter(dependency -> {
                        JavaClass targetClazz = dependency.getTargetClass();

                        // filter out all clazzes outside of our component scope (like dependencies to java... or org... etc)
                        if (! targetClazz.getPackageName().startsWith(PACKAGE_PREFIX))
                            return false;

                        // filter out DoNotCheckInArchitectureTests
                        if (targetClazz.getFullName().equals(DoNotCheckInArchitectureTests.class.getSimpleName()))
                            return false;

                        // filter out annotation usages of DoNotCheckInArchitectureTests
                        if (dependency.getOriginClass().isAnnotatedWith(DoNotCheckInArchitectureTests.class))
                            return false;

                        // filter out all allowed dependencies
                        for (String toAllowedPackage : toAllowedPackages) {
                            if (targetClazz.getPackageName().startsWith(toAllowedPackage)) {
                                return false;
                            }
                        }

                        // finally now keep all, where the dependency target is not explicitely allowed
                        return true;
                    })
                    .map(it -> new IncorrectDependencyFromTo(it.getOriginClass().getFullName(),
                                                             it.getTargetClass().getFullName()))
                    .collect(Collectors.toCollection(HashSet::new)); // do not use .toSet() as this results in an UnsupportedOperation exception

            allIncorrectDependenciesForComponent.addAll(incorrectDependenciesForClazz);
        });

        return allIncorrectDependenciesForComponent;
    }
}

// copied from ArchUnit's internal class PackageMatchingSliceIdentifier
class PackageMatchingSliceIdentifierIgnoringDependenciesToApiContract implements SliceAssignment {
    private final String packageIdentifier;

    PackageMatchingSliceIdentifierIgnoringDependenciesToApiContract(String packageIdentifier) {
        this.packageIdentifier = packageIdentifier;
    }

    @Override
    public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
        // ignore dependencies to ..api_contract..
        if (javaClass.getPackageName().contains("api_contract")) {
            return SliceIdentifier.ignore();
        }

        PackageMatcher matcher = PackageMatcher.of(packageIdentifier);
        Optional<List<String>> result = matcher.match(javaClass.getPackageName()).map(TO_GROUPS);
        List<String> parts = result.orElse(emptyList());
        return parts.isEmpty() ? SliceIdentifier.ignore() : SliceIdentifier.of(parts);
    }

    @Override
    public String getDescription() {
        return slicesMatchingDescription(packageIdentifier);
    }

    private static String slicesMatchingDescription(String packageIdentifier) {
        return "'" + packageIdentifier + "'";
    }
}