// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.groovy.lang.resolve;

import com.intellij.openapi.util.RecursionManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiImmediateClassType;
import groovy.transform.CompileStatic;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.impl.GrClosureType;
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyCommonClassNames;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;

import static com.intellij.psi.CommonClassNames.*;
import static org.jetbrains.plugins.groovy.lang.psi.dataFlow.types.NestedContextKt.allowNestedContext;
import static org.jetbrains.plugins.groovy.lang.psi.dataFlow.types.NestedContextKt.allowNestedContextOnce;

@CompileStatic
public class TypeInferenceTest extends TypeInferenceTestBase {

  public void testTryFinallyFlow() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("tryFinallyFlow/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertTrue(type instanceof PsiIntersectionType);
    final PsiType[] conjuncts = ((PsiIntersectionType)type).getConjuncts();
    assertEquals(2, conjuncts.length);
  }

  public void testTryFinallyFlow1() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("tryFinallyFlow1/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertNotNull(type);
    assertTrue(type.equalsToText("java.lang.Integer"));
  }

  public void testTryFinallyFlow2() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("tryFinallyFlow2/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertNotNull(type);
    assertTrue(type.equalsToText("java.lang.Integer"));
  }

  public void testThrowVariable() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("throwVariable/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertNotNull(type);
    assertEquals("java.lang.Exception", type.getCanonicalText());
  }

  public void testGrvy852() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("grvy852/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertNotNull(type);
    assertEquals(JAVA_LANG_OBJECT, type.getCanonicalText());
  }

