package com.cameleer.server.app.diagram;

import com.cameleer.common.graph.NodeType;
import com.cameleer.common.graph.RouteEdge;
import com.cameleer.common.graph.RouteGraph;
import com.cameleer.common.graph.RouteNode;
import com.cameleer.server.core.diagram.DiagramLayout;
import com.cameleer.server.core.diagram.PositionedNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Unit tests for {@link ElkDiagramRenderer}.
 * No Spring context needed -- pure unit test.
 */
class ElkDiagramRendererTest {

    private ElkDiagramRenderer renderer;

    @BeforeEach
    void setUp() {
        renderer = new ElkDiagramRenderer();
    }

    /**
     * Build a simple 3-node route: from(endpoint) -> process(bean) -> to(endpoint)
     */
    private RouteGraph buildSimpleGraph() {
        RouteGraph graph = new RouteGraph("test-route");
        graph.setExtractedAt(Instant.now());
        graph.setVersion(1);

        RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
        RouteNode process = new RouteNode("node-2", NodeType.BEAN, "myProcessor");
        RouteNode to = new RouteNode("node-3", NodeType.TO, "log:output");

        from.setChildren(List.of(process, to));
        graph.setRoot(from);
        graph.setEdges(List.of(
                new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW)
        ));

        return graph;
    }

    /**
     * Build a compound graph: from -> choice -> (when, otherwise) -> to
     */
    private RouteGraph buildCompoundGraph() {
        RouteGraph graph = new RouteGraph("compound-route");
        graph.setExtractedAt(Instant.now());
        graph.setVersion(1);

        RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "direct:start");
        RouteNode choice = new RouteNode("node-2", NodeType.EIP_CHOICE, "choice");
        RouteNode when = new RouteNode("node-3", NodeType.EIP_WHEN, "when(simple)");
        RouteNode otherwise = new RouteNode("node-4", NodeType.EIP_OTHERWISE, "otherwise");
        RouteNode to = new RouteNode("node-5", NodeType.TO, "log:result");

        // Build tree: from → [choice, to]; choice → [when, otherwise]
        choice.setChildren(List.of(when, otherwise));
        from.setChildren(List.of(choice, to));
        graph.setRoot(from);
        graph.setEdges(List.of(
                new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-4", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-3", "node-5", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-4", "node-5", RouteEdge.EdgeType.FLOW)
        ));

        return graph;
    }

    @Test
    void renderSvg_simpleGraph_producesValidSvg() {
        String svg = renderer.renderSvg(buildSimpleGraph());

        assertNotNull(svg);
        assertTrue(svg.contains("<svg"), "SVG should contain <svg element");
        assertTrue(svg.contains("</svg>"), "SVG should be properly closed");
    }

    @Test
    void renderSvg_simpleGraph_containsNodeShapes() {
        String svg = renderer.renderSvg(buildSimpleGraph());

        // Should contain rect elements for nodes
        assertTrue(svg.contains("<rect") || svg.contains("<path"),
                "SVG should contain rect or path elements for nodes");
    }

    @Test
    void renderSvg_simpleGraph_containsNodeLabels() {
        String svg = renderer.renderSvg(buildSimpleGraph());

        assertTrue(svg.contains("timer:tick"), "SVG should contain endpoint label");
        assertTrue(svg.contains("myProcessor"), "SVG should contain processor label");
        assertTrue(svg.contains("log:output"), "SVG should contain to label");
    }

    @Test
    void renderSvg_endpointNodes_haveBlueColor() {
        String svg = renderer.renderSvg(buildSimpleGraph());

        // Endpoint nodes should have blue fill (#3B82F6)
        assertTrue(svg.contains("#3B82F6") || svg.contains("#3b82f6") ||
                        svg.contains("rgb(59,130,246)") || svg.contains("rgb(59, 130, 246)"),
                "Endpoint nodes should have blue fill color (#3B82F6)");
    }

    @Test
    void renderSvg_containsEdgeLines() {
        String svg = renderer.renderSvg(buildSimpleGraph());

        // Edges should be drawn as lines or paths
        assertTrue(svg.contains("<line") || svg.contains("<polyline") || svg.contains("<path"),
                "SVG should contain line/path elements for edges");
    }

    @Test
    void layoutJson_simpleGraph_returnsCorrectNodeCount() {
        DiagramLayout layout = renderer.layoutJson(buildSimpleGraph());

        assertNotNull(layout);
        assertEquals(3, layout.nodes().size(), "Should have 3 positioned nodes");
    }

    @Test
    void layoutJson_simpleGraph_nodesHavePositiveCoordinates() {
        DiagramLayout layout = renderer.layoutJson(buildSimpleGraph());

        for (PositionedNode node : layout.nodes()) {
            assertTrue(node.x() >= 0, "Node x should be >= 0: " + node.id());
            assertTrue(node.y() >= 0, "Node y should be >= 0: " + node.id());
            assertTrue(node.width() > 0, "Node width should be > 0: " + node.id());
            assertTrue(node.height() > 0, "Node height should be > 0: " + node.id());
        }
    }

    @Test
    void layoutJson_simpleGraph_hasPositiveDimensions() {
        DiagramLayout layout = renderer.layoutJson(buildSimpleGraph());

        assertTrue(layout.width() > 0, "Layout width should be positive");
        assertTrue(layout.height() > 0, "Layout height should be positive");
    }

    @Test
    void layoutJson_simpleGraph_hasEdges() {
        DiagramLayout layout = renderer.layoutJson(buildSimpleGraph());

        assertEquals(2, layout.edges().size(), "Should have 2 edges");
    }

    @Test
    void layoutJson_compoundGraph_choiceNodeHasChildren() {
        DiagramLayout layout = renderer.layoutJson(buildCompoundGraph());

        PositionedNode choiceNode = layout.nodes().stream()
                .filter(n -> "node-2".equals(n.id()))
                .findFirst()
                .orElseThrow(() -> new AssertionError("Choice node not found"));

        assertNotNull(choiceNode.children(), "Choice node should have children");
        assertFalse(choiceNode.children().isEmpty(), "Choice node should have non-empty children");
        assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
    }

    /**
     * Build a DO_TRY graph: from -> doTry(try: [process, log], doFinally: [cleanup], doCatch: [errorLog]) -> to
     */
    private RouteGraph buildDoTryGraph() {
        RouteGraph graph = new RouteGraph("try-catch-route");
        graph.setExtractedAt(Instant.now());
        graph.setVersion(1);

        RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
        RouteNode doTry = new RouteNode("node-2", NodeType.DO_TRY, "doTry");
        RouteNode process = new RouteNode("node-3", NodeType.PROCESSOR, "process");
        RouteNode log1 = new RouteNode("node-4", NodeType.LOG, "log:tryBody");
        RouteNode doFinally = new RouteNode("node-5", NodeType.DO_FINALLY, "doFinally");
        RouteNode cleanup = new RouteNode("node-6", NodeType.LOG, "log:cleanup");
        RouteNode doCatch = new RouteNode("node-7", NodeType.DO_CATCH, "doCatch");
        RouteNode errorLog = new RouteNode("node-8", NodeType.LOG, "log:error");
        RouteNode to = new RouteNode("node-9", NodeType.TO, "log:done");

        doFinally.setChildren(List.of(cleanup));
        doCatch.setChildren(List.of(errorLog));
        doTry.setChildren(List.of(process, log1, doFinally, doCatch));
        from.setChildren(List.of(doTry, to));
        graph.setRoot(from);
        graph.setEdges(List.of(
                new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-3", "node-4", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-5", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-5", "node-6", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-7", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-7", "node-8", RouteEdge.EdgeType.FLOW),
                new RouteEdge("node-2", "node-9", RouteEdge.EdgeType.FLOW)
        ));

        return graph;
    }

    @Test
    void layoutJson_doTryGraph_sectionsInCorrectOrder() {
        DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());

        assertNotNull(layout);

        // Find the DO_TRY compound node
        PositionedNode doTryNode = layout.nodes().stream()
                .filter(n -> "node-2".equals(n.id()))
                .findFirst()
                .orElseThrow(() -> new AssertionError("DO_TRY node not found"));

        assertNotNull(doTryNode.children(), "DO_TRY should have children");
        assertFalse(doTryNode.children().isEmpty(), "DO_TRY should have non-empty children");

        // Find sections by ID pattern
        PositionedNode tryBody = doTryNode.children().stream()
                .filter(n -> n.id() != null && n.id().contains("._try_body"))
                .findFirst().orElse(null);
        PositionedNode finallySection = doTryNode.children().stream()
                .filter(n -> "node-5".equals(n.id()))
                .findFirst().orElse(null);
        PositionedNode catchSection = doTryNode.children().stream()
                .filter(n -> "node-7".equals(n.id()))
                .findFirst().orElse(null);

        assertNotNull(tryBody, "Try body wrapper should exist");
        assertNotNull(finallySection, "doFinally section should exist");
        assertNotNull(catchSection, "doCatch section should exist");

        // Verify vertical order: tryBody.y < doFinally.y < doCatch.y
        assertTrue(tryBody.y() < finallySection.y(),
                "Try body (y=" + tryBody.y() + ") should be above doFinally (y=" + finallySection.y() + ")");
        assertTrue(finallySection.y() < catchSection.y(),
                "doFinally (y=" + finallySection.y() + ") should be above doCatch (y=" + catchSection.y() + ")");
    }

    @Test
    void layoutJson_doTryGraph_sectionsHaveSameWidth() {
        DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());

        PositionedNode doTryNode = layout.nodes().stream()
                .filter(n -> "node-2".equals(n.id()))
                .findFirst()
                .orElseThrow(() -> new AssertionError("DO_TRY node not found"));

        List<PositionedNode> sections = doTryNode.children();
        double firstWidth = sections.get(0).width();
        for (PositionedNode section : sections) {
            assertEquals(firstWidth, section.width(), 0.1,
                    "All sections should have the same width, but " + section.id() + " differs");
        }
    }

    @Test
    void renderSvg_compoundGraph_producesValidSvg() {
        String svg = renderer.renderSvg(buildCompoundGraph());

        assertNotNull(svg);
        assertTrue(svg.contains("<svg"), "Compound SVG should contain <svg element");
        assertTrue(svg.contains("choice"), "SVG should contain choice label");
    }
}
