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

import com.intellij.idea.TestFor;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.testFramework.LightProjectDescriptor;
import com.jetbrains.python.codeInsight.regexp.PythonRegexpParserDefinition;
import com.jetbrains.python.codeInsight.regexp.PythonVerboseRegexpLanguage;
import com.jetbrains.python.codeInsight.regexp.PythonVerboseRegexpParserDefinition;
import com.jetbrains.python.fixtures.PyLexerTestCase;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import org.intellij.lang.regexp.inspection.RegExpRedundantEscapeInspection;
import org.intellij.lang.regexp.inspection.RegExpSimplifiableInspection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;


public class PyRegexpTest extends PyTestCase {

  public void testUnicodePy3() {
    doTestHighlighting();
  }

  public void testCommentModeWhitespace() {
    myFixture.enableInspections(new RegExpSimplifiableInspection());
    doTestHighlighting();
  }

  public void testLookbehind() {
    doTestHighlighting();
  }

  public void testConditional() {
    doTestHighlighting();
  }
  
  public void testCharacterClasses() {
    doTestHighlighting();
  }

  public void testRedundantEscape() {
    myFixture.enableInspections(new RegExpRedundantEscapeInspection());
    doTestHighlighting();
  }

  public void testCountedQuantifier() {
    myFixture.enableInspections(new RegExpSimplifiableInspection());
    doTestHighlighting();
  }

  public void testNotEmptyGroup() { // PY-14381
    doTestHighlighting();
  }
  
  public void testNonSimplifiableCharacterClass() {
    myFixture.enableInspections(new RegExpSimplifiableInspection());
    doTestHighlighting("""
                           import re
                           re.compile("[[]")""");
  }

  public void testNestedCharacterClasses() {  // PY-2908
    doTestHighlighting();
  }

  public void testNestedCharacterClassesLexer() {
    doTestLexer("[][]", "CLASS_BEGIN", "CHARACTER", "CHARACTER", "CLASS_END");
  }

  public void testNestedCharacterClasses2() {  // PY-2908
    doTestHighlighting();
  }

  public void testOctal() {  // PY-2906
    doTestHighlighting();
  }

  public void testBraceInPythonCharacterClass() {  // PY-1929
    doTestHighlighting();
  }

  public void testNegatedBraceInCharacterClass() {
    doTestLexer("[^][]", "CLASS_BEGIN", "CARET", "CHARACTER", "CHARACTER", "CLASS_END");
  }

  public void testDanglingMetacharacters() {  // PY-2430
    doTestHighlighting();
  }

  public void testVerbose() {
    Lexer lexer = new PythonVerboseRegexpParserDefinition().createLexer(myFixture.getProject());
    PyLexerTestCase.doLexerTest("# abc", lexer, "COMMENT");
  }

  public void testRedundantEscapeSingleQuote() {  // PY-5027
    doTestHighlighting();
  }

  public void testBraceCommaN() {  // PY-8304
    doTestHighlighting();
  }

  public void testVerboseAsKwArg() {  // PY-8143
    doTestHighlighting();
  }

  public void testVerboseEscapedHash() {  // PY-6545
    doTestHighlighting();
  }

  public void testDoubleOpenCurly() {  // PY-8252
    doTestHighlighting();
  }

  public void testReSubNotRegexp() {  // PY-11069
    doTestHighlighting();
  }

