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

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 InstanceofNarrowerTest {

    @Test
    void patternVariableProvesConcrete(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent p) {
                      use(p.getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                class ShipRichEvent { String getA() { return "SHIP"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.uniqueOrNull()).isNotNull();
        assertThat(facts.uniqueOrNull().typeFqn()).contains("PayRichEvent");
    }

    @Test
    void thenBranchClassicInstanceofProvesConcrete(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent) {
                      use(((PayRichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                class ShipRichEvent { String getA() { return "SHIP"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.uniqueOrNull()).isNotNull();
        assertThat(facts.uniqueOrNull().typeFqn()).contains("PayRichEvent");
    }

    @Test
    void outsideThenStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent p) {}
                    use(((PayRichEvent) o).getA());
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    @Test
    void refineObjectCicWithInstanceof(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent) {
                      use(((PayRichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts base = ReceiverFacts.of(new AllocationSite("java.lang.Object"));
        ReceiverFacts refined = InstanceofNarrower.refine(base, getA.getExpression(), getA, f.context);
        assertThat(refined.uniqueOrNull().typeFqn()).contains("PayRichEvent");
    }

    @Test
    void guardedAndInstanceofProvesConcrete(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent p && p.getA() != null) {
                      use(p.getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.uniqueOrNull()).isNotNull();
        assertThat(facts.uniqueOrNull().typeFqn()).contains("PayRichEvent");
    }

    @Test
    void castToConcreteRefinesWeakBase(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    use(((PayRichEvent) o).getA());
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts base = ReceiverFacts.of(new AllocationSite("java.lang.Object"));
        ReceiverFacts refined = InstanceofNarrower.refine(base, getA.getExpression(), getA, f.context);
        assertThat(refined.uniqueOrNull().typeFqn()).contains("PayRichEvent");
    }

    @Test
    void refineConcreteConflictFailsClosed(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent) {
                      use(((PayRichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                class ShipRichEvent { String getA() { return "SHIP"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts base = ReceiverFacts.of(new AllocationSite("com.example.ShipRichEvent"));
        ReceiverFacts refined = InstanceofNarrower.refine(base, getA.getExpression(), getA, f.context);
        assertThat(refined.isEmpty()).isTrue();
    }

    @Test
    void elseBranchStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof PayRichEvent) {
                    } else {
                      use(((PayRichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    @Test
    void interfaceInstanceofTargetStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof RichEvent) {
                      use(((RichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                interface RichEvent { String getA(); }
                class PayRichEvent implements RichEvent { public String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    @Test
    void abstractInstanceofTargetStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o) {
                    if (o instanceof BaseEvent) {
                      use(((BaseEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                abstract class BaseEvent { abstract String getA(); }
                class PayRichEvent extends BaseEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    @Test
    void nonSimpleNameReceiverStaysEmpty(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  Holder holder;
                  void fire() {
                    if (holder.getEvent() instanceof PayRichEvent) {
                      use(((PayRichEvent) holder.getEvent()).getA());
                    }
                  }
                  void use(String s) {}
                }
                class Holder { Object getEvent() { return null; } }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    @Test
    void orGuardInstanceofStaysFailClosed(@TempDir Path tempDir) throws Exception {
        Fixture f = scan(tempDir, """
                package com.example;
                class C {
                  void fire(Object o, boolean flag) {
                    if (flag || o instanceof PayRichEvent) {
                      use(((PayRichEvent) o).getA());
                    }
                  }
                  void use(String s) {}
                }
                class PayRichEvent { String getA() { return "PAY"; } }
                """);
        MethodInvocation getA = findInvocation(f.fire, "getA");
        ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
        assertThat(facts.isEmpty()).isTrue();
    }

    private static Fixture scan(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();
        return new Fixture(context, fire);
    }

    private static MethodInvocation findInvocation(MethodDeclaration method, String name) {
        MethodInvocation[] found = {null};
        method.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (name.equals(node.getName().getIdentifier())) {
                    found[0] = node;
                    return false;
                }
                return super.visit(node);
            }
        });
        assertThat(found[0]).as("expected invocation %s", name).isNotNull();
        return found[0];
    }

    private record Fixture(CodebaseContext context, MethodDeclaration fire) {
    }
}
