package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.service.ExportService;
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.List;
import java.util.stream.StreamSupport;

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

/**
 * End-to-end verification for {@code state_machines/layered_dispatcher_sample}:
 * REST endpoint → string key → {@code DomainCommand} → transition event → single matched transition.
 */
class LayeredDispatcherSampleTest {

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

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

        ObjectMapper mapper = new ObjectMapper();
        JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", mapper);
        JsonNode documentMachine = readMachineJson(tempDir, "StandardDocumentStateMachineConfiguration", mapper);

        assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY",
                "click.kamil.examples.statemachine.layered.order.OrderState.NEW",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID");
        assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/ship",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.SHIP",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID",
                "click.kamil.examples.statemachine.layered.order.OrderState.SHIPPED");
        assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/cancel",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.CANCEL",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID",
                "click.kamil.examples.statemachine.layered.order.OrderState.CANCELLED");

        assertEndpointLinksSingleTransition(documentMachine, "POST /api/documents/submit",
                "click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent.SUBMIT",
                "click.kamil.examples.statemachine.layered.document.DocumentState.DRAFT",
                "click.kamil.examples.statemachine.layered.document.DocumentState.SUBMITTED");
        assertEndpointLinksSingleTransition(documentMachine, "POST /api/documents/approve",
                "click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent.APPROVE",
                "click.kamil.examples.statemachine.layered.document.DocumentState.SUBMITTED",
                "click.kamil.examples.statemachine.layered.document.DocumentState.APPROVED");
        assertEndpointLinksSingleTransition(documentMachine, "POST /api/documents/reject",
                "click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent.REJECT",
                "click.kamil.examples.statemachine.layered.document.DocumentState.SUBMITTED",
                "click.kamil.examples.statemachine.layered.document.DocumentState.REJECTED");

        assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/rich/pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY",
                "click.kamil.examples.statemachine.layered.order.OrderState.NEW",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID");
        assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/rich/ship",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.SHIP",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID",
                "click.kamil.examples.statemachine.layered.order.OrderState.SHIPPED");

        assertEndpointLinksSingleTransition(orderMachine, "POST /api/string-dispatch/orders/pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY",
                "click.kamil.examples.statemachine.layered.order.OrderState.NEW",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID");
        assertEndpointLinksSingleTransition(orderMachine, "POST /api/string-dispatch/orders/ship",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.SHIP",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID",
                "click.kamil.examples.statemachine.layered.order.OrderState.SHIPPED");
    }

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

        JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", new ObjectMapper());

        assertSingleMatchedChain(orderMachine, "POST /api/string-dispatch/orders/pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY");
        assertSingleMatchedChain(orderMachine, "POST /api/string-dispatch/orders/ship",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.SHIP");
    }

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

        JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", new ObjectMapper());

        assertEndpointLinksSingleTransition(orderMachine, "POST /api/commands/order.pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY",
                "click.kamil.examples.statemachine.layered.order.OrderState.NEW",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID");
        assertEndpointLinksSingleTransition(orderMachine, "POST /api/commands/order.ship",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.SHIP",
                "click.kamil.examples.statemachine.layered.order.OrderState.PAID",
                "click.kamil.examples.statemachine.layered.order.OrderState.SHIPPED");
        assertSingleMatchedChain(orderMachine, "POST /api/commands/order.pay",
                "click.kamil.examples.statemachine.layered.order.OrderTransitionEvent.PAY");

        JsonNode documentMachine = readMachineJson(tempDir, "StandardDocumentStateMachineConfiguration", new ObjectMapper());
        assertEndpointLinksSingleTransition(documentMachine, "POST /api/commands/document.submit",
                "click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent.SUBMIT",
                "click.kamil.examples.statemachine.layered.document.DocumentState.DRAFT",
                "click.kamil.examples.statemachine.layered.document.DocumentState.SUBMITTED");
        JsonNode entryPoints = documentMachine.path("metadata").path("entryPoints");
        assertThat(entryPoints.isArray()).isTrue();
        assertThat(entryPoints.findValuesAsText("name"))
                .noneMatch(name -> name.contains("/api/orders/"));
    }

    private static void assertSingleMatchedChain(JsonNode machineJson, String endpointName, String expectedEvent) {
        List<JsonNode> matchedChains = findMatchedCallChains(machineJson, endpointName);
        assertThat(matchedChains).as("matched chains for %s", endpointName).hasSize(1);
        assertThat(matchedChains.get(0).get("matchedTransitions")).hasSize(1);
        assertThat(matchedChains.get(0).get("matchedTransitions").get(0).get("event").asText()).isEqualTo(expectedEvent);
    }

    private static List<JsonNode> findMatchedCallChains(JsonNode machineJson, String endpointName) {
        JsonNode callChains = machineJson.path("metadata").path("callChains");
        if (!callChains.isArray()) {
            return List.of();
        }
        List<JsonNode> result = new java.util.ArrayList<>();
        for (JsonNode chain : callChains) {
            if (!endpointName.equals(chain.path("entryPoint").path("name").asText())) {
                continue;
            }
            JsonNode matched = chain.get("matchedTransitions");
            if (matched != null && matched.isArray() && !matched.isEmpty()) {
                result.add(chain);
            }
        }
        return result;
    }

    private static JsonNode readMachineJson(Path outputDir, String configBaseName, ObjectMapper mapper)
            throws Exception {
        Path machineDir;
        try (var stream = Files.list(outputDir)) {
            machineDir = stream
                    .filter(Files::isDirectory)
                    .filter(path -> path.getFileName().toString().endsWith(configBaseName))
                    .findFirst()
                    .orElseThrow(() -> new IllegalStateException("No output for " + configBaseName));
        }
        Path jsonFile = machineDir.resolve(machineDir.getFileName() + ".json");
        return mapper.readTree(jsonFile.toFile());
    }

    private static void assertEndpointLinksSingleTransition(
            JsonNode machineJson,
            String endpointName,
            String expectedEvent,
            String expectedSource,
            String expectedTarget) {
        JsonNode chain = findCallChain(machineJson, endpointName);
        assertThat(chain).as("call chain for %s", endpointName).isNotNull();

        JsonNode matched = chain.get("matchedTransitions");
        assertThat(matched).as("matchedTransitions for %s", endpointName).isNotNull().hasSize(1);
        assertThat(matched.get(0).get("event").asText()).isEqualTo(expectedEvent);
        assertThat(matched.get(0).get("sourceState").asText()).isEqualTo(expectedSource);
        assertThat(matched.get(0).get("targetState").asText()).isEqualTo(expectedTarget);
    }

    private static JsonNode findCallChain(JsonNode machineJson, String endpointName) {
        JsonNode callChains = machineJson.path("metadata").path("callChains");
        if (!callChains.isArray()) {
            return null;
        }
        return StreamSupport.stream(callChains.spliterator(), false)
                .filter(chain -> endpointName.equals(chain.path("entryPoint").path("name").asText()))
                .findFirst()
                .orElse(null);
    }
}
