package click.kamil.springstatemachineexporter.analysis.flow.eventid;

import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationFactPropagator;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.VirtualAccessorConstResolver;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

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

/**
 * Unique vs hint-gated expand split for EventIdentityResolver.
 */
class EventIdentityResolveExpandedTest {

    @Test
    void toUpperCaseExpandsWhileUniqueStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = fixture(tempDir, """
                package com.example;
                class C {
                  void process(String action) {
                    send(OrderEvent.valueOf(action.toUpperCase()));
                  }
                  void send(OrderEvent e) {}
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                """);
        List<String> unique = f.resolver.resolveUnique(f.valueOfCall, List.of("com.example.C.process"), Map.of());
        ExpandConsult consult = f.resolver.resolveExpandConsult(
                f.valueOfCall, List.of("com.example.C.process"), Map.of());
        assertThat(unique).isEmpty();
        assertThat(consult.source()).isEqualTo(ExpandConsult.Source.ENUM_EXPAND);
        assertThat(consult.licensedByHint()).isTrue();
        assertThat(consult.members()).containsExactlyInAnyOrder(
                "OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void localeToUpperCaseStillExpands(@TempDir Path tempDir) throws Exception {
        Fixture f = fixture(tempDir, """
                package com.example;
                import java.util.Locale;
                class C {
                  void process(String action) {
                    send(OrderEvent.valueOf(action.toUpperCase(Locale.ROOT)));
                  }
                  void send(OrderEvent e) {}
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                """);
        assertThat(f.resolver.resolveUnique(f.valueOfCall, List.of("com.example.C.process"), Map.of())).isEmpty();
        assertThat(f.resolver.resolveExpanded(f.valueOfCall, List.of("com.example.C.process"), Map.of()))
                .containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void plainValueOfStaysEmptyForUniqueAndExpanded(@TempDir Path tempDir) throws Exception {
        Fixture f = fixture(tempDir, """
                package com.example;
                class C {
                  void process(String action) {
                    send(OrderEvent.valueOf(action));
                  }
                  void send(OrderEvent e) {}
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                """);
        assertThat(f.resolver.resolveUnique(f.valueOfCall, List.of("com.example.C.process"), Map.of())).isEmpty();
        ExpandConsult consult = f.resolver.resolveExpandConsult(
                f.valueOfCall, List.of("com.example.C.process"), Map.of());
        assertThat(consult.source()).isEqualTo(ExpandConsult.Source.EMPTY);
        assertThat(consult.members()).isEmpty();
    }

    @Test
    void allocProvenUniqueAlsoReturnedByExpanded(@TempDir Path tempDir) throws Exception {
        Fixture f = fixture(tempDir, """
                package com.example;
                class C {
                  void pay() {
                    fire(new PayRichEvent());
                  }
                  void fire(RichEvent e) {
                    send(OrderEvent.valueOf(e.getA()));
                  }
                  void send(OrderEvent e) {}
                }
                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(); }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                """);
        List<String> path = List.of("com.example.C.pay", "com.example.C.fire");
        Map<String, List<CallEdge>> callGraph = Map.of(
                "com.example.C.pay",
                List.of(new CallEdge("com.example.C.fire", List.of("new PayRichEvent()"))));
        List<String> unique = f.resolver.resolveUnique(f.valueOfCall, path, callGraph);
        List<String> expanded = f.resolver.resolveExpanded(f.valueOfCall, path, callGraph);
        assertThat(unique).containsExactly("PAY");
        assertThat(expanded).containsExactly("OrderEvent.PAY");
        ExpandConsult consult = f.resolver.resolveExpandConsult(f.valueOfCall, path, callGraph);
        assertThat(consult.source()).isEqualTo(ExpandConsult.Source.UNIQUE);
        assertThat(consult.licensedByHint()).isFalse();
        assertThat(consult.adoptableAsPolyExpand()).isFalse();
        assertThat(consult.isUniqueProof()).isTrue();
        assertThat(consult.members()).containsExactly("OrderEvent.PAY");
        assertThat(EventIdentityNormalize.sameConstant(unique.get(0), consult.members().get(0))).isTrue();
    }

    @Test
    void helperReturnNameExpandsViaConsult(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("C.java"), """
                package com.example;
                abstract class AbstractOrderService {
                  public void process(String event) {
                    String resolved = assertSupportedOrderEvent(event);
                    sendEvent(resolved);
                  }
                  protected abstract String assertSupportedOrderEvent(String event);
                  public void sendEvent(String ev) {}
                }
                class LogisticsService extends AbstractOrderService {
                  @Override
                  protected String assertSupportedOrderEvent(String event) {
                    return LogisticsEvent.valueOf(event).name();
                  }
                }
                enum LogisticsEvent { DELIVERED, RETURNED }
                """);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        ConstantResolver cr = context.getConstantResolver();
        VariableTracer vt = new VariableTracer(context, cr);
        ConstructorAnalyzer ctor = new ConstructorAnalyzer(context, cr);
        AllocationFactPropagator afp = new AllocationFactPropagator(context, vt, ctor);
        ConstantExtractor extractor = new ConstantExtractor(context, cr, mi -> null);
        AccessorResolver accessors = new AccessorResolver(context, extractor, ctor, cr);
        VirtualAccessorConstResolver vacr = new VirtualAccessorConstResolver(context, accessors, cr);
        EventIdentityResolver resolver = new EventIdentityResolver(context, afp, vacr, vt, cr);
        TypeDeclaration td = context.getTypeDeclaration("com.example.AbstractOrderService");
        assertThat(td).isNotNull();
        Expression sendArg = null;
        for (MethodDeclaration md : td.getMethods()) {
            if (!"process".equals(md.getName().getIdentifier()) || md.getBody() == null) {
                continue;
            }
            Expression[] found = {null};
            md.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    if ("sendEvent".equals(node.getName().getIdentifier()) && !node.arguments().isEmpty()) {
                        found[0] = (Expression) node.arguments().get(0);
                        return false;
                    }
                    return super.visit(node);
                }
            });
            sendArg = found[0];
        }
        assertThat(sendArg).isNotNull();
        List<String> path = List.of(
                "com.example.LogisticsService.process",
                "com.example.AbstractOrderService.sendEvent");
        ExpandConsult consult = resolver.resolveExpandConsult(sendArg, path, Map.of());
        assertThat(consult.source()).isEqualTo(ExpandConsult.Source.ENUM_EXPAND);
        assertThat(consult.licensedByHint()).isTrue();
        assertThat(consult.members()).containsExactlyInAnyOrder(
                "LogisticsEvent.DELIVERED", "LogisticsEvent.RETURNED");
        assertThat(consult.adoptableAsPolyExpand()).isTrue();
    }

    @Test
    void helperExpandStaysEmptyWhenOnlyAbstractOnPath(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("C.java"), """
                package com.example;
                abstract class AbstractOrderService {
                  public void process(String event) {
                    String resolved = assertSupportedOrderEvent(event);
                    sendEvent(resolved);
                  }
                  protected abstract String assertSupportedOrderEvent(String event);
                  public void sendEvent(String ev) {}
                }
                class LogisticsService extends AbstractOrderService {
                  @Override
                  protected String assertSupportedOrderEvent(String event) {
                    return LogisticsEvent.valueOf(event).name();
                  }
                }
                class ComputerStoreService extends AbstractOrderService {
                  @Override
                  protected String assertSupportedOrderEvent(String event) {
                    return ComputerEvent.valueOf(event).name();
                  }
                }
                enum LogisticsEvent { DELIVERED, RETURNED }
                enum ComputerEvent { SHIPPED, REFUNDED }
                """);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        ConstantResolver cr = context.getConstantResolver();
        VariableTracer vt = new VariableTracer(context, cr);
        ConstructorAnalyzer ctor = new ConstructorAnalyzer(context, cr);
        AllocationFactPropagator afp = new AllocationFactPropagator(context, vt, ctor);
        ConstantExtractor extractor = new ConstantExtractor(context, cr, mi -> null);
        AccessorResolver accessors = new AccessorResolver(context, extractor, ctor, cr);
        VirtualAccessorConstResolver vacr = new VirtualAccessorConstResolver(context, accessors, cr);
        EventIdentityResolver resolver = new EventIdentityResolver(context, afp, vacr, vt, cr);
        TypeDeclaration td = context.getTypeDeclaration("com.example.AbstractOrderService");
        Expression sendArg = null;
        for (MethodDeclaration md : td.getMethods()) {
            if (!"process".equals(md.getName().getIdentifier()) || md.getBody() == null) {
                continue;
            }
            Expression[] found = {null};
            md.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    if ("sendEvent".equals(node.getName().getIdentifier()) && !node.arguments().isEmpty()) {
                        found[0] = (Expression) node.arguments().get(0);
                        return false;
                    }
                    return super.visit(node);
                }
            });
            sendArg = found[0];
        }
        assertThat(sendArg).isNotNull();
        // Abstract frames only — must not flatten Logistics+Computer overrides.
        List<String> abstractOnlyPath = List.of(
                "com.example.AbstractOrderService.process",
                "com.example.AbstractOrderService.sendEvent");
        ExpandConsult miss = resolver.resolveExpandConsult(sendArg, abstractOnlyPath, Map.of());
        assertThat(miss.source()).isEqualTo(ExpandConsult.Source.EMPTY);
        assertThat(miss.adoptableAsPolyExpand()).isFalse();
    }

    private static Fixture fixture(Path tempDir, String source) throws Exception {
        Files.writeString(tempDir.resolve("C.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        ConstantResolver cr = context.getConstantResolver();
        VariableTracer vt = new VariableTracer(context, cr);
        ConstructorAnalyzer ctor = new ConstructorAnalyzer(context, cr);
        AllocationFactPropagator afp = new AllocationFactPropagator(context, vt, ctor);
        ConstantExtractor extractor = new ConstantExtractor(context, cr, mi -> null);
        AccessorResolver accessors = new AccessorResolver(context, extractor, ctor, cr);
        VirtualAccessorConstResolver vacr = new VirtualAccessorConstResolver(context, accessors, cr);
        EventIdentityResolver resolver = new EventIdentityResolver(context, afp, vacr, vt, cr);
        TypeDeclaration td = context.getTypeDeclaration("com.example.C");
        if (td == null) {
            td = context.getTypeDeclaration("C");
        }
        assertThat(td).isNotNull();
        MethodInvocation valueOf = null;
        for (MethodDeclaration md : td.getMethods()) {
            MethodInvocation[] found = {null};
            md.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    if ("valueOf".equals(node.getName().getIdentifier())) {
                        found[0] = node;
                        return false;
                    }
                    return super.visit(node);
                }
            });
            if (found[0] != null) {
                valueOf = found[0];
                break;
            }
        }
        assertThat(valueOf).isNotNull();
        return new Fixture(resolver, valueOf);
    }

    private record Fixture(EventIdentityResolver resolver, MethodInvocation valueOfCall) {
    }
}
