package architecturetests;

import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import com.tngtech.archunit.library.Architectures;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import jakarta.transaction.Transactional;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static architecturetests.ArchUnitHelper.*;
import static architecturetests.FlexinaleComponent.*;

public class OnionDependenciesTest {

    private record Layer(String layer, String... packages) {}

    private static Stream<Arguments> allFlexinaleComponents() {
        return Stream.of(
                    Arguments.of(besucherportal.name)
                  , Arguments.of(backoffice.name)
                  , Arguments.of(ticketing.name)
                //, Arguments.of(common.name)                  // does not have the onion structure
                //, Arguments.of(security.name)                // does not have the onion structure
                //, Arguments.of(ticketing_api_contract.name)   // does not have the onion structure
                //, Arguments.of(security_api_contract.name)    // does not have the onion structure
                //, Arguments.of(backoffice_api_contract.name ) // does not have the onion structure
        );
    }

    private static Stream<Arguments> allFlexinaleComponentsWithOutBesucherportal() {
        return Stream.of(
                  Arguments.of(backoffice.name)
                , Arguments.of(ticketing.name)
                //, Arguments.of(common.name)                  // does not have the onion structure
                //, Arguments.of(security.name)                // does not have the onion structure
                //, Arguments.of(ticketing_api_contract.name)   // does not have the onion structure
                //, Arguments.of(security_api_contract.name)    // does not have the onion structure
                //, Arguments.of(backoffice_api_contract.name ) // does not have the onion structure
        );
    }

    @ParameterizedTest
    @MethodSource("allFlexinaleComponents")
    void test_onion_architecture_for_each_component(final String component) {
        String componentPackagePrefix = PACKAGE_PREFIX + "." + component;

        final Layer apiRest          = new Layer("API Rest",          componentPackagePrefix + ".api_in.rest..");
        final Layer apiWeb           = new Layer("API Web",           componentPackagePrefix + ".api_in.web..");
        final Layer application      = new Layer("Application",       componentPackagePrefix + ".application..");
        final Layer domainServices   = new Layer("DomainServices",    componentPackagePrefix + ".domain.services..", componentPackagePrefix + ".domain.dao..");
        final Layer domainModel      = new Layer("DomainModel",       componentPackagePrefix + ".domain.model..");
        final Layer infrastructure   = new Layer("Infra",             componentPackagePrefix + ".infrastructure");
        final Layer infraPersistence = new Layer("Infra Persistence", componentPackagePrefix + ".infrastructure.persistence..");
        final Layer sharedKernel     = new Layer("SharedKernel",      componentPackagePrefix + ".shared_kernel..");

        Architectures.LayeredArchitecture layeredArchitecture = Architectures.layeredArchitecture()
                .consideringOnlyDependenciesInAnyPackage(componentPackagePrefix + "..")
                .withOptionalLayers(true)  // needed in modulith-2 (while commented out in modulith-1)
                .layer(apiRest.layer)         .definedBy(apiRest.packages)
                .layer(apiWeb.layer)          .definedBy(apiWeb.packages)
                .layer(application.layer)     .definedBy(application.packages)
                .layer(domainServices.layer)  .definedBy(domainServices.packages)
                .layer(domainModel.layer)     .definedBy(domainModel.packages)
                .layer(infrastructure.layer)  .definedBy(infrastructure.packages)
                .layer(infraPersistence.layer).definedBy(infraPersistence.packages)
                .layer(sharedKernel.layer)    .definedBy(sharedKernel.packages);

        // shared kernel
        Architectures.LayeredArchitecture onionArchitecture = layeredArchitecture
                .whereLayer(sharedKernel.layer).mayNotAccessAnyLayer()

        // outer layer ring: no ingoing dependencies allowed
                .whereLayer( apiWeb.layer).mayNotBeAccessedByAnyLayer()
                .whereLayer(apiRest.layer).mayNotBeAccessedByAnyLayer()
                .whereLayer(infrastructure.layer).mayNotBeAccessedByAnyLayer()

        // web and rest - define outgoing dependencies
                .whereLayer(apiWeb.layer)
                .mayOnlyAccessLayers(application.layer, domainModel.layer, sharedKernel.layer)

                .whereLayer(apiRest.layer)
                .mayOnlyAccessLayers(application.layer, domainModel.layer, sharedKernel.layer)

        // infrastructure - define outgoing dependencies
                .whereLayer(infrastructure.layer)
                .mayOnlyAccessLayers(application.layer, domainServices.layer, infraPersistence.layer, sharedKernel.layer)

                .whereLayer(infraPersistence.layer)
                .mayOnlyAccessLayers(domainServices.layer, domainModel.layer, sharedKernel.layer)

        // application - define outgoing dependencies
                .whereLayer(application.layer)
                .mayOnlyAccessLayers(domainServices.layer, domainModel.layer, sharedKernel.layer)

        // domain services - define outgoing dependencies
                .whereLayer(domainServices.layer)
                .mayOnlyAccessLayers(sharedKernel.layer, domainModel.layer)

        // domain model - define outgoing dependencies
                .whereLayer(domainModel.layer)
                .mayOnlyAccessLayers(sharedKernel.layer);

        // check the onion architecture
        onionArchitecture
                .ignoreDependency(isSpringBootTestClass, isSpringBootApplicationMainClass)
                .ignoreDependency(isFlexinaleTestClassAnnotatedWithDoNotCheckAnnotation, isAnyClass)
                .because("we want to enforce the onion architecture inside each component")
                .check(allJavaClassesToCheck);
    }

    @ParameterizedTest
    @MethodSource("allFlexinaleComponents")
    void test_that_only_onion_application_services_are_annotated_with_transactional(final String component) {
        // arrange
        String componentPackagePrefix = PACKAGE_PREFIX + "." + component;

        String applicationServicesPackage = componentPackagePrefix + ".application.services..";

        ArchRuleDefinition.classes().that()
                .resideInAnyPackage(componentPackagePrefix + "..") // only check classes in Component under consideration
                .and()
                .areAnnotatedWith(Transactional.class)
                .should().resideInAnyPackage(applicationServicesPackage)
                .check(allJavaClassesToCheck);
    }

    @ParameterizedTest
    @MethodSource("allFlexinaleComponentsWithOutBesucherportal")
    void test_that_all_onion_application_services_are_annotated_with_transactional(final String component) {
        // arrange
        String componentPackagePrefix = PACKAGE_PREFIX + "." + component;

        String applicationServicesPackage = componentPackagePrefix + ".application.services..";

        ArchRuleDefinition.classes().that()
                .resideInAnyPackage(applicationServicesPackage)
                .and()
                .haveSimpleNameEndingWith("Service")
                .and()
                .areNotAnnotatedWith(DoNotCheckInArchitectureTests.class)
                .and()
                .areNotInterfaces()
                .should().beAnnotatedWith(Transactional.class)
                .check(allJavaClassesToCheck);
    }
}