  public void testSingleStringRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         re.search('<caret>.*bar',
                                   'foobar')
                         """,
                       ".*bar");
  }

  // PY-11057
  public void testAdjacentStringRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         re.search('<caret>.*()'
                                   'abc',
                                   'foo')
                         """,
                       ".*()abc");
  }

  public void testParenthesizedStringRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         re.search((('<caret>foo')), 'foobar')
                         """,
                       "foo");
  }

  public void testConcatStringRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         re.search('<caret>(.*' + 'bar)' + 'baz', 'foobar')
                         """,
                       "(.*bar)baz");
  }

  public void testConcatStringWithValuesRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         def f(x, y):
                             re.search('<caret>.*(' + x + ')' + y, 'foo')
                         """,
                       ".*(missing_value)missing_value");
  }

  public void testPercentFormattingRegexpAutoInjection() {
    doTestInjectedText("""
                         import re\s

                         def f(x, y):
                             re.search('<caret>.*%s-%d' % (x, y), 'foo')
                         """,
                       ".*missing_value-missing_value");
  }

  public void testNewStyleFormattingRegexpAutoInjection() {
    doTestInjectedText("""
                         import re

                         def f(x, y):
                             re.search('<caret>.*{foo}-{}'.format(x, foo=y), 'foo')
                         """,
                       ".*missing_value-missing_value");
  }

  public void testNewStyleFormattingEndsWithConstant() {
    doTestInjectedText("""
                         import re

                         def f(**kwargs):    re.search('<caret>(foo{bar}baz$)'.format(**kwargs), 'foo')
                         """,
                       "(foomissing_valuebaz$)");
  }

  // PY-21493
  public void testFStringSingleStringRegexpFragmentFirst() {
    doTestInjectedText("""
                         import re

                         re.search(rf'{42}.<caret>*{42}', 'foo')""", "missing_value.*missing_value");
  }

  // PY-21493
  public void testFStringSingleStringRegexpFirstFragmentInMiddle() {
    doTestInjectedText("""
                         import re

                         re.search(rf'<caret>.*{42}.*{42}', 'foo')""", ".*missing_value.*missing_value");
  }

  // PY-21493
  public void testFStringMultiStringRegexp() {
    doTestInjectedText("""
                         import re

                         re.search(rf'<caret>.*{42}'
                                   r'.*{42}.*'
                                   rf'{42}.*', 'foo')""", ".*missing_value.*{42}.*missing_value.*");
  }

  // PY-21493
  public void testFStringSingleStringIncompleteFragment() {
    doTestInjectedText("""
                         import re

                         re.search(rf'<caret>.*{42.*', 'foo')""", ".*missing_value");
  }

  // PY-21493
  public void testFStringSingleStringNestedFragments() {
    doTestInjectedText("""
                         import re

                         re.search(rf'<caret>.*{42:{42}}.*{42}', 'foo')""", ".*missing_value.*missing_value");
  }

  // PY-18881
  public void testVerboseSyntaxWithShortFlag() {
    final PsiElement element =
      doTestInjectedText("""
                           import re

                           re.search(""\"
                           .* # <caret>comment
                           ""\", re.I | re.M | re.X)""",
                         "\n.* # comment\n");
    assertEquals(PythonVerboseRegexpLanguage.INSTANCE, element.getLanguage());
  }

  // PY-16404
  public void testFullmatchPy3() {
    doTestInjectedText("import re\n" +
                       "re.fullmatch(\"<caret>\\w+\", \"string\"",
                       "\\w+");
  }

  @TestFor(issues="PY-61777")
  public void testPossessiveQuantifierSupportedPy3() {
    runWithLanguageLevel(LanguageLevel.PYTHON311, () ->
      doTestHighlighting("""
                           import re
                           re.compile("a++ b*+ c?+")""")
    );
  }

  @TestFor(issues="PY-61777")
  public void testPossessiveQuantifierUnsupportedPy3() {
    runWithLanguageLevel(LanguageLevel.PYTHON310, () ->
      doTestHighlighting("""
                           import re
                           re.compile("a+<error descr="Nested quantifier in regexp">+</error> b*<error descr="Nested quantifier in regexp">+</error> c?<error descr="Nested quantifier in regexp">+</error>")""")
    );
  }

  @TestFor(issues="PY-61777")
  public void testAtomicGroupSupportedPy3() {
    runWithLanguageLevel(LanguageLevel.PYTHON311, () ->
      doTestHighlighting("""
                           import re
                           re.compile("(?>a+)")""")
    );
  }

  @TestFor(issues="PY-61777")
  public void testAtomicGroupUnsupportedPy3() {
    runWithLanguageLevel(LanguageLevel.PYTHON310, () ->
      doTestHighlighting("""
                           import re
                           re.compile("<error descr="Atomic groups are not supported in this regex dialect">(?>a+)</error>")""")
    );
  }

  @TestFor(issues="PY-35730")
  public void testGroupNameIsValidIdentifier() {
    //noinspection NonAsciiCharacters
    doTestHighlighting(
      """
        import re
        re.compile("(?P<水>水)")  # non-ascii is a valid group name, a warning will be reported by `NonAsciiCharactersInspection`
        re.compile("(?P<𝕏>水)")  # ok character outside the BMP
        
        # broken case IJPL-217664
        # re.compile("(?Perror descr="Invalid group name">😀</error>>水)")  # bad character outside the BMP
        
        re.compile("(?P<<error descr="Group name expected"> </error>a>水)")
        re.compile("(?P<<error descr="Group name expected">0</error>a>水)")
        re.compile("(?P<<error descr="Invalid group name">a$b</error>>水)")
        """);
  }

  @Nullable
  @Override
  protected LightProjectDescriptor getProjectDescriptor() {
    return getName().endsWith("Py3") ? super.getProjectDescriptor() : ourPy2Descriptor;
  }

  @NotNull
  private PsiElement doTestInjectedText(@NotNull String text, @NotNull String expected) {
    myFixture.configureByText(PythonFileType.INSTANCE, text);
    final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
    final PsiLanguageInjectionHost host = languageManager.getInjectionHost(getElementAtCaret());
    assertNotNull(host);
    final List<Pair<PsiElement, TextRange>> files = languageManager.getInjectedPsiFiles(host);
    assertNotNull(files);
    assertFalse(files.isEmpty());
    final PsiElement injected = files.get(0).getFirst();
    assertEquals(expected, injected.getText());
    return injected;
  }

  private void doTestHighlighting() {
    myFixture.testHighlighting(true, false, true, "regexp/" + getTestName(true) + ".py");
  }

  private void doTestHighlighting(String text) {
    myFixture.configureByText("test.py", text);
    myFixture.testHighlighting(true, false, true, "test.py");
  }

  private void doTestLexer(final String text, String... expectedTokens) {
    Lexer lexer = new PythonRegexpParserDefinition().createLexer(myFixture.getProject());
    PyLexerTestCase.doLexerTest(text, lexer, expectedTokens);
  }
}
