// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package org.jetbrains.kotlin.idea.quickfix

import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.quickFix.ActionHint
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.codeInspection.InspectionEP
import com.intellij.codeInspection.LocalInspectionEP
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.CharsetToolkit
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl
import com.intellij.util.ArrayUtil
import com.intellij.util.PathUtil
import com.intellij.util.ThrowableRunnable
import junit.framework.ComparisonFailure
import junit.framework.TestCase
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.quickfix.utils.findInspectionFile
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.runAll
import org.jetbrains.kotlin.idea.test.withCustomCompilerOptions
import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.Directives
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.TestFiles
import java.io.File
import java.util.regex.Pattern

abstract class AbstractQuickFixMultiFileTest : KotlinLightCodeInsightFixtureTestCase() {
    protected open fun doTestWithExtraFile(beforeFileName: String) {
        enableInspections(beforeFileName)

        if (beforeFileName.endsWith(".test")) {
            doMultiFileTest(beforeFileName)
        } else {
            doTest(beforeFileName)
        }
    }

    private fun enableInspections(beforeFileName: String) {
        val inspectionFile = findInspectionFile(File(beforeFileName).parentFile)
        if (inspectionFile != null) {
            val className = FileUtil.loadFile(inspectionFile).trim { it <= ' ' }
            val inspectionClass = Class.forName(className)
            enableInspectionTools(inspectionClass)
        }
    }

    private fun enableInspectionTools(klass: Class<*>) {
        val eps = mutableListOf<InspectionEP>().apply {
            addAll(LocalInspectionEP.LOCAL_INSPECTION.extensions)
            addAll(InspectionEP.GLOBAL_INSPECTION.extensions)
        }

        val tool = eps.firstOrNull { it.implementationClass == klass.name }?.instantiateTool()
            ?: error("Could not find inspection tool for class: $klass")

        myFixture.enableInspections(tool)
    }

    override fun setUp() {
        super.setUp()
        CodeInsightSettings.getInstance().EXCLUDED_PACKAGES = arrayOf("excludedPackage", "somePackage.ExcludedClass")
    }

    override fun tearDown() {
        runAll(
            ThrowableRunnable { CodeInsightSettings.getInstance().EXCLUDED_PACKAGES = ArrayUtil.EMPTY_STRING_ARRAY },
            ThrowableRunnable { super.tearDown() }
        )
    }

    /**
     * @param subFiles   subFiles of multiFile test
     * *
     * @param beforeFile will be added last, as subFiles are dependencies of it
     */
    private fun configureMultiFileTest(subFiles: List<TestFile>, beforeFile: TestFile): List<VirtualFile> {
        val vFiles = subFiles.map(this::createTestFile).toMutableList()
        val beforeVFile = createTestFile(beforeFile)
        vFiles.add(beforeVFile)
        myFixture.configureFromExistingVirtualFile(beforeVFile)
        TestCase.assertEquals(guessFileType(beforeFile), myFixture.file.virtualFile.fileType)

        TestCase.assertTrue("\"<caret>\" is probably missing in file \"" + beforeFile.path + "\"", myFixture.editor.caretModel.offset != 0)
        return vFiles
    }

    private fun createTestFile(testFile: TestFile): VirtualFile {
        return runWriteAction {
            val vFile = myFixture.tempDirFixture.createFile(testFile.path)
            vFile.charset = CharsetToolkit.UTF8_CHARSET
            VfsUtil.saveText(vFile, testFile.content)
            vFile
        }
    }

