package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class MachineScopeFilterEntryPointTest {

    @Test
    void shouldDeriveScopedEntryPointsFromCallChainsAndKeepGenericPathVariables() {
        EntryPoint orderPay = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/order/pay")
                .className("com.example.StateMachineController")
                .methodName("payOrder")
                .build();
        EntryPoint genericTransition = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/{machineType}/transition/{event}")
                .className("com.example.StateMachineController")
                .methodName("transition")
                .metadata(Map.of("path", "/api/machine/{machineType}/transition/{event}"))
                .build();

        CallChain payChain = CallChain.builder()
                .entryPoint(orderPay)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .eventTypeFqn("com.example.OrderEvent")
                        .build())
                .build();

        List<EntryPoint> scoped = EntryPointScopeResolver.scopeFromCallChains(
                List.of(payChain),
                List.of(orderPay, genericTransition));

        assertThat(scoped).extracting(EntryPoint::getName)
                .containsExactlyInAnyOrder(
                        "POST /api/machine/order/pay",
                        "POST /api/machine/{machineType}/transition/{event}");
    }

    @Test
    void shouldScopeEnterpriseDedicatedPayEndpointToOrderMachine(@TempDir Path tempDir) throws Exception {
        writeEnterpriseWebLayer(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        EntryPoint payOrder = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/order/pay")
                .className("com.example.StateMachineController")
                .methodName("payOrder")
                .metadata(Map.of("path", "/api/machine/order/pay"))
                .build();
        EntryPoint submitDocument = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/document/submit")
                .className("com.example.StateMachineController")
                .methodName("submitDocument")
                .metadata(Map.of("path", "/api/machine/document/submit"))
                .build();

        List<EntryPoint> orderScoped = MachineScopeFilter.filterEntryPointsForMachine(
                List.of(payOrder, submitDocument),
                "com.example.OrderStateMachineConfiguration",
                context);

        assertThat(orderScoped).extracting(EntryPoint::getName)
                .containsExactly("POST /api/machine/order/pay");
    }

    @Test
    void shouldExcludeDocumentDedicatedEndpointFromOrderMachine(@TempDir Path tempDir) throws Exception {
        writeEnterpriseWebLayer(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        EntryPoint submitDocument = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/document/submit")
                .className("com.example.StateMachineController")
                .methodName("submitDocument")
                .metadata(Map.of("path", "/api/machine/document/submit"))
                .build();

        List<EntryPoint> orderScoped = MachineScopeFilter.filterEntryPointsForMachine(
                List.of(submitDocument),
                "com.example.OrderStateMachineConfiguration",
                context);

        assertThat(orderScoped).isEmpty();
    }

    @Test
    void shouldFilterSharedDispatcherCallChainsByProvenMachineTypes(@TempDir Path tempDir) throws Exception {
        writeEnterpriseWebLayer(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        CallChain payChain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .eventTypeFqn("com.example.OrderEvent")
                        .stateTypeFqn("com.example.OrderState")
                        .className("com.example.StateMachineDispatcher")
                        .methodName("payOrder")
                        .build())
                .methodChain(List.of("com.example.StateMachineController.payOrder()"))
                .build();
        CallChain documentChain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.DocumentEvent.SUBMIT")
                        .eventTypeFqn("com.example.DocumentEvent")
                        .stateTypeFqn("com.example.DocumentState")
                        .className("com.example.StateMachineDispatcher")
                        .methodName("submitDocument")
                        .build())
                .methodChain(List.of("com.example.StateMachineController.submitDocument()"))
                .build();

        List<CallChain> orderScoped = MachineScopeFilter.filterCallChainsForMachine(
                List.of(payChain, documentChain),
                "com.example.OrderStateMachineConfiguration",
                context);

        assertThat(orderScoped).hasSize(1);
        assertThat(orderScoped.get(0).getTriggerPoint().getEvent()).isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldExcludeOrderAndAuditChainsFromPaymentMachineExport(@TempDir Path tempDir) throws Exception {
        writeExtendedStringMachineSample(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        CallChain orderSubmit = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/orders/submit")
                        .className("com.example.OrderController")
                        .methodName("submitOrder")
                        .build())
                .methodChain(List.of(
                        "com.example.OrderController.submitOrder",
                        "com.example.OrderService.processSubmit"))
                .triggerPoint(TriggerPoint.builder()
                        .event("java.lang.String.SUBMIT_EVENT")
                        .eventTypeFqn("java.lang.String")
                        .stateTypeFqn("java.lang.String")
                        .className("com.example.OrderService")
                        .methodName("processSubmit")
                        .build())
                .build();
        CallChain paymentAuthorize = CallChain.builder()
                .methodChain(List.of("com.example.PaymentService.processPayment"))
                .triggerPoint(TriggerPoint.builder()
                        .event("java.lang.String.AUTHORIZE")
                        .eventTypeFqn("java.lang.String")
                        .stateTypeFqn("java.lang.String")
                        .stateMachineId("paymentStateMachine")
                        .className("com.example.PaymentService")
                        .methodName("processPayment")
                        .build())
                .build();

        List<CallChain> paymentScoped = MachineScopeFilter.filterCallChainsForMachine(
                List.of(orderSubmit, paymentAuthorize),
                "com.example.PaymentStateMachineConfig",
                context,
                List.of());

        assertThat(paymentScoped).hasSize(1);
        assertThat(paymentScoped.get(0).getTriggerPoint().getEvent()).isEqualTo("java.lang.String.AUTHORIZE");
    }

    private static void writeExtendedStringMachineSample(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Machines.java"), """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine(name = "paymentStateMachine")
                public class PaymentStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                @org.springframework.statemachine.config.EnableStateMachine(name = "extendedStateMachine")
                public class ExtendedStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                class OrderController {
                    private final OrderService orderService = new OrderService();
                    public void submitOrder() { orderService.processSubmit(); }
                }
                class OrderService {
                    org.springframework.statemachine.StateMachine<String, String> stateMachine;
                    public void processSubmit() { stateMachine.sendEvent("SUBMIT_EVENT"); }
                }
                class PaymentService {
                    @org.springframework.beans.factory.annotation.Qualifier("paymentStateMachine")
                    org.springframework.statemachine.StateMachine<String, String> stateMachine;
                    public void processPayment() { stateMachine.sendEvent("AUTHORIZE"); }
                }
                """);
    }

    private static void writeEnterpriseWebLayer(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                public class StateMachineController {
                    private final StateMachineDispatcher dispatcher = new StateMachineDispatcher();
                    public void payOrder() { dispatcher.payOrder(); }
                    public void submitDocument() { dispatcher.submitDocument(); }
                }
                class StateMachineDispatcher {
                    void payOrder() {
                        org.springframework.statemachine.StateMachine<OrderState, OrderEvent> machine = null;
                        machine.sendEvent(OrderEvent.PAY);
                    }
                    void submitDocument() {
                        org.springframework.statemachine.StateMachine<DocumentState, DocumentEvent> machine = null;
                        machine.sendEvent(DocumentEvent.SUBMIT);
                    }
                }
                enum OrderEvent { PAY }
                enum OrderState { NEW, PAID }
                enum DocumentEvent { SUBMIT }
                enum DocumentState { DRAFT }
                class OrderStateMachineConfiguration
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderState, OrderEvent> {}
                class DocumentStateMachineConfiguration
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<DocumentState, DocumentEvent> {}
                """);
    }
}
