// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.structuralsearch;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Disposer;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.structuralsearch.*;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.tools.ide.metrics.benchmark.Benchmark;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;

/**
 * @author Maxim.Mossienko
 */
public class JavaStructuralReplaceTest extends StructuralReplaceTestCase {
  private final Disposable myBeforeParentDisposeDisposable = Disposer.newDisposable();

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    IdeaTestUtil.setProjectLanguageLevel(getProject(), LanguageLevel.JDK_16, myBeforeParentDisposeDisposable);
    final MatchOptions matchOptions = options.getMatchOptions();
    matchOptions.setFileType(JavaFileType.INSTANCE);
  }

  @Override
  protected void tearDown() throws Exception {
    try {
      Disposer.dispose(myBeforeParentDisposeDisposable);
    }
    catch (Throwable e) {
      addSuppressedException(e);
    }
    finally {
      super.tearDown();
    }
  }

  @Override
  protected Sdk getProjectJDK() {
    return IdeaTestUtil.getMockJdk18();
  }

  public void testReplaceInLiterals() {
    String s1 = "String ID_SPEED = \"Speed\";";
    String s2 = "String 'name = \"'_string\";";
    String s2_2 = "String 'name = \"'_string:[regex( .* )]\";";
    String s3 = "VSegAttribute $name$ = new VSegAttribute(\"$string$\");";
    String expectedResult = "VSegAttribute ID_SPEED = new VSegAttribute(\"Speed\");";

    assertEquals("Matching/replacing literals", expectedResult, replace(s1, s2, s3));
    assertEquals("Matching/replacing literals", expectedResult, replace(s1, s2_2, s3));

    String s4 = "params.put(\"BACKGROUND\", \"#7B528D\");";
    String s5 = "params.put(\"$FieldName$\", \"#$exp$\");";
    String s6 = "String $FieldName$ = \"$FieldName$\";\n" +
                "params.put($FieldName$, \"$exp$\");";
    String expectedResult2 = "String BACKGROUND = \"BACKGROUND\";\n" +
                             "params.put(BACKGROUND, \"7B528D\");";

    assertEquals("string literal replacement 2", expectedResult2, replace(s4, s5, s6));

    String s7 = """
      IconLoader.getIcon("/ant/property.png");
      IconLoader.getIcon("/ant/another/property.png");
      """;
    String s8 = "IconLoader.getIcon(\"/'_module/'_name:[regex( \\w+ )].png\");";
    String s9 = "Icons.$module$.$name$;";
    String expectedResult3 = """
      Icons.ant.property;
      IconLoader.getIcon("/ant/another/property.png");
      """;

    assertEquals("string literal replacement 3", expectedResult3, replace(s7, s8, s9));

    String s10 = """
      configureByFile(path + "1.html");
          checkResultByFile(path + "1_after.html");
          checkResultByFile(path + "1_after2.html");
          checkResultByFile(path + "1_after3.html");""";
    String s11 = "\"'a.html\"";
    String s12 = "\"$a$.\"+ext";
    String expectedResult4 = """
      configureByFile(path + "1."+ext);
          checkResultByFile(path + "1_after."+ext);
          checkResultByFile(path + "1_after2."+ext);
          checkResultByFile(path + "1_after3."+ext);""";

    assertEquals("string literal replacement 4", expectedResult4, replace(s10, s11, s12));
  }

  public void testReplace2() {
    String s1 = """
      package com.www.xxx.yyy;
      
      import javax.swing.*;
      
      public class Test {
        public static void main(String[] args) {
          if (1==1)
            JOptionPane.showMessageDialog(null, "MESSAGE");
        }
      }""";
    String s2 = "JOptionPane.'_showDialog(null, '_msg);";
    String s3 = "//FIXME provide a parent frame\n" +
                "JOptionPane.$showDialog$(null, $msg$);";

    String expectedResult = """
      package com.www.xxx.yyy;
      
      import javax.swing.*;
      
      public class Test {
        public static void main(String[] args) {
          if (1==1)
            //FIXME provide a parent frame
      JOptionPane.showMessageDialog(null, "MESSAGE");
        }
      }""";

    assertEquals("adding comment to statement inside the if body", expectedResult, replace(s1, s2, s3));

    String s4 = "myButton.setText(\"Ok\");";
    String s5 = "'_Instance.'_MethodCall:[regex( setText )]('_Parameter*:[regex( \"Ok\" )]);";
    String s6 = "$Instance$.$MethodCall$(\"OK\");";

    String expectedResult2 = "myButton.setText(\"OK\");";

    assertEquals("adding comment to statement inside the if body", expectedResult2, replace(s4, s5, s6));
  }

  public void testReplace() {
    String str = """
      // searching for several constructions
          lastTest = "several constructions match";
          matches = testMatcher.findMatches(s5,s4, options);
          if (matches==null || matches.size()!=3) return false;
      
          // searching for several constructions
          lastTest = "several constructions 2";
          matches = testMatcher.findMatches(s5,s6, options);
          if (matches.size()!=0) return false;
      
          //options.setLooseMatching(true);
          // searching for several constructions
          lastTest = "several constructions 3";
          matches = testMatcher.findMatches(s7,s8, options);
          if (matches.size()!=2) return false;""";

    String str2 = """
            lastTest = '_Descr;
            matches = testMatcher.findMatches('_In,'_Pattern, options);
            if (matches.size()!='_Number) return false;\
      """;
    String str3 = "assertEquals($Descr$,testMatcher.findMatches($In$,$Pattern$, options).size(),$Number$);";
    String expectedResult1 = """
      // searching for several constructions
      lastTest = "several constructions match";
      matches = testMatcher.findMatches(s5, s4, options);
      if (matches == null || matches.size() != 3) return false;
      
      // searching for several constructions
      assertEquals("several constructions 2", testMatcher.findMatches(s5, s6, options).size(), 0);
      
      //options.setLooseMatching(true);
      // searching for several constructions
      assertEquals("several constructions 3", testMatcher.findMatches(s7, s8, options).size(), 2);""";

    String str4 = "";

    options.setToReformatAccordingToStyle(true);
    assertEquals("Basic replacement with formatter", expectedResult1, replace(str, str2, str3));
    options.setToReformatAccordingToStyle(false);

    String expectedResult2 = """
      // searching for several constructions
          lastTest = "several constructions match";
          matches = testMatcher.findMatches(s5,s4, options);
          if (matches==null || matches.size()!=3) return false;
      
          // searching for several constructions
      
          //options.setLooseMatching(true);
          // searching for several constructions""";
    assertEquals("Empty replacement", expectedResult2, replace(str, str2, str4));

    String str5 = "testMatcher.findMatches('_In,'_Pattern, options).size()";
    String str6 = "findMatchesCount($In$,$Pattern$)";
    String expectedResult3 = """
      // searching for several constructions
      lastTest = "several constructions match";
      matches = testMatcher.findMatches(s5, s4, options);
      if (matches == null || matches.size() != 3) return false;
      
      // searching for several constructions
      assertEquals("several constructions 2", findMatchesCount(s5,s6), 0);
      
      //options.setLooseMatching(true);
      // searching for several constructions
      assertEquals("several constructions 3", findMatchesCount(s7,s8), 2);""";
    assertEquals("Expression replacement", expectedResult3, replace(expectedResult1, str5, str6));

    String str7 =
      "try { a.doSomething(); /*1*/b.doSomething(); } catch(IOException ex) {  ex.printStackTrace(); throw new RuntimeException(ex); }";
    String str8 = "try { '_Statements+; } catch('_ '_) { '_HandlerStatements+; }";
    String str9 = "$Statements$;";
    String expectedResult4 = "a.doSomething(); /*1*/b.doSomething();";

    assertEquals("Multi line match in replacement", expectedResult4, replace(str7, str8, str9));

    String str10 = """
          parentNode.insert(compositeNode, i);
          if (asyncMode) {
             myTreeModel.nodesWereInserted(parentNode,new int[] {i} );
          }\
      """;
    String str11 = """
          '_parentNode.insert('_newNode, '_i);
          if (asyncMode) {
             myTreeModel.nodesWereInserted('_parentNode,new int[] {'_i} );
          }\
      """;
    String str12 = "addChild($parentNode$,$newNode$, $i$);";
    String expectedResult5 = "    addChild(parentNode,compositeNode, i);";

    assertEquals("Array initializer replacement", expectedResult5, replace(str10, str11, str12));

    String str13 = "  aaa(5,6,3,4,1,2);";
    String str14 = "aaa('_t{2,2},3,4,'_q{2,2});";
    String str15 = "aaa($q$,3,4,$t$);";
    String expectedResult6 = "  aaa(1,2,3,4,5,6);";

    assertEquals("Parameter multiple match", expectedResult6, replace(str13, str14, str15));

    String str16 = "  int c = a();";
    String str17 = "'_t:a ('_q*,'_p*)";
    String str18 = "$t$($q$,1,$p$)";
    String expectedResult7 = "  int c = a(1);";

    assertEquals("Replacement of init in definition + empty substitution", expectedResult7,
                 replace(str16, str17, str18));

    String str19 = "  aaa(bbb);";
    String str20 = "'_t('_);";
    String str21 = "$t$(ccc);";
    String expectedResult8 = "  aaa(ccc);";

    assertEquals("One substitution replacement", expectedResult8, replace(str19, str20, str21));

    String str22 = "  instance.setAAA(anotherInstance.getBBB());";
    String str23 = "  '_i.'_m:set(.+) ('_a.'_m2:get(.+) ());";
    String str24 = "  $a$.set$m2_1$( $i$.get$m_1$() );";
    String expectedResult9 = "  anotherInstance.setBBB( instance.getAAA() );";

    assertEquals("Reg exp substitution replacement", expectedResult9, replace(str22, str23, str24));

    String str25 = """
        LaterInvocator.invokeLater(new Runnable() {
                public void run() {
                  LOG.info("refreshFilesAsync, modalityState=" + ModalityState.current());
                  myHandler.getFiles().refreshFilesAsync(new Runnable() {
                    public void run() {
                      semaphore.up();
                    }
                  });
                }
              });\
      """;
    String str26 = "  LaterInvocator.invokeLater('_Params{1,10});";
    String str27 = "  com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater($Params$);";
    String expectedResult10 = """
        com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(new Runnable() {
                public void run() {
                  LOG.info("refreshFilesAsync, modalityState=" + ModalityState.current());
                  myHandler.getFiles().refreshFilesAsync(new Runnable() {
                    public void run() {
                      semaphore.up();
                    }
                  });
                }
              });\
      """;

    assertEquals("Anonymous in parameter", expectedResult10, replace(str25, str26, str27));

    String str28 = """
      UTElementNode elementNode = new UTElementNode(myProject, processedElement, psiFile,
                                                                processedElement.getTextOffset(), true,
                                                                !myUsageViewDescriptor.toMarkInvalidOrReadonlyUsages(), null);""";
    String str29 = "new UTElementNode('_param, '_directory, '_null, '_0, '_true, !'_descr.toMarkInvalidOrReadonlyUsages(),\n" +
                   "  '_referencesWord)";
    String str30 = "new UTElementNode($param$, $directory$, $null$, $0$, $true$, true,\n" +
                   "  $referencesWord$)";

    String expectedResult11 =
      "UTElementNode elementNode = new UTElementNode(myProject, processedElement, psiFile, processedElement.getTextOffset(), true, true,\n" +
      "  null);";
    assertEquals("Replace in def initializer", expectedResult11, replace(str28, str29, str30));

    String s31 = "a = b; b = c; a=a; c=c;";
    String s32 = "'_a = '_a;";
    String s33 = "1 = 1;";
    String expectedResult12 = "a = b; b = c; 1 = 1; 1 = 1;";

    assertEquals("replace silly assignments", expectedResult12, replace(s31, s32, s33));

    String s34 = "ParamChecker.isTrue(1==1, \"!!!\");";
    String s35 = "ParamChecker.isTrue('_expr, '_msg);";
    String s36 = "assert $expr$ : $msg$;";

    String expectedResult13 = "assert 1==1 : \"!!!\";";
    assertEquals("replace with assert", expectedResult13, replace(s34, s35, s36));

    String s37 = """
      try {\s
        ParamChecker.isTrue(1==1, "!!!");
       \s
        // comment we want to leave
       \s
        ParamChecker.isTrue(2==2, "!!!");
      } catch(Exception ex) {}""";
    String s38 = """
      try {
        '_Statement{0,100};
      } catch(Exception ex) {}""";
    String s39 = "$Statement$;";

    String expectedResult14 = """
      ParamChecker.isTrue(1==1, "!!!");
       \s
        // comment we want to leave
       \s
        ParamChecker.isTrue(2==2, "!!!");""";
    assertEquals("remove try with comments inside", expectedResult14, replace(s37, s38, s39));

    String s40 = "ParamChecker.instanceOf(queryKey, GroupBySqlTypePolicy.GroupKey.class);";
    String s41 = "ParamChecker.instanceOf('_obj, '_class.class);";
    String s42 = "assert $obj$ instanceof $class$ : \"$obj$ is an instance of \" + $obj$.getClass() + \"; expected \" + $class$.class;";
    String expectedResult15 =
      "assert queryKey instanceof GroupBySqlTypePolicy.GroupKey : \"queryKey is an instance of \" + queryKey.getClass() + " +
      "\"; expected \" + GroupBySqlTypePolicy.GroupKey.class;";

    assertEquals("Matching/replacing .class literals", expectedResult15, replace(s40, s41, s42));

    String s43 = """
      class Wpd {
        static final String TAG_BEAN_VALUE = "";
      }
      class X {
        XmlTag beanTag = rootTag.findSubTag(Wpd.TAG_BEAN_VALUE);
      }""";
    String s44 = "'_Instance?.findSubTag( '_Parameter:[exprtype( *String ) ])";
    String s45 = "jetbrains.fabrique.util.XmlApiUtil.findSubTag($Instance$, $Parameter$)";
    String expectedResult16 = """
      class Wpd {
        static final String TAG_BEAN_VALUE = "";
      }
      class X {
        XmlTag beanTag = jetbrains.fabrique.util.XmlApiUtil.findSubTag(rootTag, Wpd.TAG_BEAN_VALUE);
      }""";

    assertEquals("Matching/replacing static fields", expectedResult16, replace(s43, s44, s45, true));

    String s46 = """
      Rectangle2D rec = new Rectangle2D.Double(
                      drec.getX(),
                      drec.getY(),
                      drec.getWidth(),
                      drec.getWidth());""";
    String s47 = "$Instance$.$MethodCall$()";
    String s48 = "OtherClass.round($Instance$.$MethodCall$(),5)";
    String expectedResult17 = """
      Rectangle2D rec = new Rectangle2D.Double(
                      OtherClass.round(drec.getX(),5),
                      OtherClass.round(drec.getY(),5),
                      OtherClass.round(drec.getWidth(),5),
                      OtherClass.round(drec.getWidth(),5));""";
    assertEquals("Replace in constructor", expectedResult17, replace(s46, s47, s48));

    String s49 = """
      class A {}
      class B extends A {}
      class C {
        A a = new B();
      }""";
    String s50 = "A '_b = new '_B:*A ();";
    String s51 = "A $b$ = new $B$(\"$b$\");";
    String expectedResult18 = """
      class A {}
      class B extends A {}
      class C {
        A a = new B("a");
      }""";

    assertEquals("Class navigation", expectedResult18, replace(s49, s50, s51, true));

    String s52 = """
      try {
        aaa();
      } finally {
        System.out.println();}
      try {
        aaa2();
      } catch(Exception ex) {
        aaa3();
      }
      finally {
        System.out.println();
      }
      try {
        aaa4();
      } catch(Exception ex) {
        aaa5();
      }
      """;
    String s53 = """
      try { '_a; } finally {
        '_b;
      }""";
    String s54 = "$a$;";
    String expectedResult19 = """
      aaa();
      try {
        aaa2();
      } catch(Exception ex) {
        aaa3();
      }
      finally {
        System.out.println();
      }
      try {
        aaa4();
      } catch(Exception ex) {
        aaa5();
      }
      """;

    options.getMatchOptions().setLooseMatching(false);
    try {
      assertEquals("Try/finally unwrapped with strict matching", expectedResult19, replace(s52, s53, s54));
    }
    finally {
      options.getMatchOptions().setLooseMatching(true);
    }

    String expectedResult19Loose = """
      aaa();
      aaa2();
      try {
        aaa4();
      } catch(Exception ex) {
        aaa5();
      }
      """;
    assertEquals("Try/finally unwrapped with loose matching", expectedResult19Loose, replace(s52, s53, s54));


    String s55 = """
      for(Iterator<String> iterator = stringlist.iterator(); iterator.hasNext();) {
            String str = iterator.next();
            System.out.println( str );
      }""";
    String s56 = """
      for (Iterator<$Type$> $variable$ = $container$.iterator(); $variable$.hasNext();) {
          $Type$ $var$ = $variable$.next();
          $Statements$;
      }""";
    String s57 = """
      for($Type$ $var$:$container$) {
        $Statements$;
      }""";
    String expectedResult20 = """
      for(String str:stringlist) {
        System.out.println( str );
      }""";

    assertEquals("for with foreach", expectedResult20, replace(s55, s56, s57));

    String s58 = """
      class A {
        static Set<String> b_MAP = new HashSet<String>();
        int c;
      }""";
    String s59 = "'a:[ regex( (.*)_MAP ) ]";
    String s60 = "$a_1$_SET";
    String expectedResult21 = """
      class A {
        static Set<String> b_SET = new HashSet<String>();
        int c;
      }""";

    assertEquals("replace symbol in definition", expectedResult21, replace(s58, s59, s60));

    String s64 = "int x = 42;\n" +
                 "int y = 42; // Stuff";
    String s65 = "'_Type '_Variable = '_Value; // '_Comment";
    String s66 = """
      /**
       * $Comment$
       */
      $Type$ $Variable$ = $Value$;""";
    String expectedResult23 = """
      int x = 42;
      /**
       * Stuff
       */
      int y = 42;""";

    assertEquals("Replacement of the comment with javadoc", expectedResult23, replace(s64, s65, s66));

    String s61 = "try { 1=1; } catch(Exception e) { 1=1; } catch(Throwable t) { 2=2; }";
    String s62 = "try { '_a; } catch(Exception e) { '_b; }";
    String s63 = "try { $a$; } catch(Exception1 e) { $b$; } catch(Exception2 e) { $b$; }";
    String expectedResult22 = "try { 1=1; } catch(Exception1 e) { 1=1; } catch(Exception2 e) { 1=1; } catch(Throwable t) { 2=2; }";

    assertEquals("try replacement by another try will leave the unmatched catch", expectedResult22, replace(s61, s62, s63));
  }

  public void testReplaceExpr() {
    String s1 = "new SimpleDateFormat(\"yyyyMMddHHmmss\")";
    String s2 = "'expr";
    String s3 = "new AtomicReference<DateFormat>($expr$)";
    String expectedResult = "new AtomicReference<DateFormat>(new SimpleDateFormat(\"yyyyMMddHHmmss\"))";

    assertEquals("Replacement of top-level expression only", expectedResult, replace(s1, s2, s3));

    String s4 = "get(\"smth\")";
    String s5 = "'expr";
    String s6 = "new Integer($expr$)";
    String expectedResult1 = "new Integer(get(\"smth\"))";

    assertEquals("Replacement of top-level expression only", expectedResult1, replace(s4, s5, s6));

    String in = """
      class X {
        boolean x(int foo, int bar) {
          return foo - 1 < bar - 1;
        }
      }""";
    String what = "'_intA:[exprtype( *int|Integer )] - '_const < '_intB:[exprtype( *int|Integer )] - '_const";
    String by = "$intA$ < $intB$";
    String expected = """
      class X {
        boolean x(int foo, int bar) {
          return foo < bar;
        }
      }""";

    assertEquals("No errors in pattern", expected, replace(in, what, by));
  }

  public void testReplaceRecordComponents() {
    String in1 = """
      record Bar(String field, int i){}
      """;
    String ex1 = """
      record Bar(String field, int i) {} // comment
      """;
    assertEquals(ex1, replace(in1, "record '_Record('_Type '_component* ) {}", "record $Record$($Type$ $component$) {} // comment"));
  }

  public void testReplaceParameter() {
    String in1 = """
      class A {
        void b(int c,
               int d, /*1*/ int e) {}
      }""";

    String expected1a = """
      class A {
        void b(int c,
               int d2, /*1*/ int e) {}
      }""";
    assertEquals("replace method parameter", expected1a, replace(in1, "int d;", "int d2;"));

    String expected1b = """
      class A {
        void b(int /*!*/ c,
               int /*!*/ d, /*1*/ int /*!*/ e) {}
      }""";
    assertEquals(expected1b, replace(in1, "void b('_T '_v*);", "void b($T$ /*!*/ $v$);"));

    String expected1c = """
      class A {
        void /**/ b(int c,
               int d, /*1*/ int e) {}
      }""";
    assertEquals("replace multi match parameter", expected1c, replace(in1, "void b(int '_x*);", "void /**/ b(int $x$);"));

    String expected1d = """
      class A {
        void b(int c,
               int d, /*1*/ int e) {}
      void c(int c,
               int d, /*1*/ int e) {}
      }""";
    assertEquals("replace multiple occurrences of the same variable", expected1d, replace(in1, "void b('_T '_p*);", "void b($T$ $p$);\n" +
                                                                                                                    "void c($T$ $p$) {}"));

    String in2 = """
      class X {
        void x() {}
      }""";
    String expected2 = """
      class X {
        void /**/ x() {}
      }""";
    assertEquals("replace no match parameter", expected2, replace(in2, "void x(int '_a*);", "void /**/ x() {}"));

    String in3 = """
      class X {
        void x(String s, Integer i) {}
      }""";
    String expected3 = """
      class X {
        void x(List<String> /*>*/ s, List<Integer> /*>*/ i) {}
      }""";
    assertEquals(expected3, replace(in3, "void x('_T '_v*);", "void x(List<$T$> /*>*/ $v$);"));

    String in4 = """
      class X {
        void a(Map<String, Integer> b, Map<String, Integer> c) {}
      }""";
    String expected4 = "class X {\n" +
                       "  void a(Map<String, Integer> /*!*/ b, Map<, > /*!*/ c) {}\n" + // todo fix replacement of second parameter type
                       "}";
    assertEquals(expected4, replace(in4, "void a('_T<'_K, '_V> '_p*);", "void a($T$<$K$, $V$> /*!*/ $p$);"));
  }

  public void testReplaceWithComments() {
    String s1 = "map.put(key, value); // line 1";
    String s2 = "map.put(key, value); // line 1";
    String s3 = "map.put(key, value); // line 1";
    String expectedResult = "map.put(key, value); // line 1";

    assertEquals("replace self with comment after", expectedResult, replace(s1, s2, s3));

    String s4 = "if (true) System.out.println(\"1111\"); else System.out.println(\"2222\");\n" +
                "while(true) System.out.println(\"1111\");";
    String s5 = "System.out.println('_Test);";
    String s6 = "/* System.out.println($Test$); */";
    String expectedResult2 = "if (true) /* System.out.println(\"1111\"); */; else /* System.out.println(\"2222\"); */;\n" +
                             "while(true) /* System.out.println(\"1111\"); */;";
    assertEquals("replace with comment", expectedResult2, replace(s4, s5, s6));

    String source1 = """
      public class AnotherTestClass extends TestClass {
          /* Test comment */
          public void testMeThOd() {
              int x = 0;
          }
      }""";
    String what1 = """
      public class '_A extends '_B {
          /* '_Comment */
          public void '_Method() {
              '_statement;
          }
      }""";
    String by1 = """
      public class $A$ extends $B$ {
          /* $Comment$ */
          public void $NewMethod$() {
              $statement$;
          }
      }""";
    final ReplacementVariableDefinition variable = options.addNewVariableDefinition("NewMethod");
    variable.setScriptCodeConstraint("Method.name.toLowerCase()");
    assertEquals("""
                   public class AnotherTestClass extends TestClass {
                       /* Test comment */
                       public void testmethod() {
                           int x = 0;
                       }
                   }""", replace(source1, what1, by1));
  }

  public void testSeveralStatements() {
    String s1 = """
      {
              System.out.println(1);
              System.out.println(2);
              System.out.println(3);
            }
      {
              System.out.println(1);
              System.out.println(2);
              System.out.println(3);
            }
      {
              System.out.println(1);
              System.out.println(2);
              System.out.println(3);
            }""";
    String s2 =
      """
                System.out.println(1);
                System.out.println(2);
                System.out.println(3);
        """;
    String s3 = """
              System.out.println(3);
              System.out.println(2);
              System.out.println(1);
      """;
    String expectedResult1 = """
      {
          System.out.println(3);
          System.out.println(2);
          System.out.println(1);
      }
      {
          System.out.println(3);
          System.out.println(2);
          System.out.println(1);
      }
      {
          System.out.println(3);
          System.out.println(2);
          System.out.println(1);
      }""";
    options.setToReformatAccordingToStyle(true);
    assertEquals("three statements replacement", expectedResult1, replace(s1, s2, s3));
    options.setToReformatAccordingToStyle(false);

    String s4 = """
      ProgressManager.getInstance().startNonCancelableAction();
          try {
            read(id, READ_PARENT);
            return myViewport.parent;
          }
          finally {
            ProgressManager.getInstance().finishNonCancelableAction();
          }""";
    String s5 = """
      ProgressManager.getInstance().startNonCancelableAction();
          try {
            '_statement{2,2};
          }
          finally {
            ProgressManager.getInstance().finishNonCancelableAction();
          }""";
    String s6 = "$statement$;";
    String expectedResult2 = "read(id, READ_PARENT);\n" +
                             "      return myViewport.parent;";
    assertEquals("extra ;", expectedResult2, replace(s4, s5, s6));

    String s7 = """
      public class A {
          void f() {
              new Runnable() {
                  public void run() {
                      l();
                  }
      
                  private void l() {
                      int i = 9;
                      int j = 9;
                  }
              };
              new Runnable() {
                  public void run() {
                      l();
                  }
      
                  private void l() {
                      l();
                      l();
                  }
              };
          }
      
      }""";
    String s8 = """
      new Runnable() {
          public void run() {
              '_l ();
          }
          private void '_l () {
              '_st{2,2};
          }
      };""";
    String s9 = """
      new My() {
          public void f() {
              $st$;
          }
      };""";

    String expectedResult3 = """
      public class A {
          void f() {
              new My() {
                  public void f() {
                      int i = 9;
                      int j = 9;
                  }
              };
              new My() {
                  public void f() {
                      l();
                      l();
                  }
              };
          }
      
      }""";
    boolean formatAccordingToStyle = options.isToReformatAccordingToStyle();
    options.setToReformatAccordingToStyle(true);
    assertEquals("extra ; 2", expectedResult3, replace(s7, s8, s9));

    String s10 = """
      public class A {
          void f() {
              new Runnable() {
                  public void run() {
                      l();
                      l();
                  }
                  public void run2() {
                      l();
                      l();
                  }
      
              };
              new Runnable() {
                  public void run() {
                      l();
                      l();
                  }
                  public void run2() {
                      l();
                      l();
                  }
      
              };
      new Runnable() {
                  public void run() {
                      l();
                      l();
                  }
                  public void run2() {
                      l2();
                      l2();
                  }
      
              };
          }
      
          private void l() {
              int i = 9;
              int j = 9;
          }
      }
      
      abstract class My {
          abstract void f();
      }""";
    String s11 = """
      new Runnable() {
                  public void run() {
                      '_l{2,2};
                  }
                  public void run2() {
                      '_l;
                  }
      
              };""";
    String s12 = """
      new My() {
                  public void f() {
                      $l$;
                  }
              };""";
    String expectedResult4 = """
      public class A {
          void f() {
              new My() {
                  public void f() {
                      l();
                      l();
                  }
              };
              new My() {
                  public void f() {
                      l();
                      l();
                  }
              };
              new Runnable() {
                  public void run() {
                      l();
                      l();
                  }
      
                  public void run2() {
                      l2();
                      l2();
                  }
      
              };
          }
      
          private void l() {
              int i = 9;
              int j = 9;
          }
      }
      
      abstract class My {
          abstract void f();
      }""";

    assertEquals("same multiple occurrences 2 times", expectedResult4, replace(s10, s11, s12));

    options.setToReformatAccordingToStyle(formatAccordingToStyle);

    String s13 = """
          PsiLock.LOCK.acquire();
          try {
            return value;
          }
          finally {
            PsiLock.LOCK.release();
          }\
      """;
    String s13_2 = """
          PsiLock.LOCK.acquire();
          try {
            if (true) { return value; }
          }
          finally {
            PsiLock.LOCK.release();
          }\
      """;
    String s13_3 = """
          PsiLock.LOCK.acquire();
          try {
            if (true) { return value; }
      
            if (true) { return value; }
          }
          finally {
            PsiLock.LOCK.release();
          }\
      """;
    String s14 = """
          PsiLock.LOCK.acquire();
          try {
            '_T{1,1000};
          }
          finally {
            PsiLock.LOCK.release();
          }\
      """;
    String s15 = """
      synchronized(PsiLock.LOCK) {
        $T$;
      }""";

    String expectedResult5 = """
      synchronized (PsiLock.LOCK) {
          return value;
      }""";
    options.setToReformatAccordingToStyle(true);
    assertEquals("extra ; over return", expectedResult5, replace(s13, s14, s15));
    options.setToReformatAccordingToStyle(false);


    String expectedResult6 = """
      synchronized (PsiLock.LOCK) {
          if (true) {
              return value;
          }
      }""";
    options.setToReformatAccordingToStyle(true);
    assertEquals("extra ; over if", expectedResult6, replace(s13_2, s14, s15));