  public void testGenericMethod() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("genericMethod/A.groovy").getElement();
    final PsiType type = ref.getType();
    assertNotNull(type);
    assertEquals("java.util.List<" + JAVA_LANG_STRING + ">", type.getCanonicalText());
  }

  public void testCircular() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("circular/A.groovy").getElement();
    assertNull(ref.getType());
  }

  public void testCircular1() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("circular1/A.groovy").getElement();
    assertNull(ref.getType());
  }

  public void testCircularMap() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile(getTestName(true) + "/A.groovy").getElement();
    assertNotNull(ref.getType().getInternalCanonicalText());
  }

  public void testClosure() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("closure/A.groovy").getElement();
    assertNotNull(ref.getType());
  }

  public void testClosure1() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("closure1/A.groovy").getElement();
    assertTrue(ref.getType().equalsToText("java.lang.Integer"));
  }

  public void testClosure2() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("closure2/A.groovy").getElement();
    assertTrue(ref.getType().equalsToText("int"));
  }

  public void testClosureWithUndefinedValues() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile(getTestName(true) + "/A.groovy").getElement();
    assertNotNull(ref.getType());
  }

  public void testBindingFromInsideClosure() {
    doTest("list = ['a', 'b']; list.each { <caret>it }", JAVA_LANG_STRING);
  }

  public void testGrvy1209() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("grvy1209/A.groovy").getElement();
    assertTrue(ref.getType().equalsToText(JAVA_LANG_STRING));
  }

  public void testLeastUpperBoundClosureType() {
    GrReferenceExpression ref = (GrReferenceExpression)configureByFile("leastUpperBoundClosureType/A.groovy").getElement();
    assertInstanceOf(ref.getType(), PsiImmediateClassType.class);
  }

  public void testJavaLangClassType() {
    final GrReferenceExpression ref = (GrReferenceExpression)configureByFile("javaLangClassType/A.groovy").getElement();
    assertEquals(JAVA_LANG_STRING, ref.getType().getCanonicalText());
  }

  public void testGenericWildcard() {
    final GrReferenceExpression ref = (GrReferenceExpression)configureByFile("genericWildcard/A.groovy").getElement();
    assertEquals("X<Base>", ref.getType().getCanonicalText());
  }

  public void testArrayLikeAccessWithIntSequence() {
    final GrReferenceExpression ref = (GrReferenceExpression)configureByFile("arrayLikeAccessWithIntSequence/A.groovy").getElement();
    assertEquals("java.util.List<java.lang.Integer>", ref.getType().getCanonicalText());
  }

  public void testArrayAccess() {
    final GrReferenceExpression ref = (GrReferenceExpression)configureByFile("arrayAccess/A.groovy");
    assertEquals(JAVA_LANG_STRING, ref.getType().getCanonicalText());
  }

  public void testReturnTypeByTailExpression() {
    final GrReferenceExpression ref = (GrReferenceExpression)configureByFile("returnTypeByTailExpression/A.groovy");
    assertEquals(JAVA_LANG_STRING, ref.getType().getCanonicalText());
  }

  public void testParameterWithBuiltinType() {
    GrReferenceExpression refExpr = (GrReferenceExpression)configureByFile("parameterWithBuiltinType/A.groovy");
    assertEquals("int", refExpr.getType().getCanonicalText());
  }

  public void testRawTypeInReturnExpression() {
    assertNotNull(resolve("A.groovy"));
  }

  public void testMethodCallInvokedOnArrayAccess() {
    final GrReferenceExpression reference = (GrReferenceExpression)configureByFile("A.groovy");
    assertNotNull(reference);
    assertNotNull(reference.getType());
    assertEquals("java.lang.Integer", reference.getType().getCanonicalText());
  }

  private void assertTypeEquals(String expected, String fileName) {
    final PsiReference ref = configureByFile(getTestName(true) + "/" + fileName);
    assertInstanceOf(ref, GrReferenceExpression.class);
    final PsiType type = ((GrReferenceExpression)ref).getType();
    assertNotNull(type);
    assertEquals(expected, type.getCanonicalText());
  }

  public void testTypeOfGroupBy() {
    assertTypeEquals("java.util.Map<java.lang.Integer,java.util.List<java.lang.Integer>>", "A.groovy");
  }

  public void testConditionalExpressionWithNumericTypes() {
    assertTypeEquals("java.lang.Number", "A.groovy");
  }

  public void testImplicitCallMethod() {
    assertEquals(JAVA_LANG_STRING, ((GrExpression)configureByFile("A.groovy")).getType().getCanonicalText());
  }

  public void testImplicitlyReturnedMethodCall() {
    assertTypeEquals("java.util.Map<BasicRange,java.util.Map<BasicRange,java.lang.Double>>", "A.groovy");
  }

  public void testInferWithClosureType() {
    assertTypeEquals("java.util.Date", "A.groovy");
  }

  public void testPlusEquals1() {
    assertTypeEquals("Test", "A.groovy");
  }

  public void testPlusEquals2() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testPlusEquals3() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testPlusEqualsClosure() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testGetAtClosure() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testPreferMethodOverloader() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testSafeInvocationInClassQualifier() {
    final PsiReference ref = configureByFile(getTestName(true) + "/SafeInvocationInClassQualifier.groovy");
    assertInstanceOf(ref, GrReferenceExpression.class);
    assertNull(((GrReferenceExpression)ref).getType());
  }

  public void testReturnTypeFromMethodClosure() {
    assertTypeEquals(JAVA_LANG_STRING, "A.groovy");
  }

  public void testNoSOF() {
    final PsiReference ref = configureByFile(getTestName(true) + "/A.groovy");
    assertInstanceOf(ref, GrReferenceExpression.class);
    //noinspection unused
    final PsiType type = ((GrReferenceExpression)ref).getType();
    assertTrue(true); // test just should not fail with SOF exception
  }

  public void testTraditionalForVar() {
    allowNestedContext(2, getTestRootDisposable());
    assertTypeEquals(JAVA_LANG_INTEGER, "A.groovy");
  }

  public void testIncMethod() {
    assertTypeEquals(JAVA_LANG_INTEGER, "A.groovy");
  }

  public void testDGMFind() {
    assertTypeEquals("java.io.File", "a.groovy");
  }

  public void testMultiTypeParameter() {
    assertTypeEquals("X | Y", "a.groovy");
  }

  public void testSingleParameterInStringInjection() {
    assertTypeEquals("java.io.StringWriter", "a.groovy");
  }

  public void testIndexPropertyPlusAssigned() {
    //noinspection GrMethodMayBeStatic
    GroovyFile file = (GroovyFile)myFixture.configureByText("a.groovy", """
      class X {
          def putAt(String s, X x){new Date()}
    
          def getAt(String s) {new X()}
    
          def plus(int i) {this}
      }
    
      map = new X()
    
      map['i'] += 2
    """);
    GrAssignmentExpression assignment = (GrAssignmentExpression)file.getTopStatements()[2];
    assertType("java.util.Date", assignment.getType());
  }

  public void testAllTypeParamsAreSubstituted() {
    assertTypeEquals("java.util.Map<" + JAVA_LANG_OBJECT + "," + JAVA_LANG_OBJECT + ">", "a.groovy");
  }

  public void testWildCardsNormalized() {
    assertTypeEquals(JAVA_LANG_OBJECT, "a.groovy");
  }

  public void testIndexPropertyInLHS() {
    assertTypeEquals("java.util.LinkedHashMap<" + JAVA_LANG_OBJECT + ", " + JAVA_LANG_OBJECT + ">", "a.groovy");
  }

  public void testRawCollectionsInCasts() {
    doTest("""
      String[] a = ["a"];
      def b = a as ArrayList;
      def cc = b[0];
      print c<caret>c
      """, JAVA_LANG_STRING);
  }

  public void testFind() {
    doTest("""
      def arr =  ['1', '2', '3'] as String[];
      def found = arr.find({it=='1'});
      print fou<caret>nd
      """, JAVA_LANG_STRING);
  }

  public void testFindAll() {
    doTest("""
      def arr =  ['1', '2', '3'];
      def found = arr.findAll({it==1});
      print fou<caret>nd
      """, "java.util.ArrayList<" + JAVA_LANG_STRING + ">");
  }

  public void testFindAllForArray() {
    doTest("""
      def arr =  ['1', '2', '3'] as String[];
      def found = arr.findAll({it==1});
      print fou<caret>nd
      """, "java.util.ArrayList<" + JAVA_LANG_STRING + ">");
  }

  public void testFindAllForSet() {
    myFixture.addClass("""
      package java.util;
      class HashSet<T> implements Set<T> {}
      """);
    doTest("""
      def arr =  ['1', '2', '3'] as Set<String>;
      def found = arr.findAll({it==1});
      print fou<caret>nd
      """, "java.util.HashSet<" + JAVA_LANG_STRING + ">");
  }

  public void testInferArgumentTypeFromMethod1() {
    allowNestedContextOnce(getTestRootDisposable());
    doTest("""
      def bar(String s) {}
      
      def foo(Integer a) {
        while(true) {
          bar(a)
          <caret>a.substring(2)
        }
      }
      """, "[java.lang.Integer," + JAVA_LANG_STRING + "]");
  }

  public void testInferArgumentTypeFromMethod2() {
    doTest("""
      def bar(String s) {}
      
      def foo(Integer a) {
          bar(a)
          <caret>a.substring(2)
      }
      """, "[java.lang.Integer," + JAVA_LANG_STRING + "]");
  }

  public void testInferArgumentTypeFromMethod3() {
    doTest("""
      def bar(String s) {}
      
      def foo(Integer a) {
          bar(a)
          print a
          <caret>a.substring(2)
      }
      """, "[java.lang.Integer," + JAVA_LANG_STRING + "]");
  }

  public void testInferArgumentTypeFromMethod4() {
    allowNestedContext(2, getTestRootDisposable());
    doTest("""
      def bar(String s) {}
      
      def foo(Integer a) {
        while(true) {
          bar(a)
          print a
          <caret>a.substring(2)
        }
      }
      """, "[java.lang.Integer," + JAVA_LANG_STRING + "]");
  }

  public void testInferArgumentTypeFromMethod5NoRecursion() {
    allowNestedContextOnce(getTestRootDisposable());
    doTest("""
      void usage(Collection<UnknownClass> x) {
        while (unknownCondition) {
          <caret>foo(x)
        }
      }
      void foo(Collection<UnknownClass> list) {}
    """, "void");
  }

  public void testEmptyListOrListWithGenerics() {
    doTest("""
      def list = cond ? [1, 2, 3] : [];
      print lis<caret>t
      """, JAVA_UTIL_LIST + "<" + JAVA_LANG_INTEGER + ">");
  }

  public void testEmptyListOrListWithGenerics2() {
    doTest("""
      def List<Integer> foo(){}
      def list = cond ? foo() : [];
      print lis<caret>t
      """, JAVA_UTIL_LIST + "<" + JAVA_LANG_INTEGER + ">");
  }

  public void testEmptyMapOrMapWithGenerics() {
    doExprTest("""
      cond ? [1:'a', 2:'a', 3:'a'] : [:]
      """, "java.util.LinkedHashMap<java.lang.Integer, " + JAVA_LANG_STRING + ">");
  }

  public void testEmptyMapOrMapWithGenerics2() {
    doTest("""
      def Map<String, String> foo(){}
      def map = cond ? foo() : [:];
      print ma<caret>p
      """, JAVA_UTIL_MAP + "<" + JAVA_LANG_STRING + "," + JAVA_LANG_STRING + ">");
  }

  public void testSpread1() {
    myFixture.addClass("""
      class A {
        String getString() {return "a";}
      }
      """);
    doTest("""
      [new A()].stri<caret>ng
      """, JAVA_UTIL_ARRAY_LIST + "<" + JAVA_LANG_STRING + ">");
  }

  public void testSpread2() {
    myFixture.addClass("""
      class A {
        String getString() {return "a";}
      }
      """);
    doTest("""
      class Cat {
        static getFoo(String b) { 2 }
      }
      use(Cat) {
        [new A()].string.fo<caret>o
      }
    """, JAVA_UTIL_ARRAY_LIST + "<" + JAVA_LANG_INTEGER + ">");
  }

  public void testSpread3() {
      myFixture.addClass("""
        class A {
          String getString() {return "a";}
        }
        """);
    doTest("""
        [[new A()]].stri<caret>ng
      """, JAVA_UTIL_ARRAY_LIST + "<" + JAVA_UTIL_ARRAY_LIST + "<" + JAVA_LANG_STRING + ">>");
  }

  public void testSpread4() {
    myFixture.addClass("""
      class A {
        String getString() {return "a";}
      }
      """);
    doTest("""
      class Cat {
        static getFoo(String b) { 2 }
      }
    
      use(Cat){
        [[new A()]].string.fo<caret>o
      }
    """, JAVA_UTIL_ARRAY_LIST + "<" + JAVA_UTIL_ARRAY_LIST + "<" + JAVA_LANG_INTEGER + ">>");
  }

  public void testInstanceOfInferring1() {
    doTest("""
      def bar(oo) {
        boolean b = oo instanceof String || oo != null;
        o<caret>o
      }
      """, null);
  }

  public void testNegatedInstanceOfInferring1() {
    doTest("""
      def bar(oo) {
        boolean b = oo !instanceof String || oo != null;
        o<caret>o
      }
      """, null);
  }

  public void testInstanceOfInferring2() {
    doTest("""
      def bar(oo) {
        boolean b = oo instanceof String || o<caret>o != null;
        oo
      }
      """, null);
  }

  public void testNegatedInstanceOfInferring2() {
    doTest("""
      def bar(oo) {
        boolean b = oo !instanceof String || o<caret>o != null;
        oo
      }
      """, JAVA_LANG_STRING);
  }

  public void testInstanceOfInferring3() {
    doTest("""
      def bar(oo) {
        boolean b = oo instanceof String && o<caret>o != null;
        oo
      }
      """, JAVA_LANG_STRING);
  }

  public void testNegatedInstanceOfInferring3() {
    doTest("""
      def bar(oo) {
        boolean b = oo !instanceof String && o<caret>o != null;
        oo
      }
      """, null);
  }

  public void testInstanceOfInferring4() {
    doTest("""
      def bar(oo) {
        boolean b = oo instanceof String && oo != null;
        o<caret>o
      }
      """, null);
  }

  public void testNegatedInstanceOfInferring4() {
    doTest("""
      def bar(oo) {
        boolean b = oo !instanceof String && oo != null;
        o<caret>o
      }
      """, null);
  }

  public void testInstanceOfInferring5() {
    doTest("""
      def foo(def oo) {
        if (oo instanceof String && oo instanceof CharSequence) {
          oo;
        }
        else {
          o<caret>o
        }
      
      }
      """, null);
  }

  public void testNegatedInstanceOfInferring5() {
    doTest("""
      def foo(def oo) {
        if (oo !instanceof String || oo !instanceof CharSequence) {
          oo;
        }
        else {
          o<caret>o
        }
      
      }
      """, JAVA_LANG_STRING);
  }

  public void testInstanceOfInferring6() {
    doTest("""
      def foo(bar) {
        if (!(bar instanceof String) && bar instanceof Runnable) {
          ba<caret>r
        }
      }
      """, "java.lang.Runnable");
  }

  public void testNegatedInstanceOfInferring6() {
    doTest("""
      def foo(bar) {
        if (!(bar !instanceof String) || bar !instanceof Runnable) {
      
        } else {
          ba<caret>r
        }
      }
      """, "java.lang.Runnable");
  }

  public void testInstanceOfOrInstanceOf() {
    doTest("""
      class A {}
      class B extends A {}
      class C extends A {}
      def foo(a) {
          if (a instanceof B || a instanceof C) {
              <caret>a
          }
      }
      """, "A");
  }

  public void testIfNullTyped() {
    doTest("""
      def foo(String a) {
          if (a == null) {
              <caret>a
          }
      }
      """, JAVA_LANG_STRING);
  }

  public void testNullOrInstanceOf() {
    doTest("""
      def foo(a) {
          if (a == null || a instanceof String) {
              <caret>a
          }
      }
      """, JAVA_LANG_STRING);
  }

  public void testNullOrInstanceOf2() {
    doTest("""
      def foo(a) {
          if (null == a || a instanceof String) {
              <caret>a
          }
      }
      """, JAVA_LANG_STRING);
  }

  public void testInstanceOfOrNull() {
    doTest("""
      def foo(a) {
          if (a == null || a instanceof String) {
              <caret>a
          }
      }
      """, JAVA_LANG_STRING);
  }

  public void testInstanceOfOrNull2() {
    doTest("""
      def foo(a) {
          if (null == a || a instanceof String) {
              <caret>a
          }
      }
      """, JAVA_LANG_STRING);
  }
  
  public void testInstanceOfOrNullOnField() {
    doTest(
      """
        class C {
          Object f
          def foo() {
              if (null == f || f instanceof String) {
                  <caret>f
              }
          }
        }
        """, JAVA_LANG_STRING);
  }

  public void testInstanceOfOrNullOnField2() {
    doTest(
      """
        class C {
          Object f
          def foo() {
              if (null == this.f || this.f instanceof String) {
                  <caret>f
              }
          }
        }
        """, JAVA_LANG_STRING);
  }

  public void testNullOrInstanceOfElse() {
    doTest(
      """
        def foo(a) {
            if (a != null && a !instanceof String) {
        
            } else {
                <caret>a
            }
        }
        """, JAVA_LANG_STRING);
  }

  public void testWhileNull() {
    doTest(
      """
        def s = null
        while (s == null) {
          s = ""
        }
        <caret>s
        """, JAVA_LANG_STRING);
  }

  public void testWhileNullTyped() {
    doTest(
      """
        String s = null
        while (s == null) {
          s = ""
        }
        <caret>s
        """, JAVA_LANG_STRING);
  }

  public void testEnumConstant() {
    doTest(
      """
        import static MyEnum.*
        enum MyEnum {ONE}
        if (ONE instanceof String) {
          <caret>ONE
        }
        """, "MyEnum");
  }

  public void testInString() {
    doTest(
      """
        def foo(ii) {
          if (ii in String)
            print i<caret>i
        }
        """, JAVA_LANG_STRING);
  }

  public void testNegatedInString() {
    doTest(
      """
        def foo(ii) {
          if (ii !in String) {}
          else print i<caret>i
        }
        """, JAVA_LANG_STRING);
  }

  public void testIndexProperty() {
    allowNestedContext(2, getTestRootDisposable());
    doTest(
      """
        private void getCommonAncestor() {
            def c1 = [new File('a')]
            for (int i = 0; i < 2; i++) {
                if (c1[i] != null) break
                def cur = c1[i]
                print cu<caret>r
            }
        }
        """, "java.io.File");
  }

  public void testWildcardClosureParam() {
    doTest(
      """
        class Tx {
            def methodOfT() {}
        }
        
        def method(List<? extends Tx> t) {
            t.collect { print i<caret>t }
        }
        """, "Tx");
  }

  public void testAssert() {
    doTest(
      """
        def foo(def var) {
          assert var instanceof String
          va<caret>r.isEmpty()
        }
        """, JAVA_LANG_STRING);
  }

  public void testUnresolvedSpread() {
    doTest(
      """
        def xxx = abc*.foo
        print xx<caret>x""", "java.util.List");
  }

  public void testThisInCategoryClass() {
    doTest(
      """
        class Cat {}
        
        @groovy.lang.Category(Cat)
        class Any {
          void foo() {
            print th<caret>is
          }
        }
        """, "Cat");
  }

  public void testNormalizeTypeFromMap() {
    doTest(
      """
        def pp = new HashMap<?, ?>().a
        print p<caret>p
        """, JAVA_LANG_OBJECT);
  }

  public void testRange() {
    doTest(
      """
        def m = new int[3]
        for (ii in 0..<m.length) {
         print i<caret>i
        }
        """, "java.lang.Integer");

    doTest(
      """
        def m = new int[3]
        for (ii in m.size()..< m[0]) {
         print i<caret>i
        }
        """, "java.lang.Integer");
  }

  public void testUnary() {
    doExprTest("~/abc/", "java.util.regex.Pattern");
  }

  public void testUnary2() {
    doExprTest("-/abc/", null);
  }

  public void testUnary3() {
    doExprTest("""
                 class A {
                   def bitwiseNegate() {'abc'}
                 }
                 ~new A()
                 """, JAVA_LANG_STRING);
  }

  public void testMultiply1() {
    doExprTest("2*2", "java.lang.Integer");
  }

  public void testMultiply2() {
    doExprTest("2f*2f", "java.lang.Double");
  }

  public void testMultiply3() {
    doExprTest("2d*2d", "java.lang.Double");
  }

  public void testMultiply4() {
    doExprTest("2.4*2", "java.math.BigDecimal");
  }

  public void testMultiply5() {
    doExprTest("((byte)2)*((byte)2)", "java.lang.Integer");
  }

  public void testMultiply6() {
    doExprTest("\"abc\"*\"cde\"", JAVA_LANG_STRING); //expected number as a right operand
  }

  public void testMultiply7() {
    doExprTest("""
                 class A {
                   def multiply(A a) {new B()}
                 }
                 class B{}
                 new A()*new A()
                 """, "B");
  }

  public void testMultiply8() {
    doExprTest("""
                 class A { }
                 new A()*new A()
                 """, null);
  }

  public void testDiv1() {
    doExprTest("1/2", "java.math.BigDecimal");
  }

  public void testDiv2() {
    doExprTest("1/2.4", "java.math.BigDecimal");
  }

  public void testDiv3() {
    doExprTest("1d/2", "java.lang.Double");
  }

  public void testDiv4() {
    doExprTest("1f/2", "java.lang.Double");
  }

  public void testDiv5() {
    doExprTest("1f/2.4", "java.lang.Double");
  }

  public void testRecursionWithMaps() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    allowNestedContext(2, getTestRootDisposable());
    doTest(
      """
        def foo(Map map) {
          while(true)
            ma<caret>p = [a:map]
        }
        """, "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", java.util.Map>");
  }

  public void testRecursionWithLists() {
    allowNestedContextOnce(getTestRootDisposable());
    doTest(
      """
        def foo(List list) {
          while(true)
            lis<caret>t = [list]
        }
        """, "java.util.ArrayList<java.util.List>");
  }

  public void testReturnNullWithGeneric() {
    doTest(
      """
        import groovy.transform.CompileStatic
        
        @CompileStatic
        class SomeClass {
          protected List foo(String text) {
            return null
          }
        
          def bar() {
            fo<caret>o("")
          }
        }
        """, "java.util.List");
  }

  public void testSubstitutorIsNotInferredWhileInferringInitializerType() {
    GroovyFile file = (GroovyFile)myFixture.configureByText("_.groovy", """
       class A { Closure foo = { 42 } }
      """);

    GrClosureType.forbidClosureInference(() -> {
      PsiMethod getter = file.getTypeDefinitions()[0].findMethodsByName("getFoo", false)[0];
      PsiClassType type = (PsiClassType)PsiUtil.getSmartReturnType(getter);
      assertEquals(GroovyCommonClassNames.GROOVY_LANG_CLOSURE, type.resolve().getQualifiedName());
    });
  }

  public void testVariableTypeFromNullInitializer() {
    doTest("def v = null; <caret>v", "null");
  }

  public void testVariableTypeFromNullInitializerCompileStatic() {
    doTest("""
              @groovy.transform.CompileStatic
                 def foo() {
                   def v = null;
                   <caret>v;
                 }
             """, JAVA_LANG_OBJECT);
  }

  public void testClassExpressions() {
    doExprTest("String[]", "java.lang.Class<" + JAVA_LANG_STRING + "[]>");
    doExprTest("Class[]", "java.lang.Class<java.lang.Class[]>");
    doExprTest("int[]", "java.lang.Class<int[]>");
    doExprTest("float[][]", "java.lang.Class<float[][]>");
    doExprTest("Integer[][]", "java.lang.Class<java.lang.Integer[][]>");
    doExprTest("boolean[][][]", "java.lang.Class<boolean[][][]>");

    doExprTest("String.class", "java.lang.Class<" + JAVA_LANG_STRING + ">");
    doExprTest("byte.class", "java.lang.Class<byte>");

    doExprTest("String[].class", "java.lang.Class<" + JAVA_LANG_STRING + "[]>");
    doExprTest("Class[].class", "java.lang.Class<java.lang.Class[]>");
    doExprTest("int[].class", "java.lang.Class<int[]>");
    doExprTest("float[][]", "java.lang.Class<float[][]>");
    doExprTest("Integer[][].class", "java.lang.Class<java.lang.Integer[][]>");
    doExprTest("double[][][].class", "java.lang.Class<double[][][]>");
  }

  public void testClassExpressionsWithArguments() {
    doExprTest("String[1]", JAVA_LANG_OBJECT);
    doExprTest("String[1][]", JAVA_LANG_OBJECT);
    doExprTest("String[1][].class", "java.lang.Class<? extends " + JAVA_LANG_OBJECT + ">");
    doExprTest("int[][1].class", "java.lang.Class<? extends " + JAVA_LANG_OBJECT + ">");
  }

  public void testClassReference() {
    doExprTest("[].class", "java.lang.Class<? extends java.util.ArrayList>");
    doExprTest("1.class", "java.lang.Class<? extends java.lang.Integer>");
    doExprTest("String.valueOf(1).class", "java.lang.Class<? extends " + JAVA_LANG_STRING + ">");
    doExprTest("1.getClass()", "java.lang.Class<? extends java.lang.Integer>");

    doCSExprTest("[].class", "java.lang.Class<? extends java.util.List>");
    doCSExprTest("1.class", "java.lang.Class<? extends java.lang.Integer>");
    doCSExprTest("String.valueOf(1).class", "java.lang.Class<? extends " + JAVA_LANG_STRING + ">");
    doCSExprTest("1.getClass()", "java.lang.Class<? extends java.lang.Integer>");
  }

  public void testUnknownClass() {
    doExprTest("a.class", null);
    doCSExprTest("a.class", "java.lang.Class<? extends " + JAVA_LANG_OBJECT + ">");

    doExprTest("a().class", null);
    doCSExprTest("a().class", "java.lang.Class");
  }

  public void testListLiteralType() {
    doExprTest("[null]", "java.util.ArrayList");
    doExprTest("[\"foo\", \"bar\"]", "java.util.ArrayList<" + JAVA_LANG_STRING + ">");
    doExprTest("[\"${foo}\", \"${bar}\"]", "java.util.ArrayList<groovy.lang.GString>");
    doExprTest("[1, \"a\"]", "java.util.ArrayList<java.io.Serializable>");
  }

  public void testListLiteralTypeCS() {
    doCSExprTest("[null]", "java.util.List");
    doCSExprTest("[\"foo\", \"bar\"]", "java.util.List<" + JAVA_LANG_STRING + ">");
    doCSExprTest("[\"${foo}\", \"${bar}\"]", "java.util.List<groovy.lang.GString>");
    doCSExprTest("[1, \"a\"]", "java.util.List<java.io.Serializable>");
  }

  public void testMapLiteralType() {
    doExprTest("[a: 'foo']", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", " + JAVA_LANG_STRING + ">");
    doExprTest("[1: 'foo']", "java.util.LinkedHashMap<java.lang.Integer, " + JAVA_LANG_STRING + ">");
    doExprTest("[1L: 'foo']", "java.util.LinkedHashMap<java.lang.Long, " + JAVA_LANG_STRING + ">");
    doExprTest("[null: 'foo']", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", " + JAVA_LANG_STRING + ">");
    doExprTest("[(null): 'foo']", "java.util.LinkedHashMap<null, " + JAVA_LANG_STRING + ">");
    doExprTest("[foo: null]", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", null>");
    doExprTest("[(null): 'foo', bar: null]", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", " + JAVA_LANG_STRING + ">");
    doExprTest("[foo: 'bar', 2: 'goo']", "java.util.LinkedHashMap<java.io.Serializable, " + JAVA_LANG_STRING + ">");
  }

  public void testRecursiveLiteralTypes() {
    RecursionManager.disableMissedCacheAssertions(getTestRootDisposable());
    doExprTest(" def foo() { [foo()] }\nfoo()", "java.util.ArrayList<" + JAVA_LANG_OBJECT + ">");
    doExprTest(" def foo() { [new Object(), foo()] }\nfoo()", "java.util.ArrayList<" + JAVA_LANG_OBJECT + ">");
    doExprTest(" def foo() { [someKey1: foo()] }\nfoo()", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", java.util.LinkedHashMap>");
    doExprTest(" def foo() { [someKey0: new Object(), someKey1: foo()] }\nfoo()", "java.util.LinkedHashMap<" + JAVA_LANG_STRING + ", " + JAVA_LANG_OBJECT + ">");
  }

  public void testRangeLiteralType() {
    doExprTest("1..10", "groovy.lang.IntRange");
    doExprTest("'a'..'z'", "groovy.lang.Range<" + JAVA_LANG_STRING + ">");
    doExprTest("'b'..1", "groovy.lang.Range<java.io.Serializable>");
  }

  public void testListWithSpread() {
    doExprTest("def l = [1, 2]; [*l]", "java.util.ArrayList<java.lang.Integer>");
    doExprTest("def l = [1, 2]; [*[*[*l]]]", "java.util.ArrayList<java.lang.Integer>");
  }

  public void testMapSpreadDotAccess() {
    doExprTest("[foo: 2, bar: 4]*.key", "java.util.ArrayList<" + JAVA_LANG_STRING + ">");
    doExprTest("[foo: 2, bar: 4]*.value", "java.util.ArrayList<java.lang.Integer>");
    doExprTest("[foo: 2, bar: 4]*.undefined", "java.util.List");
  }

  public void testInstanceofDoesNotInterfereWithOuterIf() {
    doTest("""
              def bar(CharSequence xx) {
                   if (xx instanceof String) {
                         1 instanceof Object
                         <caret>xx.hashCode()
                     }
                 }
             """, JAVA_LANG_STRING);
  }

  public void testGenericTupleInferenceWithTypeParam() {
    doTest("""
              def <T> T func(T arg){
                     return arg
                 }
             
                 def bar() {
                     def ll = func([[""]])
                     l<caret>l
                 }
             """, "java.util.ArrayList<java.util.ArrayList<" + JAVA_LANG_STRING + ">>");
  }

  public void testGenericTupleInferenceWithTypeParam2() {
    doTest("""
              def <T> T func(T arg){
                     return arg
                 }
             
