package click.kamil.springstatemachineexporter.analysis.resolver;

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;

class MachineEnumCanonicalizerTest {

    @Test
    void shouldResolveMachineEnumTypesFromConfigurerAdapter(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        assertThat(types.stateTypeFqn()).isEqualTo("com.example.order.OrderState");
        assertThat(types.eventTypeFqn()).isEqualTo("com.example.order.OrderEvent");
    }

    @Test
    void shouldCanonicalizeTransitionIdentifiersUsingMachineTypes(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        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")));

        MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types);

        assertThat(transition.getEvent().fullIdentifier())
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(transition.getEvent().rawName()).isEqualTo("OrderEvent.PAY");
        assertThat(transition.getSourceStates().get(0).fullIdentifier())
                .isEqualTo("com.example.order.OrderState.NEW");
        assertThat(transition.getSourceStates().get(0).rawName()).isEqualTo("OrderState.NEW");
        assertThat(transition.getTargetStates().get(0).fullIdentifier())
                .isEqualTo("com.example.order.OrderState.PAID");
        assertThat(transition.getTargetStates().get(0).rawName()).isEqualTo("OrderState.PAID");
    }

    @Test
    void shouldAlignBareRawNameToFnForm(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

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

        MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types);

        assertThat(transition.getEvent().rawName()).isEqualTo("OrderEvent.PAY");
        assertThat(transition.getSourceStates().get(0).rawName()).isEqualTo("OrderState.NEW");
    }

    @Test
    void shouldCanonicalizeBareConstantNames(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        assertThat(MachineEnumCanonicalizer.canonicalizeLabel("PAY", types.eventTypeFqn()))
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(MachineEnumCanonicalizer.canonicalizeStateLabels(Set.of("NEW"), types.stateTypeFqn()))
                .containsExactly("com.example.order.OrderState.NEW");
    }

    @Test
    void shouldLeaveStringStatesUntouched(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        assertThat(MachineEnumCanonicalizer.canonicalizeLabel("\"DRAFT\"", types.stateTypeFqn()))
                .isEqualTo("DRAFT");
    }

    @Test
    void shouldCanonicalizePolymorphicEventsUsingMachineEventType(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.PAY")
                .className("com.example.web.Controller")
                .methodName("pay")
                .sourceFile("Controller.java")
                .polymorphicEvents(List.of("OrderEvent.PAY", "PAY"))
                .eventTypeFqn("OrderEvent")
                .build();

        TriggerPoint canonical = MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, types);

        assertThat(canonical.getPolymorphicEvents()).containsExactly(
                "com.example.order.OrderEvent.PAY",
                "com.example.order.OrderEvent.PAY");
        assertThat(canonical.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldQualifyEventIdentifierToPackageFqn() {
        String eventTypeFqn = "com.example.order.OrderEvent";

        assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("PAY", eventTypeFqn))
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("OrderEvent.PAY", eventTypeFqn))
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("getType()", eventTypeFqn))
                .isEqualTo("getType()");
    }

    @Test
    void shouldSyncFnFormRawNameWhenFullIdentifierAlreadyCanonical(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        State mismatched = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
        State canonical = MachineEnumCanonicalizer.canonicalizeStates(Set.of(mismatched), types.stateTypeFqn())
                .iterator().next();

        assertThat(canonical.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
        assertThat(canonical.rawName()).isEqualTo("OrderState.NEW");
    }

    @Test
    void shouldNotPackageCanonicalizeDynamicValueOfTriggerExpression() {
        String dynamic = "MyEvent.valueOf(event)";
        assertThat(MachineEnumCanonicalizer.classifyTriggerEvent(dynamic))
                .isEqualTo(MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION);
        assertThat(MachineEnumCanonicalizer.canonicalizeLabel(
                dynamic, "com.abcd.aaa.MyEvent"))
                .isEqualTo("MyEvent.valueOf(event)");
    }

    @Test
    void shouldNotCanonicalizeTrailingDotEnumRefs() {
        String truncated = "com.example.order.OrderEvent.";
        assertThat(MachineEnumCanonicalizer.canonicalizeLabel(
                truncated, "com.example.order.OrderEvent"))
                .isEqualTo(truncated);
    }

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

        assertThat(MachineEnumCanonicalizer.canonicalizeLabel(
                "com.example.order.PAY", "com.example.order.OrderEvent", context))
                .isEqualTo("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldDenormalizeCorruptedPackageValueOfTriggerExpression() {
        String corrupted = "com.abcd.aaa.MyEvent.valueOf(event)";
        assertThat(MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(corrupted))
                .isEqualTo("MyEvent.valueOf(event)");
        assertThat(MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(corrupted)).isTrue();
    }

    @Test
    void shouldClassifyConcreteEnumTriggerEvents() {
        assertThat(MachineEnumCanonicalizer.classifyTriggerEvent("com.example.order.OrderEvent.PAY"))
                .isEqualTo(MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM);
        assertThat(MachineEnumCanonicalizer.classifyTriggerEvent("event.getType()"))
                .isEqualTo(MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION);
        assertThat(MachineEnumCanonicalizer.classifyTriggerEvent("<SYMBOLIC: OrderEvent.*>"))
                .isEqualTo(MachineEnumCanonicalizer.TriggerEventKind.SYMBOLIC);
    }

    @Test
    void shouldExpandSymbolicPolymorphicEventsForMatchingMachineType(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        List<String> expanded = MachineEnumCanonicalizer.expandSymbolicPolymorphicEvents(
                List.of("<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>"),
                "com.example.order.OrderEvent",
                context);

        assertThat(expanded).containsExactly(
                "com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldNotExpandSymbolicPlaceholderWhenSimpleEnumNameIsAmbiguous(@TempDir Path tempDir) throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY, SHIP }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");

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

        List<String> expanded = MachineEnumCanonicalizer.expandSymbolicPolymorphicEvents(
                List.of("<SYMBOLIC: OrderEvent.*>"),
                "a.OrderEvent",
                context);

        assertThat(expanded).isEmpty();
    }

    @Test
    void shouldNotInferMachineTransitionsForAmbiguousTriggerWithoutConstraint(@TempDir Path tempDir)
            throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY, SHIP }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");

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

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.valueOf(eventStr)")
                .ambiguous(true)
                .polymorphicEvents(List.of())
                .build();
        StateMachineTypeResolver.MachineTypes types =
                new StateMachineTypeResolver.MachineTypes(null, "a.OrderEvent");

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

        TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, context, List.of(pay), true);

        assertThat(enriched.getPolymorphicEvents()).isNullOrEmpty();
    }

    @Test
    void shouldNotCapForeignPackageEnumConstantToMachineTransition() {
        List<String> capped = MachineEnumCanonicalizer.capToConfiguredTransitionEvents(
                List.of("b.OrderEvent.PAY"),
                List.of("a.OrderEvent.PAY"),
                "a.OrderEvent");

        assertThat(capped).isEmpty();
    }

    @Test
    void shouldValidateRawNameConsistencyForCanonicalHelpers() {
        assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
                "OrderEvent.PAY", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
                .isTrue();
        assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
                "UNDEFINED", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
                .isFalse();
        assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("com.example.order.OrderEvent.PAY"))
                .isTrue();
        assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("<SYMBOLIC: OrderEvent.*>"))
                .isTrue();
        assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("OrderEvent.valueOf(event)"))
                .isFalse();
        assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
                List.of("com.example.order.OrderEvent.PAY")))
                .isTrue();
        assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
                List.of("<SYMBOLIC: OrderEvent.*>")))
                .isFalse();
    }

    @Test
    void shouldRemapMismatchedEnumPrefixToMachineEventType(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        assertThat(MachineEnumCanonicalizer.canonicalizeLabel("OrderEvents.PAY", types.eventTypeFqn()))
                .isEqualTo("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldCapFullEnumPolymorphicListToConfiguredTransitionsOnly() {
        Transition pay = new Transition();
        pay.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
        Transition ship = new Transition();
        ship.setEvent(Event.of("OrderEvent.SHIP", "com.example.order.OrderEvent.SHIP"));

        List<String> fullEnum = List.of(
                "com.example.order.OrderEvent.PAY",
                "com.example.order.OrderEvent.SHIP",
                "com.example.order.OrderEvent.CANCEL",
                "com.example.order.OrderEvent.LOG");

        TriggerPoint trigger = TriggerPoint.builder()
                .event("eventType")
                .polymorphicEvents(fullEnum)
                .external(true)
                .build();

        StateMachineTypeResolver.MachineTypes types = new StateMachineTypeResolver.MachineTypes(
                "com.example.order.OrderState", "com.example.order.OrderEvent");

        TriggerPoint linked = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, null, List.of(pay, ship));

        assertThat(linked.getPolymorphicEvents())
                .containsExactlyInAnyOrder(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderEvent.SHIP");
    }

    @Test
    void shouldExpandSymbolicPolymorphicEventsToConfiguredTransitionsWhenAvailable() {
        Transition pay = new Transition();
        pay.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));

        List<String> expanded = MachineEnumCanonicalizer.expandSymbolicPolymorphicEvents(
                List.of("<SYMBOLIC: com.example.order.OrderEvent.*>"),
                "com.example.order.OrderEvent",
                null,
                List.of(pay));

        assertThat(expanded).containsExactly("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldWidenUnresolvedValueOfToConfiguredTransitionEvents() {
        Transition pay = new Transition();
        pay.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
        Transition ship = new Transition();
        ship.setEvent(Event.of("OrderEvent.SHIP", "com.example.order.OrderEvent.SHIP"));

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.valueOf(eventString)")
                .build();

        StateMachineTypeResolver.MachineTypes types = new StateMachineTypeResolver.MachineTypes(
                "com.example.order.OrderState", "com.example.order.OrderEvent");

        TriggerPoint linked = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, null, List.of(pay, ship));

        assertThat(linked.getPolymorphicEvents())
                .containsExactly(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderEvent.SHIP");
        assertThat(linked.isAmbiguous()).isTrue();
    }

    @Test
    void shouldInferSingleTransitionEventForValueOfWhenOnlyOneConfigured() {
        Transition pay = new Transition();
        pay.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.valueOf(eventString)")
                .build();

        StateMachineTypeResolver.MachineTypes types = new StateMachineTypeResolver.MachineTypes(
                "com.example.order.OrderState", "com.example.order.OrderEvent");

        TriggerPoint linked = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, null, List.of(pay));

        assertThat(linked.getPolymorphicEvents())
                .containsExactly("com.example.order.OrderEvent.PAY");
        assertThat(linked.isAmbiguous()).isFalse();
    }

    @Test
    void shouldNotExpandSymbolicToMultipleConfiguredTransitionEvents() {
        Transition pay = new Transition();
        pay.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
        Transition ship = new Transition();
        ship.setEvent(Event.of("OrderEvent.SHIP", "com.example.order.OrderEvent.SHIP"));

        List<String> expanded = MachineEnumCanonicalizer.expandSymbolicPolymorphicEvents(
                List.of("<SYMBOLIC: com.example.order.OrderEvent.*>"),
                "com.example.order.OrderEvent",
                null,
                List.of(pay, ship));

        assertThat(expanded).isEmpty();
    }

    @Test
    void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
        writeSampleConfig(tempDir);

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

        StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
                "com.example.config.OrderStateMachineConfiguration", context);

        State shortForm = State.of("OrderState.NEW", "OrderState.NEW");
        State packageForm = State.of("UNDEFINED", "com.example.order.OrderState.NEW");

        Set<State> deduped = MachineEnumCanonicalizer.canonicalizeStates(
                Set.of(shortForm, packageForm), types.stateTypeFqn());

        assertThat(deduped).hasSize(1);
        State only = deduped.iterator().next();
        assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
        assertThat(only.rawName()).isEqualTo("OrderState.NEW");
    }

    @Test
    void shouldNotThrowWhenStringMachineStateHasTypePrefixOnly() {
        String stateTypeFqn = "com.company.project.OrderState";
        State state = State.of("PAID", stateTypeFqn + ".");

        State canonical = MachineEnumCanonicalizer.canonicalizeState(state, "String");

        assertThat(canonical).isNotNull();
        assertThat(canonical.fullIdentifier()).isEqualTo(stateTypeFqn + ".");
    }

    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> {
                }
                """);
    }
}
