package click.kamil.springstatemachineexporter.analysis.enricher.routing;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
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 static org.assertj.core.api.Assertions.assertThat;

class SharedServiceRoutingPolicyTest {

    private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();

    @Test
    void commonOrderServiceChainIsProvablySharedInfrastructure() {
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PROCESS").build())
                .methodChain(List.of(
                        "com.example.CommonOrderController.post()",
                        "com.example.CommonOrderService.processOrderEvent()"))
                .build();

        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isTrue();
    }

    @Test
    void electronicsVerticalChainIsNotProvablySharedInfrastructure() {
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of(
                        "com.example.electronics.ElectronicsOrderController.post()",
                        "com.example.electronics.ElectronicsOrderService.processOrderEvent()"))
                .build();

        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isFalse();
    }

    @Test
    void chainWithProvenTypeFqnsIsNotProvablySharedInfrastructure() {
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .eventTypeFqn("com.example.OrderEvent")
                        .stateTypeFqn("com.example.OrderState")
                        .build())
                .methodChain(List.of("com.example.SharedDispatcher.payOrder()"))
                .build();

        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isFalse();
    }

    @Test
    void unboundGenericTypeVariablesAreTreatedAsAbsentForSharedInfrastructure() {
        // Last resort only: when call-chain rebinding cannot prove concrete S/E (no subclass
        // parameterization on the path), type-variable placeholders still count as absent.
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .polymorphicEvents(List.of("com.example.OrderEvent.PAY"))
                        .eventTypeFqn("E")
                        .stateTypeFqn("S")
                        .className("com.example.SharedService")
                        .methodName("updateState")
                        .build())
                .methodChain(List.of(
                        "com.example.CommonController.post()",
                        "com.example.SharedService.updateState()"))
                .build();

        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isTrue();
    }

    @Test
    void sharedGenericBaseWithTypeVariablesRoutesViaPolymorphicEventsAsLastResort(
            @TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("OrderConfig.java"), """
                package com.example.order;
                @org.springframework.statemachine.config.EnableStateMachine
                public class OrderStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderState, OrderEvent> {}
                enum OrderEvent { PAY }
                enum OrderState { NEW, PAID }
                """);
        Files.writeString(tempDir.resolve("DocumentConfig.java"), """
                package com.example.document;
                @org.springframework.statemachine.config.EnableStateMachine
                public class DocumentStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<DocumentState, DocumentEvent> {}
                enum DocumentEvent { SUBMIT }
                enum DocumentState { DRAFT }
                """);

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

        // Fallback path: chain has no subclass extends SharedService<OrderState, OrderEvent>,
        // so TriggerMachineTypeRebinder cannot stamp concrete FQNs. Shared-infra + poly remains.
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.PAY")
                        .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                        .eventTypeFqn("E")
                        .stateTypeFqn("S")
                        .className("com.example.SharedService")
                        .methodName("updateState")
                        .build())
                .methodChain(List.of(
                        "com.example.CommonController.pay()",
                        "com.example.SharedService.updateState()"))
                .build();

        Transition pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "PAID")));
        pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));

        Transition submit = new Transition();
        submit.setSourceStates(List.of(State.of("DRAFT", "DRAFT")));
        submit.setTargetStates(List.of(State.of("SUBMITTED", "SUBMITTED")));
        submit.setEvent(Event.of("SUBMIT", "com.example.document.DocumentEvent.SUBMIT"));

        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isTrue();
        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.order.OrderStateMachineConfig", context, List.of(pay)))
                .as("poly PAY must prove affinity to the order machine despite eventTypeFqn=E")
                .isTrue();
        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.document.DocumentStateMachineConfig", context, List.of(submit)))
                .as("PAY must not prove affinity to an unrelated document machine")
                .isFalse();
    }

    @Test
    void sharedInfrastructureMayMultiAttachOnSharedEventInMultiMachineContext(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("OrderConfig.java"), """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine
                public class OrderStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                """);
        Files.writeString(tempDir.resolve("DocumentConfig.java"), """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine
                public class DocumentStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                """);

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

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PROCESS").build())
                .methodChain(List.of(
                        "com.example.CommonOrderController.post()",
                        "com.example.CommonOrderService.processOrderEvent()"))
                .build();

        Transition transition = new Transition();
        transition.setSourceStates(List.of(State.of("NEW", "NEW")));
        transition.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
        transition.setEvent(Event.of("PROCESS", "PROCESS"));
        List<Transition> machineTransitions = List.of(transition);

        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.OrderStateMachineConfig", context, machineTransitions)).isTrue();
        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.DocumentStateMachineConfig", context, machineTransitions)).isTrue();
    }

    @Test
    void verticalChainFailsClosedOnEventMatchInMultiMachineContext(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("ElectronicsConfig.java"), """
                package com.example.electronics;
                @org.springframework.statemachine.config.EnableStateMachine
                public class ElectronicsStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                """);
        Files.writeString(tempDir.resolve("ComputerConfig.java"), """
                package com.example.computerstore;
                @org.springframework.statemachine.config.EnableStateMachine
                public class ComputerStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
                """);

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

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of(
                        "com.example.electronics.ElectronicsOrderController.post()",
                        "com.example.electronics.ElectronicsOrderService.processOrderEvent()"))
                .build();

        Transition transition = new Transition();
        transition.setSourceStates(List.of(State.of("NEW", "NEW")));
        transition.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
        transition.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
        List<Transition> machineTransitions = List.of(transition);

        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.electronics.ElectronicsStateMachineConfig", context, machineTransitions))
                .isTrue();
        assertThat(engine.hasProvenMachineAffinity(
                chain, "com.example.computerstore.ComputerStateMachineConfig", context, machineTransitions))
                .isFalse();
    }

    @Test
    void provenDistinctTypesStillRouteToSingleMachine(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("SharedDispatcher.java"), """
                package com.example;
                public class SharedDispatcher {
                    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> {}
                """);

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

        CallChain orderChain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .eventTypeFqn("com.example.OrderEvent")
                        .stateTypeFqn("com.example.OrderState")
                        .className("com.example.SharedDispatcher")
                        .methodName("payOrder")
                        .build())
                .methodChain(List.of("com.example.SharedDispatcher.payOrder()"))
                .build();

        Transition payTransition = new Transition();
        payTransition.setSourceStates(List.of(State.of("NEW", "NEW")));
        payTransition.setTargetStates(List.of(State.of("PAID", "PAID")));
        payTransition.setEvent(Event.of("PAY", "PAY"));

        assertThat(engine.hasProvenMachineAffinity(
                orderChain, "com.example.OrderStateMachineConfiguration", context, List.of(payTransition)))
                .isTrue();
        assertThat(engine.hasProvenMachineAffinity(
                orderChain, "com.example.DocumentStateMachineConfiguration", context, List.of(payTransition)))
                .isFalse();
    }
}
