/*
 * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8205418 8207229 8207230 8230847 8245786 8247334 8248641 8240658 8246774 8274347 8347989
 * @summary Test the outcomes from Trees.getScope
 * @modules jdk.compiler/com.sun.tools.javac.api
 *          jdk.compiler/com.sun.tools.javac.comp
 *          jdk.compiler/com.sun.tools.javac.tree
 *          jdk.compiler/com.sun.tools.javac.util
 */

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.lang.model.element.Element;
import javax.tools.JavaCompiler;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Scope;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacScope;
import com.sun.tools.javac.api.JavacTaskImpl;

import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.comp.Analyzer;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCCase;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Context.Factory;
import java.util.Objects;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;

import static javax.tools.JavaFileObject.Kind.SOURCE;

public class TestGetScopeResult {
    public static void main(String... args) throws IOException {
        new TestGetScopeResult().run();
        new TestGetScopeResult().testAnalyzerDisabled();
        new TestGetScopeResult().testVariablesInSwitch();
        new TestGetScopeResult().testMemberRefs();
        new TestGetScopeResult().testAnnotations();
        new TestGetScopeResult().testAnnotationsLazy();
        new TestGetScopeResult().testCircular();
        new TestGetScopeResult().testRecord();
        new TestGetScopeResult().testLocalRecordAnnotation();
        new TestGetScopeResult().testRuleCases();
        new TestGetScopeResult().testNestedSwitchExpression();
        new TestGetScopeResult().testModuleImportScope();
        new TestGetScopeResult().testClassTypeSetInEnterGetScope();
    }

    public void run() throws IOException {
        String[] simpleLambda = {
            "s:java.lang.String",
            "i:Test.I",
            "super:java.lang.Object",
            "this:Test"
        };
        doTest("class Test { void test() { I i = s -> { }; } interface I { public void test(String s); } }",
               simpleLambda);
        doTest("class Test { void test() { I i = s -> { }; } interface I { public int test(String s); } }",
               simpleLambda);
        doTest("class Test { void test() { I i = s -> { }; } interface I { public String test(String s); } }",
               simpleLambda);
        doTest("class Test { void test() { I i; inv(s -> { }); } void inv(I i) { } interface I { public void test(String s); } }",
               simpleLambda);
        doTest("class Test { void test() { I i; inv(s -> { }); } void inv(I i) { } interface I { public int test(String s); } }",
               simpleLambda);
        doTest("class Test { void test() { I i; inv(s -> { }); } void inv(I i) { } interface I { public String test(String s); } }",
               simpleLambda);
        String[] dualLambda = {
            "s:java.lang.String",
            "i:Test.I1",
            "super:java.lang.Object",
            "this:Test",
            "s:java.lang.CharSequence",
            "i:Test.I1",
            "super:java.lang.Object",
            "this:Test"
        };
        doTest("class Test { void test() { I1 i; inv(s -> { }, s -> { }); } void inv(I1 i, I2 i) { } interface I1 { public String test(String s); } interface I2 { public void test(CharSequence s); } }",
               dualLambda);
        doTest("class Test { void test() { I1 i; inv(s -> { }, s -> { }); } void inv(I1 i, I2 i) { } interface I1 { public String test(String s); } interface I2 { public int test(CharSequence s); } }",
               dualLambda);
        String[] brokenType = {
            "s:<any>",
            "u:Undefined",
            "super:java.lang.Object",
            "this:Test"
        };
        doTest("class Test { void test() { Undefined u = s -> { }; } }",
               brokenType);
        String[] multipleCandidates1 = {
            "s:<any>",
            "super:java.lang.Object",
            "this:Test"
        };
        doTest("class Test { void test() { cand1(s -> { }); } void cand1(I1 i) { } void cand1(I2 i) { } interface I1 { public String test(String s); } interface I2 { public int test(CharSequence s); } }",
               multipleCandidates1);
        String[] multipleCandidates2 = {
            "s:java.lang.String",
            "super:java.lang.Object",
            "this:Test"
        };
        doTest("class Test { void test() { cand1(s -> { }); } void cand1(I1 i) { } void cand1(I2 i, int i) { } interface I1 { public String test(String s); } interface I2 { public int test(CharSequence s); } }",
               multipleCandidates2);

        String[] implicitExplicitConflict1 = {
            "<error>:t",
            "s:java.lang.String",
            "super:java.lang.Object",
            "this:Test"
        };

        doTest("class Test { void test() { cand((var s, t) -> \"\"); } void cand(I i) { } interface I { public String test(String s); }  }",
               implicitExplicitConflict1);

        String[] implicitExplicitConflict2 = {
            "s:none",
            "<error>:t",
            "super:java.lang.Object",
            "this:Test"
        };

        doTest("class Test { void test() { cand((t, var s) -> \"\"); } void cand(I i) { } interface I { public String test(String s); }  }",
               implicitExplicitConflict2);

        String[] noFunctionInterface = {
            "s:none",
            "<error>:t",
            "super:java.lang.Object",
            "this:Test"
        };

        doTest("class Test { void test() { cand((t, var s) -> \"\"); } void cand(String s) { } }",
               noFunctionInterface);

        String[] invocationInMethodInvocation = {
            "d2:java.lang.Double",
            "d1:java.lang.Double",
            "super:java.lang.Object",
            "this:Test"
        };

        doTest("""
               class Test {
                   void test() { test(reduce(0.0, (d1, d2) -> 0)); }
                   void test(int i) {}
                   <T> T reduce(T t, BiFunction<T, T, T> f1) {}
                   static interface BiFunction<R, P, Q> {
                       R apply(P p, Q q);
                   }
               }""",
               invocationInMethodInvocation);

        String[] infer = {
            "c:java.lang.String",
            "super:java.lang.Object",
            "this:Test"
        };

        doTest("class Test { void test() { cand(\"\", c -> { }); } <T>void cand(T t, I<T> i) { } interface I<T> { public String test(T s); }  }",
               infer);
        doTest("class Test { void test() { cand(\"\", c -> { }); } <T>void cand(T t, I<T> i, int j) { } interface I<T> { public void test(T s); }  }",
               infer);
        doTest("class Test { void test() { cand(\"\", c -> { }); } <T>void cand(T t, I<T> i, int j) { } interface I<T> { public String test(T s); }  }",
               infer);
    }

