package click.kamil.springstatemachineexporter.analysis.validation;

import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;

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

class AnalysisCanonicalFormValidatorTest {

    @Test
    void shouldPassWhenAllEnumIdentifiersArePackageCanonical(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("com.example.order.OrderEvent.PAY")
                                .className("com.example.web.OrderController")
                                .methodName("pay")
                                .sourceFile("OrderController.java")
                                .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                                .eventTypeFqn("com.example.order.OrderEvent")
                                .build()))
                        .callChains(List.of(CallChain.builder()
                                .triggerPoint(TriggerPoint.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .className("com.example.web.OrderController")
                                        .methodName("pay")
                                        .sourceFile("OrderController.java")
                                        .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                                        .eventTypeFqn("com.example.order.OrderEvent")
                                        .build())
                                .matchedTransitions(List.of(MatchedTransition.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .sourceState("com.example.order.OrderState.NEW")
                                        .targetState("com.example.order.OrderState.PAID")
                                        .build()))
                                .build()))
                        .build())
                .build();

        assertThat(AnalysisCanonicalFormValidator.validate(result, context)).isEmpty();
    }

    @Test
    void shouldFailWhenTransitionFullIdentifiersUseShortEnumForm(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

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

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .transitions(List.of(transition))
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validate(result, context);

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("transitions[0].event.fullIdentifier");
        assertThatThrownBy(() -> AnalysisCanonicalFormValidator.enforce(result, context))
                .isInstanceOf(AnalysisCanonicalFormException.class);
    }

    @Test
    void shouldFailWhenMatchedTransitionStatesUseShortIdentifiers(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder()
                                .matchedTransitions(List.of(MatchedTransition.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .sourceState("OrderState.NEW")
                                        .targetState("OrderState.PAID")
                                        .build()))
                                .build()))
                        .build())
                .build();

        assertThat(AnalysisCanonicalFormValidator.validate(result, context))
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .anyMatch(path -> path.contains("matchedTransitions[0].sourceState"));
    }

    @Test
    void shouldFailWhenTriggerEventHasCorruptedPackageValueOfForm() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("com.example.order.OrderEvent.valueOf(eventString)")
                                .className("com.example.web.OrderController")
                                .methodName("dispatch")
                                .sourceFile("OrderController.java")
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.triggers[0].event");
    }

    @Test
    void shouldAllowDynamicDispatcherExpressionWithoutPackagePrefix() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("OrderEvent.valueOf(eventString.toUpperCase())")
                                .className("com.example.web.OrderController")
                                .methodName("dispatch")
                                .sourceFile("OrderController.java")
                                .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                                .build()))
                        .build())
                .build();

        assertThat(AnalysisCanonicalFormValidator.validateWithMachineTypes(
                result,
                new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                        "com.example.order.OrderState", "com.example.order.OrderEvent")))
                .noneMatch(v -> v.path().contains(".event"));
    }

    @Test
    void shouldFailWhenEventRawNameDoesNotMatchFnForm() {
        Transition transition = new Transition();
        transition.setEvent(Event.of("UNDEFINED", "com.example.order.OrderEvent.PAY"));
        transition.setSourceStates(List.of(
                State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(transition))
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("transitions[0].event.rawName");
    }

    @Test
    void shouldFailWhenPolymorphicEventIsDynamicExpression() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("OrderEvent.valueOf(eventString)")
                                .className("com.example.web.OrderController")
                                .methodName("dispatch")
                                .sourceFile("OrderController.java")
                                .polymorphicEvents(List.of("OrderEvent.valueOf(eventString)"))
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.triggers[0].polymorphicEvents[0]");
    }

    @Test
    void shouldFailWhenSourceStateHasCorruptedPackageValueOfForm() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("com.example.order.OrderEvent.PAY")
                                .sourceState("com.example.order.OrderState.valueOf(stateString)")
                                .className("com.example.web.OrderController")
                                .methodName("dispatch")
                                .sourceFile("OrderController.java")
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.triggers[0].sourceState");
    }

    @Test
    void shouldFailWhenDynamicTriggerHasConcretePolymorphicEventsButNoMatchedTransitions() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder()
                                .triggerPoint(TriggerPoint.builder()
                                        .event("event.getType()")
                                        .className("com.example.web.OrderController")
                                        .methodName("dispatch")
                                        .sourceFile("OrderController.java")
                                        .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                                        .build())
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.callChains[0].matchedTransitions");
    }

    @Test
    void shouldFailWhenExternalAmbiguousTriggerHasResolvablePolymorphicEventsButNoMatchedTransitions() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder()
                                .triggerPoint(TriggerPoint.builder()
                                        .event("eventType")
                                        .className("com.example.web.OrderController")
                                        .methodName("dispatch")
                                        .sourceFile("OrderController.java")
                                        .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                                        .external(true)
                                        .ambiguous(true)
                                        .build())
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.callChains[0].matchedTransitions");
    }

    @Test
    void shouldFailWhenDedicatedEndpointHasConcreteLiteralEventButNoMatchedTransitions() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder()
                                .entryPoint(click.kamil.springstatemachineexporter.analysis.model.EntryPoint.builder()
                                        .type(click.kamil.springstatemachineexporter.analysis.model.EntryPoint.Type.REST)
                                        .name("POST /api/machine/order/pay")
                                        .className("com.example.web.StateMachineController")
                                        .methodName("payOrder")
                                        .build())
                                .triggerPoint(TriggerPoint.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .className("com.example.web.StateMachineDispatcher")
                                        .methodName("payOrder")
                                        .sourceFile("StateMachineDispatcher.java")
                                        .eventTypeFqn("com.example.order.OrderEvent")
                                        .build())
                                .build()))
                        .build())
                .build();

        List<AnalysisCanonicalFormValidator.Violation> violations =
                AnalysisCanonicalFormValidator.validateWithMachineTypes(
                        result,
                        new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                                "com.example.order.OrderState", "com.example.order.OrderEvent"));

        assertThat(violations)
                .extracting(AnalysisCanonicalFormValidator.Violation::path)
                .contains("metadata.callChains[0].matchedTransitions");
    }

    @Test
    void shouldAllowSymbolicPolymorphicEventsWithoutMatchedTransitions() {
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(transition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder()
                                .triggerPoint(TriggerPoint.builder()
                                        .event("event.getType()")
                                        .className("com.example.web.OrderController")
                                        .methodName("dispatch")
                                        .sourceFile("OrderController.java")
                                        .polymorphicEvents(List.of("<SYMBOLIC: OrderEvent.*>"))
                                        .build())
                                .build()))
                        .build())
                .build();

        assertThat(AnalysisCanonicalFormValidator.validateWithMachineTypes(
                result,
                new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
                        "com.example.order.OrderState", "com.example.order.OrderEvent")))
                .noneMatch(v -> v.path().contains("matchedTransitions"));
    }

    @Test
    void shouldSkipImportStyleCanonicalCheckWhenSimpleNameIsAmbiguous(@TempDir Path tempDir) throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.createDirectories(tempDir.resolve("com/example/config"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"), "package a; public enum OrderEvent { PAY }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"), "package b; public enum OrderEvent { CANCEL }");
        Files.writeString(tempDir.resolve("a/OrderState.java"), "package a; public enum OrderState { NEW, PAID }");
        Files.writeString(tempDir.resolve("com/example/config/OrderStateMachineConfiguration.java"),
                """
                package com.example.config;
                import a.OrderEvent;
                import a.OrderState;
                import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
                public class OrderStateMachineConfiguration
                        extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
                }
                """);

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

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .transitions(List.of(transition(
                        "a.OrderEvent.PAY",
                        "a.OrderState.NEW",
                        "a.OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("OrderEvent.PAY")
                                .className("com.example.web.Controller")
                                .methodName("pay")
                                .sourceFile("Controller.java")
                                .build()))
                        .build())
                .build();

        assertThat(AnalysisCanonicalFormValidator.validate(result, context)).isEmpty();
    }

    @Test
    void shouldSkipValidationForStringStateMachines(@TempDir Path tempDir) throws IOException {
        Path configPkg = tempDir.resolve("com/example/config");
        Files.createDirectories(configPkg);
        Files.writeString(configPkg.resolve("StringStateMachineConfiguration.java"),
                """
                package com.example.config;
                import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
                public class StringStateMachineConfiguration extends StateMachineConfigurerAdapter<String, String> {
                }
                """);

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

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.StringStateMachineConfiguration")
                .transitions(List.of(transition("PAY", "NEW", "PAID")))
                .build();

        assertThat(AnalysisCanonicalFormValidator.validate(result, context)).isEmpty();
    }

    private static Transition transition(String eventFqn, String sourceFqn, String targetFqn) {
        String eventRaw = fnForm(eventFqn);
        String sourceRaw = fnForm(sourceFqn);
        String targetRaw = fnForm(targetFqn);
        Transition transition = new Transition();
        transition.setEvent(Event.of(eventRaw, eventFqn));
        transition.setSourceStates(List.of(State.of(sourceRaw, sourceFqn)));
        transition.setTargetStates(List.of(State.of(targetRaw, targetFqn)));
        return transition;
    }

    private static String fnForm(String identifier) {
        if (identifier == null || !identifier.contains(".")) {
            return identifier;
        }
        int lastDot = identifier.lastIndexOf('.');
        int prevDot = identifier.lastIndexOf('.', lastDot - 1);
        return prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
    }

    private static void writeSampleConfig(Path tempDir) throws IOException {
        Path orderPkg = tempDir.resolve("com/example/order");
        Path configPkg = tempDir.resolve("com/example/config");
        Files.createDirectories(orderPkg);
        Files.createDirectories(configPkg);

        Files.writeString(orderPkg.resolve("OrderState.java"),
                """
                package com.example.order;
                public enum OrderState { NEW, PAID }
                """);
        Files.writeString(orderPkg.resolve("OrderEvent.java"),
                """
                package com.example.order;
                public enum OrderEvent { PAY }
                """);
        Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
                """
                package com.example.config;
                import com.example.order.OrderEvent;
                import com.example.order.OrderState;
                import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
                public class OrderStateMachineConfiguration
                        extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
                }
                """);
    }
}
