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;

/**
 * Event-identity resolver matrix: getA + local + protected valueOf helper, concrete param, etc.
 */
class EventIdentityResolverPipelineTest {

    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 TAIL = """
            class StateMachine { public void sendEvent(OrderEvent e) {} }
            enum OrderEvent { PAY, SHIP, CANCEL }
            enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
            class OrderStateMachineConfig {}
            """;

    @Test
    void getALocalAndProtectedValueOfHelper(@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;
                protected OrderEvent toEvent(String event) {
                    return OrderEvent.valueOf(event);
                }
                void fire(RichEvent e) {
                    String event = e.getA();
                    machine.sendEvent(toEvent(event));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void sendEventDirectToEventGetA(@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;
                protected OrderEvent toEvent(String event) {
                    return OrderEvent.valueOf(event);
                }
                void fire(RichEvent e) {
                    machine.sendEvent(toEvent(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void concreteParamTypeWithoutCicOnHop(@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(PayRichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void valueOfLocalFromGetA(@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) {
                    String event = e.getA();
                    machine.sendEvent(OrderEvent.valueOf(event));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void crossMethodStringParamForwardsGetA(@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) {
                    route(e.getA());
                }
                void route(String code) {
                    machine.sendEvent(OrderEvent.valueOf(code));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void valueOfHelperWithIntermediateLocal(@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;
                protected OrderEvent toEvent(String event) {
                    String code = event;
                    return OrderEvent.valueOf(code);
                }
                void fire(RichEvent e) {
                    machine.sendEvent(toEvent(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void abstractSuperclassProtectedToEvent(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            abstract class AbstractDispatcher {
                StateMachine machine;
                protected OrderEvent toEvent(String event) {
                    String code = (String) event;
                    return OrderEvent.valueOf(code);
                }
            }
            class Dispatcher extends AbstractDispatcher {
                void fire(RichEvent e) {
                    machine.sendEvent(toEvent(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void machineFireRecoveryWithToEventGetA(@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;
                protected OrderEvent toEvent(String event) {
                    return OrderEvent.valueOf(event);
                }
                void fire(RichEvent e) {
                    machine.fire(toEvent(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            class StateMachine { public void fire(OrderEvent e) {} }
            enum OrderEvent { PAY, SHIP, CANCEL }
            enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
            class OrderStateMachineConfig {}
            """;
        assertResolvedPay(source, tempDir, "fire");
    }

    @Test
    void fieldStoreThenFlushResolves(@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;
                RichEvent carrier;
                void fire(RichEvent e) {
                    this.carrier = e;
                    machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void setterWrapperGetEventResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    EventWrapper w = new EventWrapper();
                    w.setEvent(new PayRichEvent());
                    machine.sendEvent(OrderEvent.valueOf(w.getEvent().getA()));
                }
            }
            class EventWrapper {
                RichEvent event;
                void setEvent(RichEvent e) { this.event = e; }
                RichEvent getEvent() { return event; }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void valueOfTrimAndToUpperCaseResolves(@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) {
                    String code = e.getA().trim().toUpperCase();
                    machine.sendEvent(OrderEvent.valueOf(code));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void toUpperCaseWithLocaleStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.Locale;
            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.getA().toUpperCase(Locale.ROOT)));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        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", "pay",
                "com.example.StateMachine", "sendEvent", EngineKind.JDT);
        CallChain linked = linkChain(context, raw, MACHINE_CONFIG, EVENT_TYPE, STATE_TYPE, pay, ship);
        // Locale arg must not peel; CHA may widen — never unique RESOLVED through Locale peel.
        if (linked.getLinkResolution() == LinkResolution.RESOLVED) {
            assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
        } else {
            assertThat(linked.getLinkResolution())
                    .isIn(LinkResolution.AMBIGUOUS_WIDEN, LinkResolution.NO_MATCH, LinkResolution.UNRESOLVED_EXTERNAL);
        }
    }

    @Test
    void multiReturnSameParamValueOfHelper(@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;
                protected OrderEvent toEvent(String event) {
                    try {
                        return OrderEvent.valueOf(event);
                    } finally {
                        // second return path still valueOf(same param) after alias
                    }
                }
                protected OrderEvent map(String event) {
                    if (event == null) {
                        return OrderEvent.valueOf(event);
                    }
                    return OrderEvent.valueOf(event);
                }
                void fire(RichEvent e) {
                    machine.sendEvent(map(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void compactTypeAccessorResolves(@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.type()));
                }
            }
            interface RichEvent { String type(); }
            class PayRichEvent implements RichEvent {
                public String type() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String type() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void publicFinalIdentityFieldResolves(@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(PayRichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.a));
                }
            }
            class PayRichEvent {
                public final String a = "PAY";
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void valueOfAccessorNamePeelResolves(@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(PayRichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(e.getType().name()));
                }
            }
            class PayRichEvent {
                public OrderEvent getType() { return OrderEvent.PAY; }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void twoArgEnumValueOfResolves(@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(Enum.valueOf(OrderEvent.class, e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void triggerRecoveryResolves(@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.trigger(OrderEvent.valueOf(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            class StateMachine { public void trigger(OrderEvent e) {} }
            enum OrderEvent { PAY, SHIP, CANCEL }
            enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
            class OrderStateMachineConfig {}
            """;
        assertResolvedPay(source, tempDir, "trigger");
    }

    @Test
    void passthroughAssertHelperResolves(@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;
                OrderEvent assertSupported(OrderEvent e) { return e; }
                void fire(RichEvent e) {
                    machine.sendEvent(assertSupported(OrderEvent.valueOf(e.getA())));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void fromCodeSwitchMapResolves(@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;
                OrderEvent fromCode(String code) {
                    switch (code) {
                        case "PAY": return OrderEvent.PAY;
                        case "SHIP": return OrderEvent.SHIP;
                        case "CANCEL": return OrderEvent.CANCEL;
                        default: throw new IllegalArgumentException(code);
                    }
                }
                void fire(RichEvent e) {
                    machine.sendEvent(fromCode(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return "PAY"; }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return "SHIP"; }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void fromCodeIncompleteMapStaysFailClosed(@TempDir Path tempDir) throws Exception {
        // Map has no branch for PAY; helper must not invent a constant (and has no enum returns for CHA).
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                OrderEvent fromCode(String code) {
                    if ("OTHER".equals(code)) {
                        throw new IllegalArgumentException("other");
                    }
                    throw new IllegalArgumentException(code);
                }
                void fire(RichEvent e) {
                    machine.sendEvent(fromCode(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return "PAY"; }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return "SHIP"; }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void ternarySameConstBothArmsResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                boolean flag;
                void fire() {
                    machine.sendEvent(flag ? OrderEvent.PAY : OrderEvent.PAY);
                }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void ternaryDifferentArmsStaysFailClosed(@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;
                boolean flag;
                void fire(RichEvent e) {
                    machine.sendEvent(flag ? OrderEvent.PAY : OrderEvent.SHIP);
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void deepStringHopsResolves(@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) {
                    route(e.getA());
                }
                void route(String code) {
                    dispatch(code);
                }
                void dispatch(String code) {
                    machine.sendEvent(OrderEvent.valueOf(code));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void optionalOfGetPeelResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.Optional;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new PayRichEvent()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(RichEvent e) {
                    machine.sendEvent(OrderEvent.valueOf(Optional.of(e).get().getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void renameGetCodeAndMapEventStillResolves(@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;
                protected OrderEvent mapEvent(String event) {
                    return OrderEvent.valueOf(event);
                }
                void fire(RichEvent e) {
                    String code = e.getCode();
                    machine.sendEvent(mapEvent(code));
                }
            }
            interface RichEvent { String getCode(); }
            class PayRichEvent implements RichEvent {
                public String getCode() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getCode() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void dualRouteIdenticalArgsStillResolves(@TempDir Path tempDir) throws Exception {
        // Two AST sites / edges with the same proven arg — must agree, not pick-any into Ship.
        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) {
                    String code = e.getA();
                    route(code);
                    route(code);
                }
                void route(String code) {
                    machine.sendEvent(OrderEvent.valueOf(code));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void multiFieldWriteStaysFailClosed(@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;
                RichEvent carrier;
                void fire(RichEvent e) {
                    this.carrier = e;
                    this.carrier = new ShipRichEvent();
                    machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void multiSetterWriteStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    EventWrapper w = new EventWrapper();
                    w.setEvent(new PayRichEvent());
                    w.setEvent(new ShipRichEvent());
                    machine.sendEvent(OrderEvent.valueOf(w.getEvent().getA()));
                }
            }
            class EventWrapper {
                RichEvent event;
                void setEvent(RichEvent e) { this.event = e; }
                RichEvent getEvent() { return event; }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void replaceStringTransformStaysFailClosed(@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.getA().replace("X", "")));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void sendEventMonoRecoveryResolves(@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.sendEventMono(OrderEvent.valueOf(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            class StateMachine { public void sendEventMono(OrderEvent e) {} }
            enum OrderEvent { PAY, SHIP, CANCEL }
            enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
            class OrderStateMachineConfig {}
            """;
        assertResolvedPay(source, tempDir, "sendEventMono");
    }

    @Test
    void recoveryDisagreeingUniqueProofsStaysFailClosed(@TempDir Path tempDir) throws Exception {
        // Single sendEvent chain, but recovery sees two unique disagreeing valueOf proofs.
        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) {
                    OrderEvent.valueOf(new ShipRichEvent().getA());
                    machine.sendEvent(OrderEvent.valueOf(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void opaqueExternalValidateStaysFailClosed(@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) {
                    OrderEvent resolved = UnknownExternalClass.validate(e.getA());
                    machine.sendEvent(resolved);
                }
            }
            class UnknownExternalClass {
                static OrderEvent validate(String code) { return null; }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void noAllocFieldStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void dispatch() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                RichEvent carrier;
                protected OrderEvent toEvent(String event) {
                    return OrderEvent.valueOf(event);
                }
                void fire() {
                    machine.sendEvent(toEvent(carrier.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "dispatch");
    }

    @Test
    void instanceofPatternNarrowingResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new Object()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(Object o) {
                    if (o instanceof PayRichEvent p) {
                        machine.sendEvent(OrderEvent.valueOf(p.getA()));
                    }
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

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

    @Test
    void sendOutsideInstanceofStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new Object()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(Object o) {
                    if (o instanceof PayRichEvent p) {
                        // narrowed but unused
                    }
                    machine.sendEvent(OrderEvent.valueOf(((RichEvent) o).getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void literalSupplierLambdaResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.function.Supplier;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(() -> OrderEvent.PAY); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(Supplier<OrderEvent> provider) {
                    machine.sendEvent(provider.get());
                }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void opaqueSupplierFieldStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.function.Supplier;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                Supplier<OrderEvent> provider;
                void fire() {
                    machine.sendEvent(provider.get());
                }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void messageBuilderWithPayloadResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    machine.sendEvent(MessageBuilder.withPayload(OrderEvent.PAY).build());
                }
            }
            class MessageBuilder {
                static MessageBuilder withPayload(OrderEvent e) { return new MessageBuilder(); }
                MessageBuilder setHeader(String k, Object v) { return this; }
                OrderEvent build() { return null; }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void genericMessageCtorPayloadResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    machine.sendEvent(new GenericMessage<>(OrderEvent.PAY));
                }
            }
            class GenericMessage<T> {
                GenericMessage(T payload) {}
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void messageBuilderEventHeaderResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    Object dto = new Object();
                    machine.sendEvent(MessageBuilder.withPayload(dto).setHeader("event", OrderEvent.PAY).build());
                }
            }
            class MessageBuilder {
                static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                MessageBuilder setHeader(String k, Object v) { return this; }
                OrderEvent build() { return null; }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void freeFormHeaderKeyStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    Object dto = new Object();
                    var msg = MessageBuilder.withPayload(dto).setHeader("customKey", OrderEvent.PAY).build();
                    machine.sendEvent((OrderEvent) msg.getHeaders().get("customKey"));
                }
            }
            class MessageBuilder {
                static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                MessageBuilder setHeader(String k, Object v) { return this; }
                Message build() { return new Message(); }
            }
            class Message {
                Headers getHeaders() { return new Headers(); }
            }
            class Headers {
                Object get(String k) { return null; }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void switchExpressionFromCodeResolves(@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;
                OrderEvent fromCode(String code) {
                    return switch (code) {
                        case "PAY" -> OrderEvent.PAY;
                        case "SHIP" -> OrderEvent.SHIP;
                        default -> throw new IllegalArgumentException();
                    };
                }
                void fire(RichEvent e) {
                    machine.sendEvent(fromCode(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void switchExpressionIncompleteStaysFailClosed(@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;
                OrderEvent fromCode(String code) {
                    return switch (code) {
                        case "SHIP" -> OrderEvent.SHIP;
                        default -> throw new IllegalArgumentException();
                    };
                }
                void fire(RichEvent e) {
                    machine.sendEvent(fromCode(e.getA()));
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

    @Test
    void messageGetPayloadResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    var msg = MessageBuilder.withPayload(OrderEvent.PAY).build();
                    machine.sendEvent(msg.getPayload());
                }
            }
            class MessageBuilder {
                static MessageBuilder withPayload(OrderEvent e) { return new MessageBuilder(); }
                Message build() { return new Message(OrderEvent.PAY); }
            }
            class Message {
                OrderEvent payload;
                Message(OrderEvent p) { this.payload = p; }
                OrderEvent getPayload() { return payload; }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void guardedInstanceofPatternResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(new Object()); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire(Object o) {
                    if (o instanceof PayRichEvent p && p.getA() != null) {
                        machine.sendEvent(OrderEvent.valueOf(p.getA()));
                    }
                }
            }
            interface RichEvent { String getA(); }
            class PayRichEvent implements RichEvent {
                public String getA() { return OrderEvent.PAY.name(); }
            }
            class ShipRichEvent implements RichEvent {
                public String getA() { return OrderEvent.SHIP.name(); }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

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

    @Test
    void methodReferenceSupplierResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.function.Supplier;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(dispatcher::payConst); }
            }
            class Dispatcher {
                StateMachine machine;
                OrderEvent payConst() { return OrderEvent.PAY; }
                void fire(Supplier<OrderEvent> provider) {
                    machine.sendEvent(provider.get());
                }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void optionalOrElseSameConstResolves(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.Optional;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    machine.sendEvent(Optional.of(OrderEvent.PAY).orElse(OrderEvent.PAY));
                }
            }
            """ + TAIL;
        assertResolvedPay(source, tempDir);
    }

    @Test
    void optionalOrElseDisagreeStaysFailClosed(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            import java.util.Optional;
            public class ApiController {
                Dispatcher dispatcher;
                public void pay() { dispatcher.fire(); }
            }
            class Dispatcher {
                StateMachine machine;
                void fire() {
                    machine.sendEvent(Optional.of(OrderEvent.PAY).orElse(OrderEvent.SHIP));
                }
            }
            """ + TAIL;
        assertFailClosed(source, tempDir, "pay");
    }

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

    private static void assertFailClosed(String source, Path tempDir, String entryMethod) 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);
    }

    private static void assertResolvedPay(String source, Path tempDir) throws Exception {
        assertResolvedPay(source, tempDir, "sendEvent");
    }

    private static void assertResolvedPay(String source, Path tempDir, String triggerMethod) 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",
                triggerMethod,
                MACHINE_CONFIG,
                EVENT_TYPE,
                STATE_TYPE,
                EVENT_TYPE + ".PAY",
                EngineKind.JDT,
                pay,
                ship,
                cancel);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
    }
}
