package click.kamil.springstatemachineexporter.analysis.pipeline;

import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
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 static org.assertj.core.api.Assertions.assertThat;

class MessageCarrierSupportTest {

    @Test
    void peelsWithPayload(@TempDir Path tempDir) throws Exception {
        Expression sendArg = sendArg(tempDir, """
                package com.example;
                class C {
                  void fire() {
                    send(MessageBuilder.withPayload(OrderEvent.PAY).build());
                  }
                  void send(Object e) {}
                }
                enum OrderEvent { PAY }
                class MessageBuilder {
                  static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                  Object build() { return null; }
                }
                """);
        Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
        assertThat(peeled).isNotNull();
        assertThat(peeled.toString()).contains("PAY");
    }

    @Test
    void peelsGetPayloadOnBuilderChain(@TempDir Path tempDir) throws Exception {
        Expression sendArg = sendArg(tempDir, """
                package com.example;
                class C {
                  void fire() {
                    send(MessageBuilder.withPayload(OrderEvent.PAY).build().getPayload());
                  }
                  void send(Object e) {}
                }
                enum OrderEvent { PAY }
                class MessageBuilder {
                  static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                  MessageBuilder build() { return this; }
                  Object getPayload() { return null; }
                }
                """);
        Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
        assertThat(peeled).isNotNull();
        assertThat(peeled.toString()).contains("PAY");
    }

    @Test
    void prefersAllowlistedHeaderOverPayload(@TempDir Path tempDir) throws Exception {
        Expression sendArg = sendArg(tempDir, """
                package com.example;
                class C {
                  void fire() {
                    Object dto = new Object();
                    send(MessageBuilder.withPayload(dto).setHeader("event", OrderEvent.PAY).build());
                  }
                  void send(Object e) {}
                }
                enum OrderEvent { PAY }
                class MessageBuilder {
                  static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                  MessageBuilder setHeader(String k, Object v) { return this; }
                  Object build() { return null; }
                }
                """);
        Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
        assertThat(peeled).isNotNull();
        assertThat(peeled.toString()).contains("PAY");
        assertThat(MessageCarrierSupport.findSetHeaderValueOnChain(sendArg, "event").toString())
                .contains("PAY");
    }

    @Test
    void freeFormHeaderKeyIgnored(@TempDir Path tempDir) throws Exception {
        Expression sendArg = sendArg(tempDir, """
                package com.example;
                class C {
                  void fire() {
                    Object dto = new Object();
                    send(MessageBuilder.withPayload(dto).setHeader("customKey", OrderEvent.PAY).build());
                  }
                  void send(Object e) {}
                }
                enum OrderEvent { PAY }
                class MessageBuilder {
                  static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
                  MessageBuilder setHeader(String k, Object v) { return this; }
                  Object build() { return null; }
                }
                """);
        assertThat(MessageCarrierSupport.findSetHeaderValueOnChain(sendArg, null)).isNull();
        Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
        // Falls through to dto payload — not the header const.
        assertThat(peeled).isNotNull();
        assertThat(peeled.toString()).doesNotContain("PAY");
    }

    @Test
    void peelsGenericMessageCtor(@TempDir Path tempDir) throws Exception {
        Expression sendArg = sendArg(tempDir, """
                package com.example;
                class C {
                  void fire() {
                    send(new GenericMessage<>(OrderEvent.PAY));
                  }
                  void send(Object e) {}
                }
                enum OrderEvent { PAY }
                class GenericMessage<T> { GenericMessage(T payload) {} }
                """);
        Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
        assertThat(peeled).isNotNull();
        assertThat(peeled.toString()).contains("PAY");
    }

    private static Expression sendArg(Path tempDir, String source) throws Exception {
        Files.writeString(tempDir.resolve("C.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        TypeDeclaration td = context.getTypeDeclaration("com.example.C");
        if (td == null) {
            td = context.getTypeDeclaration("C");
        }
        assertThat(td).isNotNull();
        MethodDeclaration fire = context.findMethodDeclaration(td, "fire", true);
        assertThat(fire).isNotNull();
        MethodInvocation[] send = {null};
        fire.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if ("send".equals(node.getName().getIdentifier()) && !node.arguments().isEmpty()) {
                    send[0] = node;
                    return false;
                }
                return super.visit(node);
            }
        });
        assertThat(send[0]).isNotNull();
        return (Expression) send[0].arguments().get(0);
    }
}