    public void doTest(String code, String... expected) throws IOException {
        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return code;
                }
            }
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null, List.of(new MyFileObject()));
            CompilationUnitTree cut = t.parse().iterator().next();

            ((JavacTaskImpl)t).enter();

            for (int r = 0; r < 2; r++) {
                List<String> actual = new ArrayList<>();

                new TreePathScanner<Void, Void>() {
                    @Override
                    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
                        Scope scope = Trees.instance(t).getScope(new TreePath(getCurrentPath(), node.getBody()));
                        actual.addAll(dumpScope(scope));
                        return super.visitLambdaExpression(node, p);
                    }
                }.scan(cut, null);

                List<String> expectedList = List.of(expected);

                if (!expectedList.equals(actual)) {
                    throw new IllegalStateException("Unexpected scope content: " + actual + "\n" +
                                                     "expected: " + expectedList);
                }

                t.analyze();
            }
        }
    }

    void testAnalyzerDisabled() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "class Test {" +
                           "    void test() { cand(() -> { System.err.println(); }); }" +
                           "    Runnable r = new Runnable() { public void test() { System.err.println(); } };" +
                           "    void cand(Runnable r) { }" +
                           "}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, List.of("-XDfind=lambda"), null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            TestAnalyzer analyzer = (TestAnalyzer) TestAnalyzer.instance(ctx);

            if (!analyzer.analyzeCalled) {
                throw new IllegalStateException("Analyzer didn't run!");
            }

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
                    analyzer.analyzeCalled = false;
                    Trees.instance(t).getScope(new TreePath(getCurrentPath(), node.getBody()));
                    if (analyzer.analyzeCalled) {
                        throw new IllegalStateException("Analyzer was run during getScope!");
                    }
                    return super.visitLambdaExpression(node, p);
                }

                @Override
                public Void visitVariable(VariableTree node, Void p) {
                    if (node.getInitializer() != null) {
                        analyzer.analyzeCalled = false;
                        TreePath tp = new TreePath(getCurrentPath(), node.getInitializer());
                        Trees.instance(t).getScope(tp);
                        if (analyzer.analyzeCalled) {
                            throw new IllegalStateException("Analyzer was run during getScope!");
                        }
                    }
                    return super.visitVariable(node, p);
                }
            }.scan(cut, null);
        }
    }

    private static final class TestAnalyzer extends Analyzer {

        public static void preRegister(Context context) {
            context.put(analyzerKey, (Factory<Analyzer>) ctx -> new TestAnalyzer(ctx));
        }

        private boolean analyzeCalled;

        public TestAnalyzer(Context context) {
            super(context);
        }

        @Override
        protected void analyze(JCStatement statement, Env<AttrContext> env) {
            analyzeCalled = true;
            super.analyze(statement, env);
        }
    }

    void testVariablesInSwitch() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "class Test {" +
                           "    void test() {\n" +
                           "        E e = E.A;\n" +
                           "        Object o = E.A;\n" +
                           "        switch (e) {\n" +
                           "            case A:\n" +
                           "                return;\n" +
                           "            case B:\n" +
                           "                test();\n" +
                           "                E ee = null;\n" +
                           "                break;\n" +
                           "        }\n" +
                           "    }\n" +
                           "    enum E {A, B}\n" +
                           "}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
                    Trees.instance(t).getScope(getCurrentPath());
                    return super.visitMethodInvocation(node, p);
                }
            }.scan(cut, null);
        }
    }

    void testMemberRefs() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "class Test {" +
                           "    void test() {\n" +
                           "        Test t = this;\n" +
                           "        Runnable r1 = t::test;\n" +
                           "        Runnable r2 = true ? t::test : t::test;\n" +
                           "        c(t::test);\n" +
                           "        c(true ? t::test : t::test);\n" +
                           "    }\n" +
                           "    void c(Runnable r) {}\n" +
                           "}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitConditionalExpression(ConditionalExpressionTree node, Void p) {
                    Trees.instance(t).getScope(new TreePath(getCurrentPath(), node.getCondition()));
                    return super.visitConditionalExpression(node, p);
                }

                @Override
                public Void visitBlock(BlockTree node, Void p) {
                    Trees.instance(t).getScope(getCurrentPath());
                    return super.visitBlock(node, p);
                }
            }.scan(cut, null);
        }
    }

    void testAnnotations() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "class Test {" +
                           "    void test() {\n" +
                           "        new Object() {\n" +
                           "            @A\n" +
                           "            public String t() { return null; }\n" +
                           "        };\n" +
                           "    }\n" +
                           "    @interface A {}\n" +
                           "}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitIdentifier(IdentifierTree node, Void p) {
                    if (node.getName().contentEquals("A")) {
                        Trees.instance(t).getScope(getCurrentPath());
                    }
                    return super.visitIdentifier(node, p);
                }

                @Override
                public Void visitMethod(MethodTree node, Void p) {
                    super.visitMethod(node, p);
                    if (node.getReturnType() != null) {
                        Trees.instance(t).getScope(new TreePath(getCurrentPath(), node.getReturnType()));
                    }
                    return null;
                }
            }.scan(cut, null);
        }
    }

    void testAnnotationsLazy() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "import java.lang.annotation.*;\n" +
                           "\n" +
                           "class ClassA {\n" +
                           "    Object o = ClassB.lcv;\n" +
                           "}\n" +
                           "\n" +
                           "class ClassB {\n" +
                           "    static final String[] lcv = new @TA String[0];\n" +
                           "}\n" +
                           "\n" +
                           "class ClassC {\n" +
                           "    static final Object o = (@TA Object) null;\n" +
                           "}\n" +
                           "\n" +
                           "@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})\n" +
                           "@interface TA {}\n";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            t.addTaskListener(new TaskListener() {
                @Override
                public void finished(TaskEvent e) {
                    if (e.getKind() == TaskEvent.Kind.ANALYZE) {
                        new TreePathScanner<Void, Void>() {
                            @Override
                            public Void scan(Tree tree, Void p) {
                                if (tree != null) {
                                    Trees.instance(t).getScope(new TreePath(getCurrentPath(), tree));
                                }
                                return super.scan(tree, p);
                            }
                        }.scan(Trees.instance(t).getPath(e.getTypeElement()), null);
                    }
                }
            });

            t.call();
        }
    }

    void testCircular() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "class Test extends Test {" +
                           "    {\n" +
                           "        int i;\n" +
                           "    }\n" +
                           "}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitBlock(BlockTree node, Void p) {
                    Trees.instance(t).getScope(getCurrentPath());
                    return super.visitBlock(node, p);
                }
            }.scan(cut, null);
        }
    }

    void testRecord() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return "record Test<T>(int mark) {}";
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            List<String> actual = new ArrayList<>();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitClass(ClassTree node, Void p) {
                    Scope scope = Trees.instance(t).getScope(getCurrentPath());
                    actual.addAll(dumpScope(scope));
                    return super.visitClass(node, p);
                }
            }.scan(cut, null);

            List<String> expected = List.of(
                    "super:java.lang.Record",
                    "this:Test<T>",
                    "T:T"
            );

            if (!expected.equals(actual)) {
                throw new AssertionError("Unexpected Scope content: " + actual);
            }
        }
    }

    void testLocalRecordAnnotation() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            class Variant {
                final String code;
                final List<List<String>> expectedScopeContent;
                public Variant(String code, List<List<String>> expectedScopeContent) {
                    this.code = code;
                    this.expectedScopeContent = expectedScopeContent;
                }
            }
            Variant[] variants = new Variant[] {
                new Variant("""
                            class Test {
                                void t() {
                                    record R(@Annotation int i) {
                                        void stop () {}
                                    }
                                }
                            }
                            @interface Annotation {}
                            """,
                            List.of(
                                List.of("super:java.lang.Object", "this:Test"),
                                List.of("super:java.lang.Object", "this:Test")
                            )),
                new Variant("""
                            record Test(@Annotation int i) {}
                            @interface Annotation {}
                            """,
                            List.of(
                                List.of("i:int", "super:java.lang.Record", "this:Test"),
                                List.of("super:java.lang.Record", "this:Test")
                            ))
            };
            for (Variant currentVariant : variants) {
                class MyFileObject extends SimpleJavaFileObject {
                    MyFileObject() {
                        super(URI.create("myfo:///Test.java"), SOURCE);
                    }
                    @Override
                    public String getCharContent(boolean ignoreEncodingErrors) {
                        return currentVariant.code;
                    }
                }
                Context ctx = new Context();
                TestAnalyzer.preRegister(ctx);
                JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                    List.of(new MyFileObject()), ctx);
                CompilationUnitTree cut = t.parse().iterator().next();
                t.analyze();

                List<List<String>> actual = new ArrayList<>();

                new TreePathScanner<Void, Void>() {
                    @Override
                    public Void visitAnnotation(AnnotationTree node, Void p) {
                        Scope scope = Trees.instance(t).getScope(getCurrentPath());
                        actual.add(dumpScope(scope));
                        return super.visitAnnotation(node, p);
                    }
                }.scan(cut, null);

                if (!currentVariant.expectedScopeContent.equals(actual)) {
                    throw new AssertionError("Unexpected Scope content: " + actual);
                }
            }
        }
    }

    void testRuleCases() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            String code = """
                          class Test {
                              void t(int i) {
                                  long local;
                                  System.err.println(switch (i) {
                                    case 0 -> {
                                        String var;
                                        int scopeHere;
                                        yield "";
                                    }
                                    default -> {
                                        String var;
                                        int scopeHere;
                                        yield "";
                                    }
                                  });
                                  switch (i) {
                                    case 0 -> {
                                        String var;
                                        int scopeHere;
                                    }
                                    default -> {
                                        String var;
                                        int scopeHere;
                                    }
                                  };
                                  switch (i) {
                                    case 0: {
                                        int checkTree;
                                    }
                                  }
                              }
                          }
                          """;
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return code;
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            List<List<String>> actual = new ArrayList<>();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitVariable(VariableTree node, Void p) {
                    if (node.getName().contentEquals("scopeHere")) {
                        Scope scope = Trees.instance(t).getScope(getCurrentPath());
                        actual.add(dumpScope(scope));
                        JCTree body = getCaseBody(scope);
                        if (body == null) {
                            throw new AssertionError("Unexpected null body.");
                        }
                    } else if (node.getName().contentEquals("checkTree")) {
                        Scope scope = Trees.instance(t).getScope(getCurrentPath());
                        JCTree body = getCaseBody(scope);
                        if (body != null) {
                            throw new AssertionError("Unexpected body tree: " + body);
                        }
                    }
                    return super.visitVariable(node, p);
                }
                JCTree getCaseBody(Scope scope) {
                    return ((JCCase) ((JavacScope) scope).getEnv().next.next.tree).body;
                }
            }.scan(cut, null);

            List<List<String>> expected =
                    Collections.nCopies(4,
                                        List.of("scopeHere:int",
                                                "var:java.lang.String",
                                                "local:long",
                                                "i:int",
                                                "super:java.lang.Object",
                                                "this:Test"
                                            ));

            if (!expected.equals(actual)) {
                throw new AssertionError("Unexpected Scope content: " + actual);
            }
        }
    }

    void testNestedSwitchExpression() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            String code = """
                          class Test {
                              void t(Object o1, Object o2) {
                                  System.err.println(switch (o1) {
                                    case String s -> switch (j) {
                                        case Integer i -> {
                                            int scopeHere;
                                            yield "";
                                        }
                                        default -> "";
                                    };
                                    default -> "";
                                  });
                              }
                          }
                          """;
            class MyFileObject extends SimpleJavaFileObject {
                MyFileObject() {
                    super(URI.create("myfo:///Test.java"), SOURCE);
                }
                @Override
                public String getCharContent(boolean ignoreEncodingErrors) {
                    return code;
                }
            }
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(new MyFileObject()), ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            List<List<String>> actual = new ArrayList<>();

            new TreePathScanner<Void, Void>() {
                @Override
                public Void visitVariable(VariableTree node, Void p) {
                    if (node.getName().contentEquals("scopeHere")) {
                        Scope scope = Trees.instance(t).getScope(getCurrentPath());
                        actual.add(dumpScope(scope));
                        JCTree body = getCaseBody(scope);
                        if (body == null) {
                            throw new AssertionError("Unexpected null body.");
                        }
                    }
                    return super.visitVariable(node, p);
                }
                JCTree getCaseBody(Scope scope) {
                    return ((JCCase) ((JavacScope) scope).getEnv().next.next.tree).body;
                }
            }.scan(cut, null);

            List<List<String>> expected =
                    List.of(List.of("scopeHere:int",
                                    "i:java.lang.Integer",
                                    "s:java.lang.String",
                                    "o2:java.lang.Object",
                                    "o1:java.lang.Object",
                                    "super:java.lang.Object",
                                    "this:Test"
                                ));

            if (!expected.equals(actual)) {
                throw new AssertionError("Unexpected Scope content: " + actual);
            }
        }
    }

    void testModuleImportScope() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            String code = """
                          import module java.compiler;
                          import java.util.*;
                          import java.lang.System;
                          class Test {
                          }
                          """;
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavaFileObject input =
                    SimpleJavaFileObject.forSource(URI.create("myfo:///Test.java"), code);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(input),
                                                ctx);
            CompilationUnitTree cut = t.parse().iterator().next();
            t.analyze();

            TreePath topLevelClass = new TreePath(new TreePath(cut), cut.getTypeDecls().get(0));
            Scope scope = Trees.instance(t).getScope(topLevelClass);

            if (scope.getEnclosingClass() == null) {
                throw new AssertionError("Expected an enclosing class.");
            }

            scope = scope.getEnclosingScope();

            if (scope.getEnclosingClass() != null) {
                throw new AssertionError("Did not expect an enclosing class.");
            }

            asssertScopeContainsTypeWithFQN(scope, "java.lang.System");
            asssertScopeContainsTypeWithFQN(scope, "Test");

            scope = scope.getEnclosingScope();

            if (scope.getEnclosingClass() != null) {
                throw new AssertionError("Did not expect an enclosing class.");
            }

            asssertScopeContainsTypeWithFQN(scope, "java.util.List");

            scope = scope.getEnclosingScope();

            if (scope.getEnclosingClass() != null) {
                throw new AssertionError("Did not expect an enclosing class.");
            }

            asssertScopeContainsTypeWithFQN(scope, "javax.tools.ToolProvider");

            if (scope.getEnclosingScope() != null) {
                throw new AssertionError("Did not expect an enclosing scope.");
            }
        }
    }

    //JDK-8347989
    void testClassTypeSetInEnterGetScope() throws IOException {
        JavacTool c = JavacTool.create();
        try (StandardJavaFileManager fm = c.getStandardFileManager(null, null, null)) {
            String code = """
                          import java.util.*;
                          class Test<T extends Test&CharSequence> extends ArrayList
                                                                  implements List {
                              private int test(boolean b) {
                                  int v = b ? test(!b) : 0;
                                  return v;
                              }
                          }
                          """;
            Context ctx = new Context();
            TestAnalyzer.preRegister(ctx);
            JavaFileObject input =
                    SimpleJavaFileObject.forSource(URI.create("myfo:///Test.java"), code);
            JavacTask t = (JavacTask) c.getTask(null, fm, null, null, null,
                                                List.of(input),
                                                ctx);
            Trees trees = Trees.instance(t);
            List<List<String>> actual = new ArrayList<>();

            t.addTaskListener(new TaskListener() {
                @Override
                public void finished(TaskEvent e) {
                    if (e.getKind() != TaskEvent.Kind.ENTER) {
                        return ;
                    }

                    new TreePathScanner<Void, Void>() {
                        @Override
                        public Void visitClass(ClassTree node, Void p) {
                            TypeMirror type = trees.getTypeMirror(getCurrentPath());
                            if (type == null) {
                                throw new AssertionError("Expected class type 'Test', but got: null");
                            }
                            assertEquals(TypeKind.DECLARED, type.getKind());
                            DeclaredType decl = (DeclaredType) type;
                            TypeVariable tvar = (TypeVariable) decl.getTypeArguments().get(0);
                            assertEquals("T", tvar.asElement().getSimpleName().toString());
                            assertEquals("Test&java.lang.CharSequence", tvar.getUpperBound().toString());
                            TypeElement clazz = (TypeElement) decl.asElement();
                            assertEquals("java.util.ArrayList", clazz.getSuperclass().toString().toString());
                            assertEquals("java.util.List", clazz.getInterfaces().toString().toString());
                            return super.visitClass(node, p);
                        }
                        @Override
                        public Void visitReturn(ReturnTree rt, Void p) {
                            Scope scope = trees.getScope(getCurrentPath());
                            actual.add(dumpScope(scope));
                            return super.visitReturn(rt, p);
                        }
                    }.scan(e.getCompilationUnit(), null);
                }
            });

            t.analyze();

            List<List<String>> expected =
                    List.of(List.of("v:int",
                                    "b:boolean",
                                    "super:java.util.ArrayList",
                                    "this:Test<T>",
                                    "T:T"
                                ));

            if (!expected.equals(actual)) {
                throw new AssertionError("Unexpected Scope content: " + actual);
            }
        }
    }

    private List<String> dumpScope(Scope scope) {
        List<String> content = new ArrayList<>();
        while (scope.getEnclosingClass() != null) {
            for (Element el : scope.getLocalElements()) {
                content.add(el.getSimpleName() + ":" +el.asType().toString());
            }
            scope = scope.getEnclosingScope();
        }
        return content;
    }

    private void asssertScopeContainsTypeWithFQN(Scope scope, String fqn) {
        for (TypeElement type : ElementFilter.typesIn(scope.getLocalElements())) {
            if (type.getQualifiedName().contentEquals(fqn)) {
                return ;
            }
        }

        throw new AssertionError("Expected to find: " + fqn +
                                 " in: " + scope.getLocalElements() +
                                 ", but it is missing.");
    }

    private void assertEquals(Object expected, Object actual) {
        if (!Objects.equals(expected, actual)) {
            throw new AssertionError("Expected: '" + expected + "', " +
                                     "but got: '" + actual + "'");
        }
    }
}
