package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Path;

import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.*;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Broader coverage for allocation-sensitive {@code getType()} beyond the happy-path fixture.
 */
class AllocationSensitiveEdgeCasesPipelineTest {

    private static final String MACHINE_CONFIG = "com.example.OrderStateMachineConfig";
    private static final String EVENT_TYPE = "com.example.OrderEvent";
    private static final String STATE_TYPE = "com.example.OrderState";

    private static final String COMMON_TAIL = """
            class StateMachine { public void sendEvent(OrderEvent e) {} }
            enum OrderEvent { PAY, SHIP, CANCEL }
            enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
            class OrderStateMachineConfig {}
            """;

    @Test
    void genericRichEventCarrierWithNamePeel(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent<?> e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            interface RichEvent<T> { String getType(); }
            class PayRichEvent implements RichEvent<OrderEvent> {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent<OrderEvent> {
                public String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void localVarThenPassThroughParam(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() {
                    RichEvent e = new PayRichEvent();
                    dispatcher.fire(e);
                }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            interface RichEvent { String getType(); }
            class PayRichEvent implements RichEvent {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void twoHopParamForward(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Facade facade;
                public void pay() { facade.accept(new PayRichEvent()); }
            }
            class Facade {
                Dispatcher dispatcher;
                void accept(RichEvent e) { dispatcher.fire(e); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            interface RichEvent { String getType(); }
            class PayRichEvent implements RichEvent {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void enumReturningGetTypeWithoutValueOf(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(e.getType());
                }
            }
            interface RichEvent { OrderEvent getType(); }
            class PayRichEvent implements RichEvent {
                public OrderEvent getType() { return OrderEvent.PAY; }
            }
            class ShipRichEvent implements RichEvent {
                public OrderEvent getType() { return OrderEvent.SHIP; }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void getEventAliasAccessor(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(e.getEvent());
                }
            }
            interface RichEvent { OrderEvent getEvent(); }
            class PayRichEvent implements RichEvent {
                public OrderEvent getEvent() { return OrderEvent.PAY; }
            }
            class ShipRichEvent implements RichEvent {
                public OrderEvent getEvent() { return OrderEvent.SHIP; }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void abstractBaseGetTypeOverride(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(BaseEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            abstract class BaseEvent { abstract String getType(); }
            class PayRichEvent extends BaseEvent {
                String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent extends BaseEvent {
                String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    /**
     * Factory return {@code Events.pay() → new PayRichEvent()} must propagate through the
     * interface param to a unique {@code getType()} constant.
     */
    @Test
    void factoryMethodReturningConcreteEvent(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(Events.pay()); }
            }
            class Events {
                static RichEvent pay() { return new PayRichEvent(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            interface RichEvent { String getType(); }
            class PayRichEvent implements RichEvent {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    /**
     * Wrapper getter peel: {@code new EventWrapper(new PayRichEvent()).getEvent().getType()}.
     */
    @Test
    void wrapperGetterChainCarrier(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new EventWrapper(new PayRichEvent())); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(EventWrapper w) {
                    machine.sendEvent(OrderEvent.valueOf(w.getEvent().getType()));
                }
            }
            class EventWrapper {
                private final RichEvent event;
                EventWrapper(RichEvent event) { this.event = event; }
                RichEvent getEvent() { return event; }
            }
            interface RichEvent { String getType(); }
            class PayRichEvent implements RichEvent {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getType() { return OrderEvent.SHIP.name(); }
            }
            """ + COMMON_TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void mysteryRuntimeBranchStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void mystery() { dispatcher.fire(new MysteryRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType()));
                }
            }
            interface RichEvent { String getType(); }
            class MysteryRichEvent implements RichEvent {
                public String getType() {
                    return Math.random() > 0.5 ? OrderEvent.PAY.name() : OrderEvent.SHIP.name();
                }
            }
            class PayRichEvent implements RichEvent {
                public String getType() { return OrderEvent.PAY.name(); }
            }
            """ + COMMON_TAIL;
        assertFailClosed(source, "mystery", tempDir);
    }

    private static void assertResolvedPay(String source, Path tempDir) throws Exception {
        CodebaseContext context = scanSource(source, tempDir);
        Transition pay = transition("NEW", "PAID", EVENT_TYPE + ".PAY");
        Transition ship = transition("PAID", "SHIPPED", EVENT_TYPE + ".SHIP");
        Transition cancel = transition("NEW", "CANCELLED", EVENT_TYPE + ".CANCEL");
        CallChain linked = resolveLinkAndAssertSingleMatch(
                context,
                "com.example.ApiController",
                "pay",
                "com.example.StateMachine",
                "sendEvent",
                MACHINE_CONFIG,
                EVENT_TYPE,
                STATE_TYPE,
                EVENT_TYPE + ".PAY",
                EngineKind.JDT,
                pay,
                ship,
                cancel);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getTriggerPoint().getPolymorphicEvents())
                .filteredOn(pe -> pe != null && (pe.contains("PAY") || pe.contains("SHIP") || pe.contains("CANCEL")))
                .allMatch(pe -> pe.contains("PAY"));
    }

    private static void assertFailClosed(String source, String entryMethod, Path tempDir) throws Exception {
        CodebaseContext context = scanSource(source, tempDir);
        Transition pay = transition("NEW", "PAID", EVENT_TYPE + ".PAY");
        Transition ship = transition("PAID", "SHIPPED", EVENT_TYPE + ".SHIP");
        CallChain raw = resolveChain(context, "com.example.ApiController", entryMethod,
                "com.example.StateMachine", "sendEvent", EngineKind.JDT);
        CallChain linked = linkChain(context, raw, MACHINE_CONFIG, EVENT_TYPE, STATE_TYPE, pay, ship);
        assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
        assertThat(linked.getLinkResolution())
                .isIn(LinkResolution.AMBIGUOUS_WIDEN, LinkResolution.NO_MATCH, LinkResolution.UNRESOLVED_EXTERNAL);
    }
}
