package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

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

@Tag("control_flow_sibling_avoidance")
class GenericEventDetectorControlFlowTest {

    @Test
    void shouldDetectStateFromDirectIfWrapper(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void processOrder(OrderState state) {
                        if (state == OrderState.PENDING) {
                            sm.sendEvent(OrderEvent.PAY);
                        }
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                
                enum OrderState { PENDING }
                enum OrderEvent { PAY }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        
        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
        
        assertThat(triggers).hasSize(1);
        TriggerPoint tp = triggers.get(0);
        assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
        assertThat(tp.getSourceState()).isEqualTo("PENDING");
    }

    @Test
    void shouldDetectStateFromGuardClauseSibling(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void processOrder(OrderState state) {
                        if (state != OrderState.PENDING) {
                            return;
                        }
                        
                        sm.sendEvent(OrderEvent.PAY);
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                
                enum OrderState { PENDING }
                enum OrderEvent { PAY }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        
        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
        
        assertThat(triggers).hasSize(1);
        TriggerPoint tp = triggers.get(0);
        assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
        assertThat(tp.getSourceState()).isEqualTo("PENDING");
    }

    @Test
    void shouldDetectStateFromSwitchCaseSibling(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void processOrder(OrderState state) {
                        switch (state) {
                            case PENDING:
                                sm.sendEvent(OrderEvent.PAY);
                                break;
                            case PAID:
                                sm.sendEvent(OrderEvent.SHIP);
                                break;
                        }
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                
                enum OrderState { PENDING, PAID }
                enum OrderEvent { PAY, SHIP }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        
        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
        
        assertThat(triggers).hasSize(2);
        
        TriggerPoint payTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().get();
        assertThat(payTrigger.getSourceState()).isEqualTo("PENDING");
        
        TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
        assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
    }

    @Test
    void shouldDetectStateFromSwitchExpressionArrowSyntax(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void processOrder(OrderState state) {
                        switch (state) {
                            case PENDING -> sm.sendEvent(OrderEvent.PAY);
                            case PAID -> sm.sendEvent(OrderEvent.SHIP);
                        }
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                
                enum OrderState { PENDING, PAID }
                enum OrderEvent { PAY, SHIP }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());

        assertThat(triggers).hasSize(2);
        assertThat(triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().orElseThrow()
                .getSourceState()).isEqualTo("PENDING");
        assertThat(triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().orElseThrow()
                .getSourceState()).isEqualTo("PAID");
    }

    @Test
    void shouldNotTreatMachineTypeDiscriminatorAsSourceState(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class Dispatcher {
                    private StateMachine sm;
                    public void dispatch(String machineType, String eventString) {
                        if ("ORDER".equalsIgnoreCase(machineType)) {
                            sm.sendEvent("event");
                        }
                    }
                }
                class StateMachine {
                    public void sendEvent(String e) {}
                }
                """;
        Files.writeString(tempDir.resolve("Dispatcher.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.Dispatcher").getRoot());

        assertThat(triggers).hasSize(1);
        assertThat(triggers.get(0).getSourceState()).isNull();
    }

    @Test
    void shouldNotTreatJmsMessageSwitchCasesAsSourceState(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderListener {
                    private StateMachine sm;
                    public void onMessage(String message) {
                        switch (message) {
                            case "PAY" -> sm.sendEvent(OrderEvent.PAY);
                            case "SHIP" -> sm.sendEvent(OrderEvent.SHIP);
                        }
                    }
                }
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                enum OrderState { NEW, PAID, SHIPPED }
                enum OrderEvent { PAY, SHIP }
                """;
        Files.writeString(tempDir.resolve("OrderListener.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderListener").getRoot());

        assertThat(triggers).hasSize(2);
        assertThat(triggers).allSatisfy(t -> assertThat(t.getSourceState())
                .as("JMS payload case labels must not become sourceState")
                .isNull());
    }

    @Test
    void shouldNotTreatPayloadAnnotatedCmdSwitchAsSourceState(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                import org.springframework.messaging.handler.annotation.Payload;
                public class OrderListener {
                    private StateMachine sm;
                    public void onMessage(@Payload String cmd) {
                        switch (cmd) {
                            case "PAY" -> sm.sendEvent(OrderEvent.PAY);
                            case "SHIP" -> sm.sendEvent(OrderEvent.SHIP);
                        }
                    }
                }
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                enum OrderState { NEW, PAID, SHIPPED }
                enum OrderEvent { PAY, SHIP }
                """;
        Files.writeString(tempDir.resolve("OrderListener.java"), source);
        Files.writeString(tempDir.resolve("Payload.java"), """
                package org.springframework.messaging.handler.annotation;
                import java.lang.annotation.Retention;
                import java.lang.annotation.RetentionPolicy;
                @Retention(RetentionPolicy.RUNTIME)
                public @interface Payload {}
                """);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderListener").getRoot());

        assertThat(triggers).hasSize(2);
        assertThat(triggers).allSatisfy(t -> assertThat(t.getSourceState())
                .as("@Payload String cmd switch must not invent sourceState")
                .isNull());
    }

    @Test
    void shouldNotTreatNonCarrierStringSwitchAsSourceState(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderListener {
                    private StateMachine sm;
                    public void onMessage(String cmd) {
                        switch (cmd) {
                            case "PAY" -> sm.sendEvent(OrderEvent.PAY);
                        }
                    }
                }
                class StateMachine {
                    public void sendEvent(OrderEvent e) {}
                }
                enum OrderEvent { PAY }
                """;
        Files.writeString(tempDir.resolve("OrderListener.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderListener").getRoot());

        assertThat(triggers).hasSize(1);
        assertThat(triggers.get(0).getSourceState())
                .as("String switch selector that is not a state enum must not become sourceState")
                .isNull();
    }

    @Test
    void shouldDetectSourceStateFromLiteralSendEventArgument(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void pay() {
                        sm.sendEvent(OrderEvent.PAY, OrderState.PENDING);
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e, OrderState s) {}
                }
                
                enum OrderState { PENDING }
                enum OrderEvent { PAY }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());

        assertThat(triggers).hasSize(1);
        assertThat(triggers.get(0).getSourceState()).isEqualTo("PENDING");
    }

    @Test
    void shouldNotGuessSourceStateFromVariableSendEventArgument(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    private StateMachine sm;
                    public void pay(OrderState current) {
                        sm.sendEvent(OrderEvent.PAY, current);
                    }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvent e, OrderState s) {}
                }
                
                enum OrderState { PENDING }
                enum OrderEvent { PAY }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);

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

        GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
        List<TriggerPoint> triggers = detector.detect(
                (org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());

        assertThat(triggers).hasSize(1);
        assertThat(triggers.get(0).getSourceState()).isNull();
    }
}
