import com.intellij.mock.MockVirtualFile
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.util.ui.UIUtil
import it.unimi.dsi.fastutil.ints.IntArrayList
import junit.framework.TestCase
import org.acejump.action.AceAction
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.WHOLE_FILE
import org.acejump.input.JumpMode
import org.acejump.search.Pattern.ALL_WORDS
import org.acejump.session.AceJumpListener
import org.acejump.session.SessionManager
import org.acejump.test.util.BaseTest

/**
 * Test [org.acejump.ExternalUsage] endpoints.
 */

class ExternalUsageTest: BaseTest() {
  fun `test externally tagged results and listener notification`() {
    makeEditor("test externally tagged results")

    SessionManager.start(myFixture.editor).markResults(sortedSetOf(4, 10, 15))

    TestCase.assertEquals(3, session.tags.size)

    var shouldBeTrueAfterFinished = false
    session.addAceJumpListener(object: AceJumpListener {
      override fun finished(mark: String?, query: String?) {
        shouldBeTrueAfterFinished = true
      }
    })

    typeAndWaitForResults(session.tags[0].key)

    TestCase.assertTrue(shouldBeTrueAfterFinished)
  }
  
  fun `test externally tagged results with multiple editors`() {
    val fileA = MockVirtualFile("a.txt", "first file")
    val fileB = MockVirtualFile("b.txt", "second file with more markers")
    manager?.openFile(fileA, true)
    manager?.openFile(fileB, false)
    
    val mainEditor = (manager?.selectedEditor as TextEditor).editor
    val editorA = (manager?.getEditors(fileA)?.single() as TextEditor).editor
    val editorB = (manager?.getEditors(fileB)?.single() as TextEditor).editor
    
    val session = SessionManager.start(mainEditor, listOf(editorA, editorB))
    
    session.markResults(mapOf(
      editorA to IntArrayList(intArrayOf(0, 6)),
      editorB to IntArrayList(intArrayOf(0, 7, 22))
    ))
  
    TestCase.assertEquals(5, session.tags.size)
    TestCase.assertEquals(2, session.tags.count { it.value.editor === editorA })
    TestCase.assertEquals(3, session.tags.count { it.value.editor === editorB })
    
    TestCase.assertEquals(listOf(0, 6), session.tags
      .filter { it.value.editor === editorA }
      .map { it.value.offset }
      .sorted())
    
    TestCase.assertEquals(listOf(0, 7, 22), session.tags
      .filter { it.value.editor === editorB }
      .map { it.value.offset }
      .sorted())
  }

  fun `test external pattern usage`() {
    makeEditor("test external pattern usage")

    SessionManager.start(myFixture.editor)
      .startRegexSearch(ALL_WORDS, WHOLE_FILE)

    TestCase.assertEquals(4, session.tags.size)
  }

  fun `test external regex usage`() {
    makeEditor("test external regex usage")

    SessionManager.start(myFixture.editor)
      .startRegexSearch("[aeiou]+", WHOLE_FILE)

    TestCase.assertEquals(9, session.tags.size)
  }

  fun `test external jump with bounds`() {
    makeEditor("test word and word usage")

    SessionManager.start(myFixture.editor)
      .toggleJumpMode(JumpMode.JUMP, object : Boundaries {
        override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
          return 14..18
        }

        override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
          return offset in 14..18
        }
      })

    typeAndWaitForResults("word")

    TestCase.assertEquals(1, session.tags.size)
    TestCase.assertEquals(14, session.tags.single().value.offset)
  }

  fun `test listener query and mark`() {
    "<caret>testing 1234".search("g")

    var detectedMark: String? = null
    var detectedQuery: String? = null
    session.addAceJumpListener(object: AceJumpListener {
      override fun finished(mark: String?, query: String?) {
        detectedMark = mark
        detectedQuery = query
      }
    })

    val mark = session.tags[0].key
    typeAndWaitForResults(mark)

    TestCase.assertEquals(mark, detectedMark)
    TestCase.assertEquals("g", detectedQuery)
  }

  fun `test listener after escape`() {
    "<caret>testing 1234".search("g")

    var detectedMark: String? = null
    var detectedQuery: String? = null
    session.addAceJumpListener(object: AceJumpListener {
      override fun finished(mark: String?, query: String?) {
        detectedMark = mark
        detectedQuery = query
      }
    })

    myFixture.performEditorAction("EditorEscape")
    UIUtil.dispatchAllInvocationEvents()

    TestCase.assertEquals(null, detectedMark)
    TestCase.assertEquals(null, detectedQuery)
  }

  fun `test listener for word motion`() {
    makeEditor("test word action")

    takeAction(AceAction.StartAllWordsMode())

    var detectedMark: String? = null
    var detectedQuery: String? = null
    session.addAceJumpListener(object: AceJumpListener {
      override fun finished(mark: String?, query: String?) {
        detectedMark = mark
        detectedQuery = query
      }
    })

    val mark = session.tags[1].key
    typeAndWaitForResults(mark)

    TestCase.assertEquals(mark, detectedMark)
    TestCase.assertEquals("", detectedQuery)
  }

  fun `test do not remove other highlights when the session ends`() {
    makeEditor("test do not remove other highlights when the session ends")

    val markupModel = myFixture.editor.markupModel
    val layer = HighlighterLayer.SELECTION - 1
    val existedHighlighter = markupModel.addRangeHighlighter(0, 1, layer, null, HighlighterTargetArea.EXACT_RANGE)

    takeAction(AceAction.StartAllWordsMode())
    val mark = session.tags[0].key
    typeAndWaitForResults(mark)

    TestCase.assertEquals("last session should be disposed", null, SessionManager[myFixture.editor])
    TestCase.assertTrue("existed highlighter should not be removed", existedHighlighter.isValid)

    existedHighlighter.dispose()
  }
}