package click.kamil.springstatemachineexporter.analysis.service;

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.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.service.ExportService;
import click.kamil.springstatemachineexporter.service.JsonImportService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

/**
 * Regression gate: after the full {@link ExportService} pipeline, enum-backed machines must not
 * contain mixed short-form ({@code OrderState.NEW}) and package-canonical
 * ({@code com.example.order.OrderState.NEW}) identifiers in the same export.
 */
class ExportPipelineIdentifierConsistencyTest {

    @Test
    void jsonReExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
        writeSampleProject(tempDir);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.PAY")
                .sourceState("OrderState.NEW")
                .className("com.example.web.OrderController")
                .methodName("pay")
                .sourceFile("OrderController.java")
                .polymorphicEvents(List.of("OrderEvent.PAY"))
                .build();
        CallChain chain = CallChain.builder().triggerPoint(trigger).build();

        AnalysisResult shortForm = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .states(Set.of(
                        State.of("OrderState.NEW", "OrderState.NEW"),
                        State.of("UNDEFINED", "com.example.order.OrderState.PAID")))
                .transitions(List.of(
                        shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID"),
                        multiSourceTransition("OrderEvent.PAY", List.of("OrderState.NEW", "OrderState.PAID"), "OrderState.PAID")))
                .startStates(Set.of("OrderState.NEW"))
                .endStates(Set.of("OrderState.PAID"))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(trigger))
                        .callChains(List.of(chain))
                        .properties(Map.of("default", Map.of(
                                "app.event", "OrderEvent.PAY",
                                "app.state", "OrderState.NEW")))
                        .build())
                .build();

        Path jsonFile = tempDir.resolve("machine.json");
        Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));

        Path outputDir = tempDir.resolve("out");
        ExportService exportService = new ExportService(List.of(new JsonExporter()));
        exportService.runJsonExporter(jsonFile, outputDir, List.of("json"), List.of("default"));

        AnalysisResult exported = new JsonImportService().importAnalysisResult(
                outputDir.resolve("com.example.config.OrderStateMachineConfiguration")
                        .resolve("com.example.config.OrderStateMachineConfiguration.json"));

        assertNoMixedEnumIdentifiers(exported);
        assertRawNameSync(exported);
        assertThat(exported.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNotEmpty();
    }

    static void assertRawNameSync(AnalysisResult result) {
        String stateTypeFqn = result.getStateTypeFqn();
        String eventTypeFqn = result.getEventTypeFqn();
        if (stateTypeFqn == null || !stateTypeFqn.contains(".")
                || MachineEnumCanonicalizer.isStringOrPrimitiveType(stateTypeFqn)) {
            return;
        }
        if (result.getStates() != null) {
            for (State state : result.getStates()) {
                assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
                        state.rawName(), state.fullIdentifier(), stateTypeFqn))
                        .as("state rawName sync for %s", state.fullIdentifier())
                        .isTrue();
            }
        }
        if (result.getTransitions() != null) {
            for (Transition transition : result.getTransitions()) {
                if (transition.getEvent() != null) {
                    assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
                            transition.getEvent().rawName(),
                            transition.getEvent().fullIdentifier(),
                            eventTypeFqn))
                            .as("event rawName sync for %s", transition.getEvent().fullIdentifier())
                            .isTrue();
                }
                assertStateListRawSync(transition.getSourceStates(), stateTypeFqn);
                assertStateListRawSync(transition.getTargetStates(), stateTypeFqn);
            }
        }
    }

    private static void assertStateListRawSync(List<State> states, String stateTypeFqn) {
        if (states == null) {
            return;
        }
        for (State state : states) {
            assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
                    state.rawName(), state.fullIdentifier(), stateTypeFqn))
                    .as("state rawName sync for %s", state.fullIdentifier())
                    .isTrue();
        }
    }

    private static void assertPolymorphicEventForms(AnalysisResult result) {
        if (result.getMetadata() == null) {
            return;
        }
        collectTriggers(result).forEach(trigger -> {
            if (trigger.getPolymorphicEvents() != null) {
                for (String poly : trigger.getPolymorphicEvents()) {
                    assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm(poly))
                            .as("polymorphic event form: %s", poly)
                            .isTrue();
                }
            }
        });
    }

    private static List<TriggerPoint> collectTriggers(AnalysisResult result) {
        List<TriggerPoint> triggers = new java.util.ArrayList<>();
        if (result.getMetadata().getTriggers() != null) {
            triggers.addAll(result.getMetadata().getTriggers());
        }
        if (result.getMetadata().getCallChains() != null) {
            for (CallChain chain : result.getMetadata().getCallChains()) {
                if (chain.getTriggerPoint() != null) {
                    triggers.add(chain.getTriggerPoint());
                }
            }
        }
        return triggers;
    }

    @Test
    void astExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
        Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
        Path outputDir = tempDir.resolve("enterprise-out");

        ExportService exportService = new ExportService(List.of(new JsonExporter()));
        exportService.runExporter(enterprise, outputDir, List.of("json"), true, List.of());

        ObjectMapper mapper = new ObjectMapper();
        try (var dirs = Files.list(outputDir)) {
            List<Path> machineDirs = dirs.filter(Files::isDirectory).toList();
            assertThat(machineDirs).isNotEmpty();
            for (Path machineDir : machineDirs) {
                Path json = machineDir.resolve(machineDir.getFileName() + ".json");
                JsonNode root = mapper.readTree(json.toFile());
                if (!root.hasNonNull("stateTypeFqn") || !root.get("stateTypeFqn").asText().contains(".")) {
                    continue;
                }
                AnalysisResult result = new JsonImportService().importAnalysisResult(json);
                assertNoMixedEnumIdentifiers(result);
                assertRawNameSync(result);
                assertTriggerEventForms(result);
                assertPolymorphicEventForms(result);
            }
        }
    }

    static void assertTriggerEventForms(AnalysisResult result) {
        if (result.getMetadata() == null) {
            return;
        }
        if (result.getMetadata().getTriggers() != null) {
            for (TriggerPoint trigger : result.getMetadata().getTriggers()) {
                assertTriggerEventForm(trigger.getEvent());
            }
        }
        if (result.getMetadata().getCallChains() != null) {
            for (CallChain chain : result.getMetadata().getCallChains()) {
                if (chain.getTriggerPoint() != null) {
                    assertTriggerEventForm(chain.getTriggerPoint().getEvent());
                }
            }
        }
    }

    private static void assertTriggerEventForm(String event) {
        if (event == null || event.isBlank()) {
            return;
        }
        MachineEnumCanonicalizer.TriggerEventKind kind = MachineEnumCanonicalizer.classifyTriggerEvent(event);
        assertThat(kind)
                .as("triggerPoint.event must be canonical enum, dynamic expression, symbolic, or lifecycle: %s", event)
                .isIn(
                        MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM,
                        MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION,
                        MachineEnumCanonicalizer.TriggerEventKind.SYMBOLIC,
                        MachineEnumCanonicalizer.TriggerEventKind.LIFECYCLE);
        assertThat(MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(event))
                .as("dynamic trigger event must not be package-prefixed: %s", event)
                .isFalse();
        if (kind == MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM) {
            assertThat(event).contains(".").doesNotContain("(");
        }
    }

    @Test
    void propertyResolutionShouldCanonicalizeCallChainTriggersBeforeRelink(@TempDir Path tempDir) throws Exception {
        writeSampleProject(tempDir);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("${app.event}")
                .sourceState("${app.state}")
                .className("com.example.web.OrderController")
                .methodName("pay")
                .sourceFile("OrderController.java")
                .polymorphicEvents(List.of("${app.event}"))
                .eventTypeFqn("com.example.order.OrderEvent")
                .stateTypeFqn("com.example.order.OrderState")
                .build();
        CallChain chain = CallChain.builder()
                .triggerPoint(trigger)
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .event("OrderEvent.PAY")
                        .sourceState("OrderState.NEW")
                        .targetState("OrderState.PAID")
                        .build()))
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID")))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(trigger))
                        .callChains(List.of(chain))
                        .properties(Map.of("default", Map.of(
                                "app.event", "OrderEvent.PAY",
                                "app.state", "OrderState.NEW")))
                        .build())
                .build();

        result.applyResolution(Map.of("app.event", "OrderEvent.PAY", "app.state", "OrderState.NEW"));

        TriggerPoint chainTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(chainTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(chainTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getSourceState())
                .isEqualTo("com.example.order.OrderState.NEW");
        assertNoMixedEnumIdentifiers(result);
    }

    static void assertNoMixedEnumIdentifiers(AnalysisResult result) {
        String stateTypeFqn = result.getStateTypeFqn();
        String eventTypeFqn = result.getEventTypeFqn();
        if (stateTypeFqn == null || !stateTypeFqn.contains(".")
                || MachineEnumCanonicalizer.isStringOrPrimitiveType(stateTypeFqn)) {
            return;
        }
        if (eventTypeFqn == null || !eventTypeFqn.contains(".")
                || MachineEnumCanonicalizer.isStringOrPrimitiveType(eventTypeFqn)) {
            return;
        }

        Set<String> stateIds = collectStateIdentifiers(result);
        Set<String> eventIds = collectEventIdentifiers(result);

        assertCanonicalSet(stateIds, stateTypeFqn, "state");
        assertCanonicalSet(eventIds, eventTypeFqn, "event");
        assertNoShortFormWhenPackageFormPresent(stateIds, stateTypeFqn);
        assertNoShortFormWhenPackageFormPresent(eventIds, eventTypeFqn);
    }

    private static void assertCanonicalSet(Set<String> identifiers, String enumTypeFqn, String label) {
        for (String id : identifiers) {
            if (id == null || id.isBlank() || id.startsWith("<") || id.startsWith("\"")) {
                continue;
            }
            if (!MachineEnumCanonicalizer.isMachineEnumReference(id, enumTypeFqn)) {
                continue;
            }
            assertThat(id)
                    .as("%s identifier must be package-canonical: %s", label, id)
                    .isEqualTo(MachineEnumCanonicalizer.canonicalizeLabel(id, enumTypeFqn));
        }
    }

    private static void assertNoShortFormWhenPackageFormPresent(Set<String> identifiers, String enumTypeFqn) {
        Set<String> packageForms = new HashSet<>();
        Set<String> shortForms = new HashSet<>();
        for (String id : identifiers) {
            if (id == null || !MachineEnumCanonicalizer.isMachineEnumReference(id, enumTypeFqn)) {
                continue;
            }
            if (id.startsWith(enumTypeFqn + ".")) {
                packageForms.add(id);
                String fn = fnForm(id);
                if (fn != null) {
                    shortForms.add(fn);
                }
            } else {
                shortForms.add(id);
            }
        }
        for (String pkg : packageForms) {
            String fn = fnForm(pkg);
            assertThat(identifiers)
                    .as("mixed short/package identifiers for %s", pkg)
                    .doesNotContain(fn);
        }
        assertThat(shortForms.stream().filter(id -> !id.startsWith(enumTypeFqn + ".")).toList())
                .as("unexpected bare short forms while package forms exist: %s vs %s", shortForms, packageForms)
                .isEmpty();
    }

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

    private static Set<String> collectStateIdentifiers(AnalysisResult result) {
        Set<String> ids = new LinkedHashSet<>();
        if (result.getStates() != null) {
            result.getStates().forEach(s -> addIfPresent(ids, s.fullIdentifier()));
        }
        if (result.getStartStates() != null) {
            ids.addAll(result.getStartStates());
        }
        if (result.getEndStates() != null) {
            ids.addAll(result.getEndStates());
        }
        if (result.getTransitions() != null) {
            for (Transition t : result.getTransitions()) {
                addStates(ids, t.getSourceStates());
                addStates(ids, t.getTargetStates());
            }
        }
        if (result.getMetadata() != null) {
            if (result.getMetadata().getTriggers() != null) {
                for (TriggerPoint trigger : result.getMetadata().getTriggers()) {
                    addIfPresent(ids, trigger.getSourceState());
                }
            }
            if (result.getMetadata().getCallChains() != null) {
                for (CallChain chain : result.getMetadata().getCallChains()) {
                    if (chain.getTriggerPoint() != null) {
                        addIfPresent(ids, chain.getTriggerPoint().getSourceState());
                    }
                    if (chain.getMatchedTransitions() != null) {
                        for (MatchedTransition mt : chain.getMatchedTransitions()) {
                            addIfPresent(ids, mt.getSourceState());
                            addIfPresent(ids, mt.getTargetState());
                        }
                    }
                }
            }
        }
        ids.removeIf(id -> id == null || id.isBlank());
        return ids;
    }

    private static Set<String> collectEventIdentifiers(AnalysisResult result) {
        Set<String> ids = new LinkedHashSet<>();
        if (result.getTransitions() != null) {
            for (Transition t : result.getTransitions()) {
                if (t.getEvent() != null) {
                    addIfPresent(ids, t.getEvent().fullIdentifier());
                }
            }
        }
        if (result.getMetadata() != null) {
            if (result.getMetadata().getTriggers() != null) {
                for (TriggerPoint trigger : result.getMetadata().getTriggers()) {
                    addIfPresent(ids, trigger.getEvent());
                    if (trigger.getPolymorphicEvents() != null) {
                        ids.addAll(trigger.getPolymorphicEvents());
                    }
                }
            }
            if (result.getMetadata().getCallChains() != null) {
                for (CallChain chain : result.getMetadata().getCallChains()) {
                    if (chain.getTriggerPoint() != null) {
                        addIfPresent(ids, chain.getTriggerPoint().getEvent());
                        if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
                            ids.addAll(chain.getTriggerPoint().getPolymorphicEvents());
                        }
                    }
                    if (chain.getMatchedTransitions() != null) {
                        for (MatchedTransition mt : chain.getMatchedTransitions()) {
                            addIfPresent(ids, mt.getEvent());
                        }
                    }
                }
            }
        }
        ids.removeIf(id -> id == null || id.isBlank());
        return ids;
    }

    private static void addStates(Set<String> ids, List<State> states) {
        if (states == null) {
            return;
        }
        for (State state : states) {
            addIfPresent(ids, state.fullIdentifier());
        }
    }

    private static void addIfPresent(Set<String> ids, String value) {
        if (value != null && !value.isBlank()) {
            ids.add(value);
        }
    }

    private static Transition shortTransition(String event, String source, String target) {
        Transition transition = new Transition();
        transition.setEvent(Event.of(event, event));
        transition.setSourceStates(List.of(State.of(source, source)));
        transition.setTargetStates(List.of(State.of(target, target)));
        return transition;
    }

    private static Transition multiSourceTransition(String event, List<String> sources, String target) {
        Transition transition = new Transition();
        transition.setEvent(Event.of(event, event));
        transition.setSourceStates(sources.stream().map(s -> State.of(s, s)).toList());
        transition.setTargetStates(List.of(State.of(target, target)));
        return transition;
    }

    private static void writeSampleProject(Path projectRoot) throws Exception {
        Path javaRoot = projectRoot.resolve("src/main/java");
        Path orderPkg = javaRoot.resolve("com/example/order");
        Path configPkg = javaRoot.resolve("com/example/config");
        Files.createDirectories(orderPkg);
        Files.createDirectories(configPkg);
        Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");
        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> {
                }
                """);
    }

    private static Path findProjectRoot() {
        Path current = Path.of(".").toAbsolutePath();
        while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
            current = current.getParent();
        }
        return current;
    }
}
