package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;

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

@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineExtendedTest {
    /**
     * Diagnostic: Generics, inherited processing, multi-enum constructor args,
     * and internal constructor mapping via a switch method.
     * <p>
     * Pattern:
     * 1. Abstract base class AbstractEventClass<T> holds the payload and transitionType.
     * 2. Abstract base class also contains the method that actually fires the state machine.
     * 3. MyEventClass<T> constructor takes multiple enums (InputEnum1, InputEnum2).
     * 4. MyEventClass assigns: this.transitionType = computeTransitionType(InputEnum2).
     * 5. computeTransitionType uses a switch to map InputEnum2 to TransitionEnum.
     * <p>
     * Root cause targeted: The engine must not eagerly grab InputEnum1.IGNORE or
     * InputEnum2.SOURCE_B. It must evaluate the internal computeTransitionType()
     * method during instantiation to resolve the final TransitionEnum.STATE_Y.
     */
    @Test
    void shouldResolveTransitionEnumThroughGenericsAndInternalConstructorSwitchComputation() throws IOException {
        String source = """
                package com.example;
                
                public class SystemController {
                    private StateMachine machine;
                
                    public void processIncomingPayload(String rawData) {
                        // Creates concrete class with multiple enums and a generic payload.
                        // Engine MUST NOT stop at IGNORE or SOURCE_B.
                        MyEventClass<String> event = new MyEventClass<>(
                            rawData, 
                            InputEnum1.IGNORE, 
                            InputEnum2.SOURCE_B
                        );
                
                        // Calls the inherited method that actually processes the event
                        event.fireEvent(machine);
                    }
                }
                
                abstract class AbstractEventClass<T> {
                    protected T payload;
                    protected TransitionEnum transitionType;
                
                    public AbstractEventClass(T payload) {
                        this.payload = payload;
                    }
                
                    // The method to actually process events
                    public void fireEvent(StateMachine machine) {
                        machine.fire(this.getTransitionType());
                    }
                
                    public TransitionEnum getTransitionType() {
                        return this.transitionType;
                    }
                }
                
                class MyEventClass<T> extends AbstractEventClass<T> {
                    private InputEnum1 routingFlag;
                
                    public MyEventClass(T payload, InputEnum1 flag, InputEnum2 source) {
                        super(payload);
                        this.routingFlag = flag;
                
                        // The trap: Assignment via an internal method evaluation
                        this.transitionType = computeTransitionType(source);
                    }
                
                    private TransitionEnum computeTransitionType(InputEnum2 source) {
                        return switch (source) {
                            case SOURCE_A -> TransitionEnum.STATE_X;
                            case SOURCE_B -> TransitionEnum.STATE_Y; // Expected resolution
                            case SOURCE_C -> TransitionEnum.STATE_Z;
                        };
                    }
                }
                
                class StateMachine {
                    public void fire(TransitionEnum event) {}
                }
                
                enum InputEnum1 { IGNORE, PROCESS_ASYNC }
                enum InputEnum2 { SOURCE_A, SOURCE_B, SOURCE_C }
                enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
                """;

        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_generics_computation");
        java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.SystemController")
                .methodName("processIncomingPayload")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));

        assertThat(chains).hasSize(1);

        System.out.println("GENERICS & COMPUTATION polymorphicEvents: " +
                chains.get(0).getTriggerPoint().getPolymorphicEvents());

        // The assertion to prove the bug is fixed.
        // If the engine returns InputEnum2.SOURCE_B or InputEnum1.IGNORE, it fails.
        // It MUST evaluate computeTransitionType() and return TransitionEnum.STATE_Y.
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("TransitionEnum.STATE_Y");
    }
    /**
     * Diagnostic: Enum tracked through an overridden method and a super() call.
     *
     * Pattern:
     * 1. EntryPoint calls an overridden method on a subclass (CustomHandler.handleEvent)
     * with an initial enum (ActionType.BEGIN_CHECKOUT).
     * 2. The subclass intercepts the call, does some localized logic, and delegates
     * back to the parent using super.handleEvent(ActionType).
     * 3. The parent class (BaseHandler) takes that enum, transforms it via a switch
     * statement to a TransitionEnum, and fires the state machine.
     *
     * Root cause targeted: The engine must successfully resolve the `super` keyword
     * to the parent class's AST node, maintain the argument mapping across the inheritance
     * boundary, and evaluate the switch to get the final TransitionEnum. It must not
     * get stuck in a recursive loop or eagerly return ActionType.BEGIN_CHECKOUT.
     */
    @Test
    void shouldResolveMappedEnumAcrossOverriddenMethodAndSuperDelegation() throws IOException {
        String source = """
                package com.example;

                public class ApiEndpoint {
                    private CustomHandler handler;

                    public void triggerCheckout() {
                        // Entry point passes the initial enum
                        handler.handleEvent(ActionType.BEGIN_CHECKOUT);
                    }
                }

                abstract class BaseHandler {
                    protected StateMachine machine;

                    // The method that gets overridden, but ultimately does the work
                    public void handleEvent(ActionType action) {
                        TransitionEnum transition = switch (action) {
                            case BEGIN_CHECKOUT -> TransitionEnum.CHECKOUT_STARTED; // Expected
                            case ABORT_CHECKOUT -> TransitionEnum.CHECKOUT_CANCELLED;
                        };
                        machine.fire(transition);
                    }
                }

                class CustomHandler extends BaseHandler {
                    
                    @Override
                    public void handleEvent(ActionType action) {
                        // Custom logic before delegation (simulated)
                        if (action == null) return;
                        
                        // The trap: The engine must follow 'super' to BaseHandler
                        // and carry the 'action' argument with it.
                        super.handleEvent(action);
                    }
                }

                class StateMachine {
                    public void fire(TransitionEnum event) {}
                }

                enum ActionType { BEGIN_CHECKOUT, ABORT_CHECKOUT }
                enum TransitionEnum { CHECKOUT_STARTED, CHECKOUT_CANCELLED }
                """;

        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_super_override");
        java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);

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

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.ApiEndpoint")
                .methodName("triggerCheckout")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));

        assertThat(chains).hasSize(1);

        System.out.println("SUPER DELEGATION polymorphicEvents: " +
                chains.get(0).getTriggerPoint().getPolymorphicEvents());

        // The assertion to prove the bug is fixed.
        // It MUST NOT contain "ActionType.BEGIN_CHECKOUT".
        // It MUST successfully trace through super.handleEvent() and resolve the switch.
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED");
    }

    @Test
    void shouldResolveCorrectEnumWhenNamesCollide() throws IOException {
        String source = """
                package com.example;

                public class OrderService {
                    private StateMachine machine;

                    public void processOrder() {
                        // We use OrderEvent.PROCESS
                        machine.fire(OrderEvent.PROCESS);
                    }
                }

                public class InventoryService {
                    private StateMachine machine;

                    public void updateInventory() {
                        // We use InternalEvent.PROCESS which has same simple name
                        machine.fireInternal(InternalEvent.PROCESS);
                    }

                    public void wrongProcess() {
                        // This calls fire(OrderEvent) but with InternalEvent.PROCESS
                        // It should be REJECTED by type-aware heuristic matching if it matches fire(OrderEvent)
                        machine.fire(InternalEvent.PROCESS);
                    }
                }

                class StateMachine {
                    // Two overloaded methods or methods in different classes (simulated by names)
                    public void fire(OrderEvent event) {}
                    public void fireInternal(InternalEvent event) {}
                }

                enum OrderEvent { PROCESS, CANCEL }
                enum InternalEvent { PROCESS, AUDIT }
                """;

        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_collision");
        java.nio.file.Files.writeString(tempDir.resolve("CollisionConfig.java"), source);

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

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        // Scenario 1: Trace OrderService.processOrder
        EntryPoint entryPoint1 = EntryPoint.builder()
                .className("com.example.OrderService")
                .methodName("processOrder")
                .build();

        TriggerPoint trigger1 = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains1 = engine.findChains(List.of(entryPoint1), List.of(trigger1));

        assertThat(chains1).hasSize(1);
        assertThat(chains1.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactly("OrderEvent.PROCESS")
                .doesNotContain("InternalEvent.PROCESS");

        // Scenario 2: Trace InventoryService.updateInventory
        EntryPoint entryPoint2 = EntryPoint.builder()
                .className("com.example.InventoryService")
                .methodName("updateInventory")
                .build();

        TriggerPoint trigger2 = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fireInternal")
                .event("event")
                .build();

        List<CallChain> chains2 = engine.findChains(List.of(entryPoint2), List.of(trigger2));

        assertThat(chains2).hasSize(1);
        assertThat(chains2.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactly("InternalEvent.PROCESS")
                .doesNotContain("OrderEvent.PROCESS");
    }
}
