package org.raddatz.familienarchiv.shared;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import jakarta.persistence.Entity;
import org.raddatz.familienarchiv.FamilienarchivApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RestController;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packagesOf = FamilienarchivApplication.class)
class ArchitectureTest {

    // Rule 1: Controllers must never inject repositories directly.
    // Security rationale: bypassing the service layer skips @RequirePermission
    // AOP checks that are enforced on service methods.
    @ArchTest
    static final ArchRule no_controller_injects_repository_directly =
        noClasses()
            .that().areAnnotatedWith(RestController.class)
            .should().dependOnClassesThat().areAssignableTo(JpaRepository.class);

    // Rule 2: Services access only their own domain's repositories, never a foreign domain's.
    // Prevents hidden coupling between domains that should communicate via service APIs.
    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_document =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..document..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("document"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_person =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..person..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("person"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_tag =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..tag..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("tag"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_user =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..user..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("user"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_dashboard =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..dashboard..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("dashboard"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_geschichte =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..geschichte..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("geschichte"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_notification =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..notification..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("notification"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_ocr =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..ocr..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("ocr"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_importing =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..importing..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("importing"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_audit =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..audit..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("audit"));

    @ArchTest
    static final ArchRule services_only_access_own_domain_repositories_timeline =
        noClasses()
            .that().areAnnotatedWith(Service.class)
            .and().resideInAPackage("..timeline..")
            .should().dependOnClassesThat(foreignJpaRepositoryFor("timeline"));

    // Rule 3: Infrastructure @Configuration classes must not end up scattered in domain packages.
    // Keeps cross-cutting setup (security, async, DB, storage) in dedicated packages
    // where it can be audited and reasoned about independently.
    @ArchTest
    static final ArchRule no_configuration_class_in_domain_packages =
        noClasses()
            .that().areAnnotatedWith(Configuration.class)
            .and().areNotAnnotatedWith(SpringBootApplication.class)
            .should().resideInAnyPackage(
                "..document..", "..person..", "..tag..",
                "..geschichte..", "..notification..", "..ocr..",
                "..filestorage..", "..importing..", "..dashboard..", "..audit.."
            );

    // Rule 4: Entities belong to their domain packages, not to a shared model layer.
    // Prevents regression to a flat, layer-based package layout.
    @ArchTest
    static final ArchRule entities_reside_in_domain_packages =
        classes()
            .that().areAnnotatedWith(Entity.class)
            .should().resideInAnyPackage(
                "..document..", "..person..", "..tag..", "..user..",
                "..geschichte..", "..notification..", "..ocr..", "..audit..",
                "..timeline.."
            );

    // TODO Rule 5: Controllers expose endpoints under their domain prefix
    // (e.g., classes in ..document.. are annotated with @RequestMapping("/api/documents")).
    // Implementing this requires a custom ArchUnit DescribedPredicate inspecting the
    // @RequestMapping annotation value — deferred due to brittleness concerns.
    // Tracked in: http://heim-nas:3005/marcel/familienarchiv/issues/427

    private static DescribedPredicate<JavaClass> foreignJpaRepositoryFor(String ownDomain) {
        // Exact-segment match: prevents a domain name that is a substring of another
        // (e.g. "tag" inside "tagging") from silently escaping the predicate.
        // The pattern matches the domain as a complete path segment, with an optional sub-package.
        String ownPackagePattern = ".*\\.familienarchiv\\." + ownDomain + "(\\..+)?$";
        return new DescribedPredicate<JavaClass>("be a JPA repository from a domain other than " + ownDomain) {
            @Override
            public boolean test(JavaClass clazz) {
                return clazz.isAssignableTo(JpaRepository.class)
                       && !clazz.getPackageName().matches(ownPackagePattern);
            }
        };
    }
}
