package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

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

class CallGraphPathFinderTest {

    private CallGraphPathFinder pathFinder;

    @BeforeEach
    void setUp() {
        pathFinder = new CallGraphPathFinder(new CodebaseContext());
    }

    @Test
    void shouldSkipFrameworkAndJdkPaths() {
        Map<String, List<CallEdge>> graph = new HashMap<>();

        // Setup an anonymized call graph where business logic delegates to a platform boundary
        // e.g. BusinessService.process -> java.util.stream.Stream.map -> AnotherBusinessService.doWork
        // The pathfinder should skip the java.* path, avoiding spurious hops.
        
        graph.put("com.acme.BusinessService.process", List.of(
                new CallEdge("java.util.stream.Stream.map", List.of()),
                new CallEdge("com.acme.DirectService.execute", List.of())
        ));
        graph.put("java.util.stream.Stream.map", List.of(
                new CallEdge("com.acme.AnotherBusinessService.doWork", List.of())
        ));
        graph.put("com.acme.DirectService.execute", List.of(
                new CallEdge("com.acme.AnotherBusinessService.doWork", List.of())
        ));

        List<List<String>> paths = pathFinder.findAllPaths(
                "com.acme.BusinessService.process",
                "com.acme.AnotherBusinessService.doWork",
                graph,
                new HashSet<>()
        );

        // It should ONLY find the direct business path, skipping the java.util path.
        assertThat(paths).hasSize(1);
        assertThat(paths.get(0)).containsExactly(
                "com.acme.BusinessService.process",
                "com.acme.DirectService.execute",
                "com.acme.AnotherBusinessService.doWork"
        );
    }

    @Test
    void shouldFollowValidBusinessPaths() {
        Map<String, List<CallEdge>> graph = new HashMap<>();

        graph.put("com.acme.Start.run", List.of(
                new CallEdge("com.acme.Middle.process", List.of())
        ));
        graph.put("com.acme.Middle.process", List.of(
                new CallEdge("com.acme.End.finish", List.of())
        ));

        List<List<String>> paths = pathFinder.findAllPaths(
                "com.acme.Start.run",
                "com.acme.End.finish",
                graph,
                new HashSet<>()
        );

        assertThat(paths).hasSize(1);
        assertThat(paths.get(0)).containsExactly(
                "com.acme.Start.run",
                "com.acme.Middle.process",
                "com.acme.End.finish"
        );
    }

