package architecturetests;

import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import com.tngtech.archunit.library.Architectures;
import de.accso.flexinale.shared_kernel.DoNotCheckInArchitectureTests;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;

import static architecturetests.ArchUnitHelper.*;

public class OnionDependenciesTest {

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

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

    @Test
    void test_onion_architecture() {
        Architectures.LayeredArchitecture layeredArchitecture = Architectures.layeredArchitecture()
                .consideringOnlyDependenciesInAnyPackage(PACKAGE_PREFIX + "..")
                //.withOptionalLayers(true)
                .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(infraSecurity.layer)   .definedBy(infraSecurity.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, infraSecurity.layer, sharedKernel.layer)

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

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

                .whereLayer(infraSecurity.layer)
                .mayOnlyAccessLayers(application.layer, domainServices.layer, domainModel.layer, sharedKernel.layer)

                .whereLayer(infraPersistence.layer)
                .mayOnlyAccessLayers(domainServices.layer, domainModel.layer, infraSecurity.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")
                .check(allJavaClassesToCheck);
    }

    @Test
    void test_that_only_onion_application_services_are_annotated_with_transactional() {
        final String applicationServicesPackage = PACKAGE_PREFIX + ".application.services..";
        final String infraSecurityServices      = PACKAGE_PREFIX + ".infrastructure.security.services..";

        ArchRuleDefinition.classes().that()
                .areAnnotatedWith(Transactional.class)
                .should()
                .resideInAnyPackage(applicationServicesPackage, infraSecurityServices)
                .check(allJavaClassesToCheck);
    }

    @Test
    void test_that_all_onion_application_services_are_annotated_with_transactional() {
        final String applicationServicesPackage = PACKAGE_PREFIX + ".application.services..";
        final String infraSecurityServices      = PACKAGE_PREFIX + ".infrastructure.security.services..";

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