    private fun doMultiFileTest(beforeFileName: String) {
        val multiFileText = FileUtil.loadFile(File(beforeFileName), true)

        val subFiles = TestFiles.createTestFiles(
            "single.kt",
            multiFileText,
            object : TestFiles.TestFileFactoryNoModules<TestFile>() {
                override fun create(fileName: String, text: String, directives: Directives): TestFile {
                    val linesWithoutDirectives = text.lines().filter {
                        !it.startsWith("// LANGUAGE_VERSION") && !it.startsWith("// FILE")
                    }
                    return TestFile(fileName, linesWithoutDirectives.joinToString(separator = "\n"))
                }
            }
        )

        val afterFile = subFiles.firstOrNull { file -> file.path.contains(".after") }
        val beforeFile = subFiles.firstOrNull { file -> file.path.contains(".before") }!!

        subFiles.remove(beforeFile)
        if (afterFile != null) {
            subFiles.remove(afterFile)
        }

        configureMultiFileTest(subFiles, beforeFile)
        withCustomCompilerOptions(multiFileText, project, module) {
            project.executeCommand("") {
                try {
                    val psiFile = file

                    val actionHint = ActionHint.parse(psiFile, beforeFile.content)
                    val text = actionHint.expectedText

                    val actionShouldBeAvailable = actionHint.shouldPresent()

                    if (psiFile is KtFile) {
                        DirectiveBasedActionUtils.checkForUnexpectedErrors(psiFile)
                    }

                    doAction(
                        text,
                        file,
                        editor,
                        actionShouldBeAvailable,
                        getTestName(false),
                        this::availableActions,
                        myFixture::doHighlighting,
                    )

                    val actualText = file.text
                    val afterText = StringBuilder(actualText).insert(editor.caretModel.offset, "<caret>").toString()

                    if (actionShouldBeAvailable) {
                        TestCase.assertNotNull(".after file should exist", afterFile)
                        if (afterText != afterFile!!.content) {
                            val actualTestFile = StringBuilder()
                            if (multiFileText.startsWith("// LANGUAGE_VERSION")) {
                                actualTestFile.append(multiFileText.lineSequence().first())
                            }

                            actualTestFile.append("// FILE: ").append(beforeFile.path).append("\n").append(beforeFile.content)
                            for (file in subFiles) {
                                actualTestFile.append("// FILE: ").append(file.path).append("\n").append(file.content)
                            }
                            actualTestFile.append("// FILE: ").append(afterFile.path).append("\n").append(afterText)

                            KotlinTestUtils.assertEqualsToFile(File(beforeFileName), actualTestFile.toString())
                        }
                    } else {
                        TestCase.assertNull(".after file should not exist", afterFile)
                    }
                } catch (e: ComparisonFailure) {
                    throw e
                } catch (e: AssertionError) {
                    throw e
                } catch (e: Throwable) {
                    e.printStackTrace()
                    TestCase.fail(getTestName(true))
                }
            }
        }

    }

    private fun doTest(beforeFileName: String) {
        val mainFile = File(beforeFileName)
        val originalFileText = FileUtil.loadFile(mainFile, true)
        val mainFileDir = mainFile.parentFile!!

        val mainFileName = mainFile.name
        val extraFiles = mainFileDir.listFiles { _, name ->
            name.startsWith(extraFileNamePrefix(mainFileName))
                    && name != mainFileName
                    && PathUtil.getFileExtension(name).let { it == "kt" || it == "java" || it == "groovy" }
        }!!

        val testFiles = ArrayList<String>()
        testFiles.add(mainFile.name)
        extraFiles.mapTo(testFiles) { file -> file.name }

        myFixture.configureByFiles(*testFiles.toTypedArray())

        withCustomCompilerOptions(originalFileText, project, module) {
            project.executeCommand("") {
                try {
                    val psiFile = file

                    val actionHint = ActionHint.parse(psiFile, originalFileText)
                    val text = actionHint.expectedText

                    val actionShouldBeAvailable = actionHint.shouldPresent()

                    if (psiFile is KtFile) {
                        DirectiveBasedActionUtils.checkForUnexpectedErrors(psiFile)
                    }

                    doAction(
                        text,
                        file,
                        editor,
                        actionShouldBeAvailable,
                        beforeFileName,
                        this::availableActions,
                        myFixture::doHighlighting,
                    )

                    if (actionShouldBeAvailable) {
                        val afterFilePath = beforeFileName.replace(".before.Main.", ".after.")
                        try {
                            myFixture.checkResultByFile(mainFile.name.replace(".before.Main.", ".after."))
                        } catch (e: ComparisonFailure) {
                            KotlinTestUtils.assertEqualsToFile(File(afterFilePath), editor)
                        }

                        for (file in myFixture.file.containingDirectory.files) {
                            val fileName = file.name
                            if (fileName == myFixture.file.name || !fileName.startsWith(
                                    extraFileNamePrefix(
                                        myFixture.file
                                            .name,
                                    ),
                                )
                            ) continue

                            val extraFileFullPath = beforeFileName.replace(myFixture.file.name, fileName)
                            val afterFile = File(extraFileFullPath.replace(".before.", ".after."))
                            if (afterFile.exists()) {
                                KotlinTestUtils.assertEqualsToFile(afterFile, file.text)
                            } else {
                                KotlinTestUtils.assertEqualsToFile(File(extraFileFullPath), file.text)
                            }
                        }
                    }
                } catch (e: ComparisonFailure) {
                    throw e
                } catch (e: AssertionError) {
                    throw e
                } catch (e: Throwable) {
                    e.printStackTrace()
                    TestCase.fail(getTestName(true))
                }
            }
        }
    }