    @Test
    void shouldPruneBranchesDuringSearchWhenConstraintsPresent() {
        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.acme.Start.run", List.of(
                new CallEdge("com.acme.BranchA.step", List.of(), null, "command == \"PAY\""),
                new CallEdge("com.acme.BranchB.step", List.of(), null, "command == \"SHIP\"")
        ));
        graph.put("com.acme.BranchA.step", List.of(
                new CallEdge("com.acme.End.finish", List.of())
        ));
        graph.put("com.acme.BranchB.step", List.of(
                new CallEdge("com.acme.End.finish", List.of())
        ));

        CallGraphPathFinder pathFinder = new CallGraphPathFinder(new CodebaseContext());
        PathBindingEvaluator evaluator = new PathBindingEvaluator(
                new CodebaseContext(), null, null, null);

        List<List<String>> unconstrained = pathFinder.findAllPaths(
                "com.acme.Start.run", "com.acme.End.finish", graph, new HashSet<>());
        assertThat(unconstrained).hasSize(2);

        List<List<String>> payOnly = pathFinder.findAllPaths(
                "com.acme.Start.run",
                "com.acme.End.finish",
                graph,
                new HashSet<>(),
                evaluator,
                Map.of("command", "PAY"));
        assertThat(payOnly).hasSize(1);
        assertThat(payOnly.get(0)).containsExactly(
                "com.acme.Start.run",
                "com.acme.BranchA.step",
                "com.acme.End.finish");
    }

    @Test
    void shouldRejectSiblingSubclassVirtualHopsWhenConcreteSelfIsKnown() throws Exception {
        Path tempDir = Files.createTempDirectory("pathfinder_concrete_self");
        Files.writeString(tempDir.resolve("Listeners.java"), """
                package com.example;
                abstract class AbstractListener {
                    void processMessage() { doPMessage(); }
                    abstract void doPMessage();
                }
                class OrderListener extends AbstractListener {
                    void onMessage() { processMessage(); }
                    void doPMessage() {}
                }
                class DocumentListener extends AbstractListener {
                    void onMessage() { processMessage(); }
                    void doPMessage() {}
                }
                class StateMachine {
                    void sendEvent() {}
                }
                """);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.OrderListener.onMessage", List.of(
                new CallEdge("com.example.AbstractListener.processMessage", List.of())));
        graph.put("com.example.AbstractListener.processMessage", List.of(
                new CallEdge("com.example.OrderListener.doPMessage", List.of()),
                new CallEdge("com.example.DocumentListener.doPMessage", List.of())));
        graph.put("com.example.OrderListener.doPMessage", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));
        graph.put("com.example.DocumentListener.doPMessage", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));

        List<List<String>> withoutSelf = pathFinder.findAllPaths(
                "com.example.OrderListener.onMessage",
                "com.example.StateMachine.sendEvent",
                graph,
                new HashSet<>());
        assertThat(withoutSelf).hasSize(2);

        List<List<String>> withOrderSelf = pathFinder.findAllPaths(
                "com.example.OrderListener.onMessage",
                "com.example.StateMachine.sendEvent",
                graph,
                new HashSet<>(),
                "com.example.OrderListener");
        assertThat(withOrderSelf).hasSize(1);
        assertThat(withOrderSelf.get(0))
                .containsExactly(
                        "com.example.OrderListener.onMessage",
                        "com.example.AbstractListener.processMessage",
                        "com.example.OrderListener.doPMessage",
                        "com.example.StateMachine.sendEvent")
                .doesNotContain("com.example.DocumentListener.doPMessage");
    }

    @Test
    void shouldAllowParentOverrideWhenConcreteSelfIsMoreSpecificSubclass() throws Exception {
        Path tempDir = Files.createTempDirectory("pathfinder_parent_override");
        Files.writeString(tempDir.resolve("Hierarchy.java"), """
                package com.example;
                abstract class AbstractListener {
                    void processMessage() { doPMessage(); }
                    abstract void doPMessage();
                }
                class OrderListener extends AbstractListener {
                    void doPMessage() {}
                }
                class SpecialOrderListener extends OrderListener {
                    void onMessage() { processMessage(); }
                }
                class StateMachine { void sendEvent() {} }
                """);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.SpecialOrderListener.onMessage", List.of(
                new CallEdge("com.example.AbstractListener.processMessage", List.of())));
        graph.put("com.example.AbstractListener.processMessage", List.of(
                new CallEdge("com.example.OrderListener.doPMessage", List.of()),
                new CallEdge("com.example.DocumentListener.doPMessage", List.of())));
        graph.put("com.example.OrderListener.doPMessage", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));

        List<List<String>> paths = pathFinder.findAllPaths(
                "com.example.SpecialOrderListener.onMessage",
                "com.example.StateMachine.sendEvent",
                graph,
                new HashSet<>(),
                "com.example.SpecialOrderListener");

        assertThat(paths).hasSize(1);
        assertThat(paths.get(0)).contains("com.example.OrderListener.doPMessage");
    }

    @Test
    void shouldNotPruneCollaboratorAbstractServiceHopsForRestControllerEntry() throws Exception {
        Path tempDir = Files.createTempDirectory("pathfinder_rest_collaborator");
        Files.writeString(tempDir.resolve("Rest.java"), """
                package com.example;
                class OrderController {
                    void pay() { shared.process(); }
                }
                abstract class AbstractSharedService {
                    void process() { doWork(); }
                    abstract void doWork();
                }
                class OrderSharedService extends AbstractSharedService {
                    StateMachine sm = new StateMachine();
                    void doWork() { sm.sendEvent(); }
                }
                class DocumentSharedService extends AbstractSharedService {
                    StateMachine sm = new StateMachine();
                    void doWork() { sm.sendEvent(); }
                }
                class StateMachine { void sendEvent() {} }
                """);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);

        // Controller entry self is NOT in AbstractSharedService hierarchy — without stamped
        // receiver types, pruning must fail open so REST still reaches impl hooks.
        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.OrderController.pay", List.of(
                new CallEdge("com.example.AbstractSharedService.process", List.of())));
        graph.put("com.example.AbstractSharedService.process", List.of(
                new CallEdge("com.example.OrderSharedService.doWork", List.of()),
                new CallEdge("com.example.DocumentSharedService.doWork", List.of())));
        graph.put("com.example.OrderSharedService.doWork", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));
        graph.put("com.example.DocumentSharedService.doWork", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));

        List<List<String>> paths = pathFinder.findAllPaths(
                "com.example.OrderController.pay",
                "com.example.StateMachine.sendEvent",
                graph,
                new HashSet<>(),
                "com.example.OrderController");

        assertThat(paths)
                .as("REST controller entry must still reach collaborator abstract-service impls")
                .hasSize(2);
        assertThat(paths.stream().flatMap(List::stream))
                .anyMatch(hop -> hop.contains("OrderSharedService.doWork"))
                .anyMatch(hop -> hop.contains("DocumentSharedService.doWork"));
    }

    @Test
    void shouldPruneCollaboratorTemplateFanOutWhenEdgeStampsConcreteReceiverType() throws Exception {
        Path tempDir = Files.createTempDirectory("pathfinder_stamped_receiver");
        Files.writeString(tempDir.resolve("Ops.java"), """
                package com.example;
                class OrderJmsListener {
                    void onMessage() { ops.process(); }
                    AbstractOps ops;
                }
                abstract class AbstractOps {
                    void process() { fire(); }
                    abstract void fire();
                }
                class PayOps extends AbstractOps {
                    void fire() {}
                }
                class ShipOps extends AbstractOps {
                    void fire() {}
                }
                class StateMachine { void sendEvent() {} }
                """);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.OrderJmsListener.onMessage", List.of(
                new CallEdge(
                        "com.example.PayOps.process",
                        List.of(),
                        "ops",
                        null,
                        "com.example.PayOps")));
        graph.put("com.example.AbstractOps.process", List.of(
                new CallEdge("com.example.PayOps.fire", List.of()),
                new CallEdge("com.example.ShipOps.fire", List.of())));
        graph.put("com.example.PayOps.fire", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));
        graph.put("com.example.ShipOps.fire", List.of(
                new CallEdge("com.example.StateMachine.sendEvent", List.of())));

        List<List<String>> paths = pathFinder.findAllPaths(
                "com.example.OrderJmsListener.onMessage",
                "com.example.StateMachine.sendEvent",
                graph,
                new HashSet<>(),
                "com.example.OrderJmsListener");

        assertThat(paths).hasSize(1);
        assertThat(paths.get(0))
                .contains("com.example.PayOps.fire")
                .doesNotContain("com.example.ShipOps.fire");
    }
}
