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.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
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 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.Map;
import java.util.Set;

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

class JsonRoundTripCanonicalizationTest {

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

        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"))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(TriggerPoint.builder()
                                .event("OrderEvent.PAY")
                                .polymorphicEvents(List.of("OrderEvent.PAY"))
                                .className("com.example.web.OrderController")
                                .methodName("pay")
                                .sourceFile("OrderController.java")
                                .build()))
                        .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"));

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

        assertThat(roundTripped.getStateTypeFqn()).isEqualTo("com.example.order.OrderState");
        assertThat(roundTripped.getEventTypeFqn()).isEqualTo("com.example.order.OrderEvent");
        assertThat(roundTripped.getTransitions().get(0).getEvent().fullIdentifier())
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(roundTripped.getStartStates())
                .containsExactly("com.example.order.OrderState.NEW");
        assertThat(roundTripped.getMetadata().getTriggers().get(0).getPolymorphicEvents())
                .containsExactly("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldResolveMachineTypesFromSourceProjectWhenJsonLacksEmbeddedTypes(@TempDir Path tempDir) throws Exception {
        writeSampleProject(tempDir.resolve("project"));
        Path projectRoot = tempDir.resolve("project");

        AnalysisResult shortForm = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .transitions(List.of(shortTransition(
                        "OrderEvent.PAY",
                        "OrderState.NEW",
                        "OrderState.PAID")))
                .build();

        Path jsonFile = tempDir.resolve("legacy.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(), null, null, projectRoot);

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

        assertThat(roundTripped.getTransitions().get(0).getEvent().fullIdentifier())
                .isEqualTo("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldResolvePropertyPlaceholdersOnJsonReExportWithSourceContext(@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();

        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")))
                .metadata(CodebaseMetadata.builder()
                        .triggers(List.of(trigger))
                        .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"));

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

        TriggerPoint resolvedTrigger = roundTripped.getMetadata().getTriggers().get(0);
        assertThat(resolvedTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(resolvedTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
        assertThat(resolvedTrigger.getPolymorphicEvents())
                .containsExactly("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldRelinkMatchedTransitionsAfterPreCanonicalizingShortFormJson(@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")
                .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")))
                .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 JsonExporter()));
        exportService.runJsonExporter(jsonFile, outputDir, List.of("json"));

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

        assertThat(roundTripped.getMetadata().getCallChains()).hasSize(1);
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.order.OrderEvent.PAY");
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getSourceState())
                .isEqualTo("com.example.order.OrderState.NEW");
    }

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

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.CUSTOM)
                        .className("com.example.web.OrderController")
                        .methodName("pay")
                        .sourceFile("OrderController.java")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("event")
                        .className("com.example.web.OrderStateMachine")
                        .methodName("sendEvent")
                        .sourceFile("OrderStateMachine.java")
                        .build())
                .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")))
                .metadata(CodebaseMetadata.builder()
                        .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 JsonExporter()));
        exportService.runJsonExporter(jsonFile, outputDir, List.of("json"));

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

        assertThat(roundTripped.getMetadata().getCallChains()).hasSize(1);
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint()).isNotNull();
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint().getClassName())
                .isEqualTo("com.example.web.OrderController");
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint().getMethodName())
                .isEqualTo("pay");
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
        assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.order.OrderEvent.PAY");
    }

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

        AnalysisResult result = 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.NEW")))
                .build();

        result.applyResolution(Map.of());

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

    @Test
    void shouldFindProjectRootFromJsonDirectory(@TempDir Path tempDir) throws Exception {
        Path projectRoot = tempDir.resolve("my-app");
        writeSampleProject(projectRoot);
        Path nestedJsonDir = projectRoot.resolve("build/export");
        Files.createDirectories(nestedJsonDir);

        assertThat(JsonExportContextFactory.findProjectRoot(nestedJsonDir))
                .isEqualTo(projectRoot.toAbsolutePath().normalize());
    }

    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");
        Path webPkg = javaRoot.resolve("com/example/web");
        Files.createDirectories(orderPkg);
        Files.createDirectories(configPkg);
        Files.createDirectories(webPkg);
        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> {
                }
                """);
        Files.writeString(webPkg.resolve("OrderController.java"),
                """
                package com.example.web;
                import com.example.order.OrderEvent;
                public class OrderController {
                    private final OrderStateMachine stateMachine = new OrderStateMachine();
                    public void pay() {
                        stateMachine.sendEvent(OrderEvent.PAY);
                    }
                }
                """);
        Files.writeString(webPkg.resolve("OrderStateMachine.java"),
                """
                package com.example.web;
                import com.example.order.OrderEvent;
                public class OrderStateMachine {
                    public void sendEvent(OrderEvent event) {
                    }
                }
                """);
    }
}