    private val availableActions: List<IntentionAction>
        get() {
            myFixture.doHighlighting()
            val intentions = ShowIntentionActionsHandler.calcIntentions(project, editor, file)
            val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
            cachedIntentions.wrapAndUpdateGutters()
            return cachedIntentions.allActions.map { it.action }
        }

    class TestFile internal constructor(val path: String, val content: String)

    companion object {
        private fun getActionsTexts(availableActions: List<IntentionAction>): List<String> =
            availableActions.map(IntentionAction::getText)

        private fun extraFileNamePrefix(mainFileName: String): String =
            mainFileName.replace(".Main.kt", ".").replace(".Main.java", ".")

        protected fun guessFileType(file: TestFile): FileType = when {
            file.path.contains("." + KotlinFileType.EXTENSION) -> KotlinFileType.INSTANCE
            file.path.contains("." + JavaFileType.DEFAULT_EXTENSION) -> JavaFileType.INSTANCE
            else -> PlainTextFileType.INSTANCE
        }

        private fun findActionByPattern(pattern: Pattern, availableActions: List<IntentionAction>): IntentionAction? =
            availableActions.firstOrNull { pattern.matcher(it.text).matches() }

        fun doAction(
            text: String,
            file: PsiFile,
            editor: Editor,
            actionShouldBeAvailable: Boolean,
            testFilePath: String,
            getAvailableActions: () -> List<IntentionAction>,
            doHighlighting: () -> List<HighlightInfo>,
            shouldBeAvailableAfterExecution: Boolean = false
        ) {
            val pattern = if (text.startsWith("/"))
                Pattern.compile(text.substring(1, text.length - 1))
            else
                Pattern.compile(StringUtil.escapeToRegexp(text))

            val availableActions = getAvailableActions()
            val action = findActionByPattern(pattern, availableActions)

            if (action == null) {
                if (actionShouldBeAvailable) {
                    val texts = getActionsTexts(availableActions)
                    val infos = doHighlighting()
                    TestCase.fail(
                        "Action with text '" + text + "' is not available in test " + testFilePath + "\n" +
                                "Available actions (" + texts.size + "): \n" +
                                StringUtil.join(texts, "\n") +
                                "\nActions:\n" +
                                StringUtil.join(availableActions, "\n") +
                                "\nInfos:\n" +
                                StringUtil.join(infos, "\n")
                    )
                } else {
                    DirectiveBasedActionUtils.checkAvailableActionsAreExpected(file, availableActions)
                }
            } else {
                if (!actionShouldBeAvailable) {
                    TestCase.fail("Action '$text' is available (but must not) in test $testFilePath")
                }

                CodeInsightTestFixtureImpl.invokeIntention(action, file, editor)

                if (!shouldBeAvailableAfterExecution) {
                    val afterAction = findActionByPattern(pattern, getAvailableActions())

                    if (afterAction != null) {
                        TestCase.fail("Action '$text' is still available after its invocation in test $testFilePath")
                    }
                }
            }
        }
    }
}
