package click.kamil.springstatemachineexporter.html;

import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.FlowStep;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.html.exporter.HtmlExporter;
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 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 java.util.Set;

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

/**
 * End-to-end HTML link-key consistency: SVG {@code #link_*} anchors must align with
 * {@code matchedTransitions} produced by the finalized export pipeline.
 */
class HtmlTransitionLinkConsistencyTest {

    @Test
    void jsonReExportHtmlShouldContainLinkKeysForEveryMatchedTransition(@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")
                .transitions(List.of(shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID")))
                .startStates(Set.of("OrderState.NEW"))
                .endStates(Set.of("OrderState.PAID"))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(trigger))
                        .callChains(List.of(chain))
                        .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 HtmlExporter(), new JsonExporter()));
        exportService.runJsonExporter(jsonFile, outputDir, List.of("html", "json"));

        Path machineDir = outputDir.resolve("com.example.config.OrderStateMachineConfiguration");
        Path htmlFile = machineDir.resolve("com.example.config.OrderStateMachineConfiguration.html");
        Path finalizedJson = machineDir.resolve("com.example.config.OrderStateMachineConfiguration.json");
        assertThat(htmlFile).exists();

        String html = Files.readString(htmlFile);
        AnalysisResult finalized = new JsonImportService().importAnalysisResult(finalizedJson);

        List<MatchedTransition> matched = finalized.getMetadata().getCallChains().stream()
                .flatMap(c -> c.getMatchedTransitions() == null ? java.util.stream.Stream.empty()
                        : c.getMatchedTransitions().stream())
                .toList();
        assertThat(matched).isNotEmpty();

        for (MatchedTransition mt : matched) {
            String linkKey = buildLinkKey(mt.getSourceState(), mt.getEvent());
            assertThat(html)
                    .as("HTML must contain SVG link for matched transition %s -> %s", mt.getSourceState(), mt.getEvent())
                    .contains("#link_" + linkKey);
        }

        assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
        assertThat(html).contains("com.example.order.OrderEvent.PAY");
    }

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

        AnalysisResult finalizedShape = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(shortTransition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.PAID"))
                .build();

        String html = new HtmlExporter().export(finalizedShape, ExportOptions.builder().build());
        String svg = extractSvg(html);

        assertThat(svg).contains("OrderState.NEW");
        assertThat(svg).contains("OrderState.PAID");
        assertThat(svg).doesNotContain("com.example.order.OrderState.NEW");
        assertThat(svg).doesNotContain("com.example.order.OrderState.PAID");
    }

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

        AnalysisResult finalizedShape = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(
                        shortTransition(
                                "com.example.order.OrderEvent.PAY",
                                "com.example.order.OrderState.NEW",
                                "com.example.order.OrderState.PAID"),
                        shortTransition(
                                "com.example.order.OrderEvent.SHIP",
                                "com.example.order.OrderState.PAID",
                                "com.example.order.OrderState.SHIPPED")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.SHIPPED"))
                .flows(List.of(BusinessFlow.builder()
                        .name("Ship only")
                        .description("Should not light PAY")
                        .steps(List.of(FlowStep.ofEvent("SHIP")))
                        .build()))
                .build();

        String html = new HtmlExporter().export(finalizedShape, ExportOptions.builder().build());
        assertThat(html).contains("function highlightEvents(steps)");
        assertThat(html).contains("Event-only string steps without source are not precise enough");
        assertThat(html).doesNotContain("match that event from any source");
    }

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

        AnalysisResult finalizedShape = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(
                        shortTransition(
                                "com.example.order.OrderEvent.PAY",
                                "com.example.order.OrderState.NEW",
                                "com.example.order.OrderState.PAID"),
                        shortTransition(
                                "com.example.order.OrderEvent.SHIP",
                                "com.example.order.OrderState.PAID",
                                "com.example.order.OrderState.SHIPPED")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.SHIPPED"))
                .flows(List.of(BusinessFlow.builder()
                        .name("Ship from paid")
                        .description("Highlights SHIP only")
                        .steps(List.of(FlowStep.builder()
                                .source("com.example.order.OrderState.PAID")
                                .event("com.example.order.OrderEvent.SHIP")
                                .build()))
                        .build()))
                .build();

        String html = new HtmlExporter().export(finalizedShape, ExportOptions.builder().build());

        assertThat(html).contains("#link_OrderState_PAID__OrderEvent_SHIP");
        assertThat(html).contains("\"linkKey\" : \"OrderState_PAID__OrderEvent_SHIP\"");
        assertThat(html).contains("\"source\" : \"com.example.order.OrderState.PAID\"");
        assertThat(html).contains("\"event\" : \"com.example.order.OrderEvent.SHIP\"");
    }

    @Test
    void layeredDispatcherHtmlShouldContainLinkKeysForEveryMatchedTransition(@TempDir Path tempDir) throws Exception {
        Path sampleRoot = findProjectRoot().resolve("state_machines/layered_dispatcher_sample");
        ExportService exportService = new ExportService(List.of(new HtmlExporter(), new JsonExporter()));
        exportService.runExporter(sampleRoot, tempDir, List.of("html", "json"), true, List.of(), null, null,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);

        Path machineDir = tempDir.resolve(
                "click.kamil.examples.statemachine.layered.order.config.StandardOrderStateMachineConfiguration");
        Path htmlFile = machineDir.resolve(
                "click.kamil.examples.statemachine.layered.order.config.StandardOrderStateMachineConfiguration.html");
        Path finalizedJson = machineDir.resolve(
                "click.kamil.examples.statemachine.layered.order.config.StandardOrderStateMachineConfiguration.json");
        assertThat(htmlFile).exists();

        String html = Files.readString(htmlFile);
        AnalysisResult finalized = new JsonImportService().importAnalysisResult(finalizedJson);

        List<MatchedTransition> matched = finalized.getMetadata().getCallChains().stream()
                .filter(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .flatMap(c -> c.getMatchedTransitions() == null ? java.util.stream.Stream.empty()
                        : c.getMatchedTransitions().stream())
                .toList();
        assertThat(matched).isNotEmpty();

        for (MatchedTransition mt : matched) {
            String linkKey = buildLinkKey(mt.getSourceState(), mt.getEvent());
            assertThat(html)
                    .as("HTML must contain SVG link for matched transition %s -> %s",
                            mt.getSourceState(), mt.getEvent())
                    .contains("#link_" + linkKey);
        }

        assertThat(html).contains("linkEventFormat = 'fn'");
        assertThat(html).contains("OrderState_NEW__OrderTransitionEvent_PAY");
    }

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

        AnalysisResult finalizedShape = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(shortTransition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.PAID"))
                .metadata(CodebaseMetadata.builder()
                        .entryPoints(List.of(EntryPoint.builder()
                                .type(EntryPoint.Type.REST)
                                .name("POST /orders/pay")
                                .className("com.example.web.OrderController")
                                .methodName("pay")
                                .build()))
                        .callChains(List.of(CallChain.builder()
                                .entryPoint(EntryPoint.builder()
                                        .type(EntryPoint.Type.REST)
                                        .name("POST /orders/pay")
                                        .className("com.example.web.OrderController")
                                        .methodName("pay")
                                        .build())
                                .linkResolution(LinkResolution.RESOLVED)
                                .triggerPoint(TriggerPoint.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .className("com.example.web.OrderController")
                                        .methodName("pay")
                                        .build())
                                .matchedTransitions(List.of(MatchedTransition.builder()
                                        .sourceState("com.example.order.OrderState.NEW")
                                        .targetState("com.example.order.OrderState.PAID")
                                        .event("com.example.order.OrderEvent.PAY")
                                        .build()))
                                .build()))
                        .build())
                .build();

        ExportOptions fqnOptions = ExportOptions.builder()
                .includeMetadataPane(true)
                .eventFormat(click.kamil.springstatemachineexporter.exporter.EnumFormat.fqn)
                .stateFormat(click.kamil.springstatemachineexporter.exporter.EnumFormat.fqn)
                .build();
        String html = new HtmlExporter().export(finalizedShape, fqnOptions);

        String linkKey = buildLinkKey(
                "com.example.order.OrderState.NEW",
                "com.example.order.OrderEvent.PAY",
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fqn,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fqn);
        assertThat(html).contains("linkEventFormat = 'fqn'");
        assertThat(html).contains("#link_" + linkKey);
    }

    @Test
    void enterpriseHtmlShouldContainMatchedTransitionLinkKeys(@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 HtmlExporter()));
        exportService.runExporter(enterprise, outputDir, List.of("html"), true, List.of(), null,
                "OrderStateMachineConfiguration");

        Path machineDir = outputDir.resolve("click.kamil.enterprise.machines.order.OrderStateMachineConfiguration");
        Path htmlFile = machineDir.resolve("click.kamil.enterprise.machines.order.OrderStateMachineConfiguration.html");
        assertThat(htmlFile).exists();

        String html = Files.readString(htmlFile);
        assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
        assertThat(html).contains("function buildLinkKey(source, event)");
        assertThat(html).contains("\"matchedTransitions\"");
    }

    @Test
    void htmlShouldRespectLinkResolutionInSidebarAndHighlighting() {
        EntryPoint resolvedEp = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /orders/pay")
                .className("com.example.web.OrderController")
                .methodName("pay")
                .build();
        EntryPoint unresolvedEp = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/transition/{event}")
                .className("com.example.web.DispatcherController")
                .methodName("dispatch")
                .build();
        EntryPoint noMatchEp = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /orders/suspend")
                .className("com.example.web.OrderController")
                .methodName("suspend")
                .build();
        EntryPoint ambiguousEp = EntryPoint.builder()
                .type(EntryPoint.Type.CUSTOM)
                .name("ambiguous")
                .className("com.example.web.AmbiguousController")
                .methodName("trigger")
                .build();

        CallChain resolvedChain = CallChain.builder()
                .entryPoint(resolvedEp)
                .linkResolution(LinkResolution.RESOLVED)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.PAY")
                        .className("com.example.web.OrderController")
                        .methodName("pay")
                        .build())
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .sourceState("com.example.order.OrderState.NEW")
                        .targetState("com.example.order.OrderState.PAID")
                        .event("com.example.order.OrderEvent.PAY")
                        .build()))
                .build();
        CallChain unresolvedChain = CallChain.builder()
                .entryPoint(unresolvedEp)
                .linkResolution(LinkResolution.UNRESOLVED_EXTERNAL)
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvent.valueOf(eventString.toUpperCase())")
                        .className("com.example.web.DispatcherController")
                        .methodName("dispatch")
                        .external(true)
                        .build())
                .build();
        CallChain noMatchChain = CallChain.builder()
                .entryPoint(noMatchEp)
                .linkResolution(LinkResolution.NO_MATCH)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.SUSPEND")
                        .className("com.example.web.OrderController")
                        .methodName("suspend")
                        .build())
                .build();
        CallChain ambiguousChain = CallChain.builder()
                .entryPoint(ambiguousEp)
                .linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.PAY")
                        .ambiguous(true)
                        .build())
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .sourceState("com.example.order.OrderState.NEW")
                        .targetState("com.example.order.OrderState.PAID")
                        .event("com.example.order.OrderEvent.PAY")
                        .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(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.PAID"))
                .metadata(CodebaseMetadata.builder()
                        .entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp, ambiguousEp))
                        .callChains(List.of(resolvedChain, unresolvedChain, noMatchChain, ambiguousChain))
                        .build())
                .build();

        String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());

        assertThat(html).contains("function isResolvedChain(c)");
        assertThat(html).contains("function isAmbiguousWidenChain(c)");
        assertThat(html).contains("function isHighlightableChain(c)");
        assertThat(html).contains("function isSidebarChain(c)");
        assertThat(html).contains("function highlightableChainsForEntryPoint(ep)");
        assertThat(html).contains("Ambiguous link");
        assertThat(html).contains("active-path-ambiguous");
        assertThat(html).contains("Unresolved external");
        assertThat(html).contains("const chains = highlightableChainsForEntryPoint(ep)");
        assertThat(html).contains("linkResolution === 'UNRESOLVED_EXTERNAL'");
        assertThat(html).contains("linkResolution === 'RESOLVED'");
        assertThat(html).contains("linkResolution === 'AMBIGUOUS_WIDEN'");
        assertThat(html).contains("id=\"sidebar\"");
        assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
    }

    @Test
    void htmlShouldMatchCallChainsByEntryPointNameWhenControllerMethodIsShared() {
        EntryPoint payTransition = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/ORDER/transition/PAY")
                .className("com.example.web.StateMachineController")
                .methodName("transition")
                .build();
        EntryPoint shipTransition = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/ORDER/transition/SHIP")
                .className("com.example.web.StateMachineController")
                .methodName("transition")
                .build();

        CallChain payChain = CallChain.builder()
                .entryPoint(payTransition)
                .linkResolution(LinkResolution.RESOLVED)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.PAY")
                        .build())
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .sourceState("com.example.order.OrderState.NEW")
                        .targetState("com.example.order.OrderState.PAID")
                        .event("com.example.order.OrderEvent.PAY")
                        .linkKey("OrderState_NEW__OrderEvent_PAY")
                        .build()))
                .build();
        CallChain shipChain = CallChain.builder()
                .entryPoint(shipTransition)
                .linkResolution(LinkResolution.RESOLVED)
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.SHIP")
                        .build())
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .sourceState("com.example.order.OrderState.PAID")
                        .targetState("com.example.order.OrderState.SHIPPED")
                        .event("com.example.order.OrderEvent.SHIP")
                        .linkKey("OrderState_PAID__OrderEvent_SHIP")
                        .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(
                                "com.example.order.OrderEvent.PAY",
                                "com.example.order.OrderState.NEW",
                                "com.example.order.OrderState.PAID"),
                        shortTransition(
                                "com.example.order.OrderEvent.SHIP",
                                "com.example.order.OrderState.PAID",
                                "com.example.order.OrderState.SHIPPED")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.SHIPPED"))
                .metadata(CodebaseMetadata.builder()
                        .entryPoints(List.of(payTransition, shipTransition))
                        .callChains(List.of(payChain, shipChain))
                        .build())
                .build();

        String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());

        assertThat(html).contains("function entryPointsMatch(chainEntryPoint, sidebarEntryPoint)");
        assertThat(html).contains("POST /api/machine/ORDER/transition/PAY");
        assertThat(html).contains("POST /api/machine/ORDER/transition/SHIP");
        assertThat(html).contains("\"linkKey\" : \"OrderState_NEW__OrderEvent_PAY\"");
        assertThat(html).contains("\"linkKey\" : \"OrderState_PAID__OrderEvent_SHIP\"");
    }

    @Test
    void ambiguousWidenHtmlShouldExposeMatchedTransitionLinkKeys() {
        EntryPoint ambiguousEp = EntryPoint.builder()
                .type(EntryPoint.Type.CUSTOM)
                .name("ambiguous")
                .className("com.example.web.AmbiguousController")
                .methodName("trigger")
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .stateTypeFqn("com.example.order.OrderState")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(shortTransition(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderState.NEW",
                        "com.example.order.OrderState.PAID")))
                .startStates(Set.of("com.example.order.OrderState.NEW"))
                .endStates(Set.of("com.example.order.OrderState.PAID"))
                .metadata(CodebaseMetadata.builder()
                        .entryPoints(List.of(ambiguousEp))
                        .callChains(List.of(CallChain.builder()
                                .entryPoint(ambiguousEp)
                                .linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
                                .triggerPoint(TriggerPoint.builder()
                                        .event("com.example.order.OrderEvent.PAY")
                                        .ambiguous(true)
                                        .build())
                                .matchedTransitions(List.of(MatchedTransition.builder()
                                        .sourceState("com.example.order.OrderState.NEW")
                                        .targetState("com.example.order.OrderState.PAID")
                                        .event("com.example.order.OrderEvent.PAY")
                                        .build()))
                                .build()))
                        .build())
                .build();

        String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());

        assertThat(html).contains("Ambiguous link");
        assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
        assertThat(html).contains("\"linkKey\" : \"OrderState_NEW__OrderEvent_PAY\"");
        assertThat(html).contains("com.example.web.AmbiguousController");
    }

    /** Mirrors {@code template.html} link-key derivation for regression checks. */
    private static String buildLinkKey(String source, String event) {
        return buildLinkKey(source, event,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
    }

    private static String buildLinkKey(
            String source,
            String event,
            click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat,
            click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat) {
        if (event == null || event.isBlank()) {
            return "";
        }
        String formattedEvent = formatForLinkId(event, eventFormat);
        if (source == null || source.isBlank()) {
            return "__" + normalize(formattedEvent);
        }
        return normalize(formatForLinkId(source, stateFormat)) + "__" + normalize(formattedEvent);
    }

    private static String formatForLinkId(
            String identifier,
            click.kamil.springstatemachineexporter.exporter.EnumFormat format) {
        if (identifier == null || identifier.isBlank()) {
            return "";
        }
        return switch (format) {
            case fqn -> identifier;
            case sn -> {
                int lastDot = identifier.lastIndexOf('.');
                yield lastDot >= 0 ? identifier.substring(lastDot + 1) : identifier;
            }
            case fn -> {
                int lastDot = identifier.lastIndexOf('.');
                if (lastDot <= 0) {
                    yield identifier;
                }
                int prevDot = identifier.lastIndexOf('.', lastDot - 1);
                yield prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
            }
        };
    }

    private static String normalize(String s) {
        if (s == null || s.isBlank()) {
            return "";
        }
        return s.replaceAll("[^a-zA-Z0-9]", "_");
    }

    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 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 String extractSvg(String html) {
        int start = html.indexOf("<svg");
        int end = html.indexOf("</svg>");
        if (start < 0 || end < 0) {
            return html;
        }
        return html.substring(start, end + "</svg>".length());
    }

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