# Install frontend event handlers.
installListeners()
installListener = (element, event, callback) ->
  element.addEventListener event, (-> callback.apply(this, arguments)), true
# A count of the number of keyboard events received by the page (for the most recently-sent keystroke).  E.g.,
# we expect 3 if the keystroke is passed through (keydown, keypress, keyup), and 0 if it is suppressed.
pageKeyboardEventCount = 0
sendKeyboardEvent = (key) ->
  pageKeyboardEventCount = 0
  response = window.callPhantom
    request: "keyboard"
    key: key
# These listeners receive events after the main frontend listeners, and do not receive suppressed events.
for type in [ "keydown", "keypress", "keyup" ]
  installListener window, type, (event) ->
    pageKeyboardEventCount += 1
# Some tests have side effects on the handler stack and the active mode, so these are reset on setup.
initializeModeState = ->
  Mode.reset()
  handlerStack.reset()
  initializeModes()
  # We use "m" as the only mapped key, "p" as a passkey, and "u" as an unmapped key.
  refreshCompletionKeys
    completionKeys: "mp"
  handlerStack.bubbleEvent "registerStateChange",
    enabled: true
    passKeys: "p"
  handlerStack.bubbleEvent "registerKeyQueue",
    keyQueue: ""
#
# Retrieve the hint markers as an array object.
#
getHintMarkers = ->
  Array::slice.call document.getElementsByClassName("vimiumHintMarker"), 0
#
# Generate tests that are common to both default and filtered
# link hinting modes.
#
createGeneralHintTests = (isFilteredMode) ->
  context "Link hints",
    setup ->
      initializeModeState()
      testContent = "test" + "tress"
      document.getElementById("test-div").innerHTML = testContent
      stub settings.values, "filterLinkHints", false
      stub settings.values, "linkHintCharacters", "ab"
    tearDown ->
      document.getElementById("test-div").innerHTML = ""
    should "create hints when activated, discard them when deactivated", ->
      LinkHints.activateMode()
      assert.isFalse not LinkHints.hintMarkerContainingDiv?
      LinkHints.deactivateMode()
      assert.isTrue not LinkHints.hintMarkerContainingDiv?
    should "position items correctly", ->
      assertStartPosition = (element1, element2) ->
        assert.equal element1.getClientRects()[0].left, element2.getClientRects()[0].left
        assert.equal element1.getClientRects()[0].top, element2.getClientRects()[0].top
      stub document.body, "style", "static"
      LinkHints.activateMode()
      hintMarkers = getHintMarkers()
      assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
      assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
      LinkHints.deactivateMode()
      stub document.body.style, "position", "relative"
      LinkHints.activateMode()
      hintMarkers = getHintMarkers()
      assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
      assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
      LinkHints.deactivateMode()
createGeneralHintTests false
createGeneralHintTests true
inputs = []
context "Test link hints for focusing input elements correctly",
  setup ->
    initializeModeState()
    testDiv = document.getElementById("test-div")
    testDiv.innerHTML = ""
    stub settings.values, "filterLinkHints", false
    stub settings.values, "linkHintCharacters", "ab"
    # Every HTML5 input type except for hidden. We should be able to activate all of them with link hints.
    inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file",
      "image", "month", "number", "password", "radio", "range", "reset", "search", "submit", "tel", "text",
      "time", "url", "week"]
    for type in inputTypes
      input = document.createElement "input"
      input.type = type
      testDiv.appendChild input
      inputs.push input
  tearDown ->
    document.getElementById("test-div").innerHTML = ""
  should "Focus each input when its hint text is typed", ->
    for input in inputs
      input.scrollIntoView() # Ensure the element is visible so we create a link hint for it.
      activeListener = ensureCalled (event) ->
        input.blur() if event.type == "focus"
      input.addEventListener "focus", activeListener, false
      input.addEventListener "click", activeListener, false
      LinkHints.activateMode()
      [hint] = getHintMarkers().filter (hint) -> input == hint.clickableItem
      sendKeyboardEvent char for char in hint.hintString
      input.removeEventListener "focus", activeListener, false
      input.removeEventListener "click", activeListener, false
context "Alphabetical link hints",
  setup ->
    initializeModeState()
    stub settings.values, "filterLinkHints", false
    stub settings.values, "linkHintCharacters", "ab"
    # Three hints will trigger double hint chars.
    createLinks 3
    LinkHints.init()
    LinkHints.activateMode()
  tearDown ->
    LinkHints.deactivateMode()
    document.getElementById("test-div").innerHTML = ""
  should "label the hints correctly", ->
    # TODO(philc): This test verifies the current behavior, but the current behavior is incorrect.
    # The output here should be something like aa, ab, b.
    hintMarkers = getHintMarkers()
    expectedHints = ["aa", "ba", "ab"]
    for hint, i in expectedHints
      assert.equal hint, hintMarkers[i].hintString
  should "narrow the hints", ->
    hintMarkers = getHintMarkers()
    sendKeyboardEvent "A"
    assert.equal "none", hintMarkers[1].style.display
    assert.equal "", hintMarkers[0].style.display
context "Filtered link hints",
  setup ->
    stub settings.values, "filterLinkHints", true
    stub settings.values, "linkHintNumbers", "0123456789"
  context "Text hints",
    setup ->
      initializeModeState()
      testContent = "test" + "tress" + "trait" + "track"
      document.getElementById("test-div").innerHTML = testContent
      LinkHints.init()
      LinkHints.activateMode()
    tearDown ->
      document.getElementById("test-div").innerHTML = ""
      LinkHints.deactivateMode()
    should "label the hints", ->
      hintMarkers = getHintMarkers()
      for i in [0...4]
        assert.equal (i + 1).toString(), hintMarkers[i].textContent.toLowerCase()
    should "narrow the hints", ->
      hintMarkers = getHintMarkers()
      sendKeyboardEvent "T"
      sendKeyboardEvent "R"
      assert.equal "none", hintMarkers[0].style.display
      assert.equal "1", hintMarkers[1].hintString
      assert.equal "", hintMarkers[1].style.display
      sendKeyboardEvent "A"
      assert.equal "2", hintMarkers[3].hintString
  context "Image hints",
    setup ->
      initializeModeState()
      testContent = "
        
" + "
"
      document.getElementById("test-div").innerHTML = testContent
      LinkHints.activateMode()
    tearDown ->
      document.getElementById("test-div").innerHTML = ""
      LinkHints.deactivateMode()
    should "label the images", ->
      hintMarkers = getHintMarkers()
      assert.equal "1: alt text", hintMarkers[0].textContent.toLowerCase()
      assert.equal "2: alt text", hintMarkers[1].textContent.toLowerCase()
      assert.equal "3: some title", hintMarkers[2].textContent.toLowerCase()
      assert.equal "4", hintMarkers[3].textContent.toLowerCase()
  context "Input hints",
    setup ->
      initializeModeState()
      testContent = "
        a label
        
        a label: "
      document.getElementById("test-div").innerHTML = testContent
      LinkHints.activateMode()
    tearDown ->
      document.getElementById("test-div").innerHTML = ""
      LinkHints.deactivateMode()
    should "label the input elements", ->
      hintMarkers = getHintMarkers()
      assert.equal "1", hintMarkers[0].textContent.toLowerCase()
      assert.equal "2", hintMarkers[1].textContent.toLowerCase()
      assert.equal "3", hintMarkers[2].textContent.toLowerCase()
      assert.equal "4: a label", hintMarkers[3].textContent.toLowerCase()
      assert.equal "5: a label", hintMarkers[4].textContent.toLowerCase()
context "Input focus",
  setup ->
    initializeModeState()
    testContent = "
      "
    document.getElementById("test-div").innerHTML = testContent
  tearDown ->
    document.getElementById("test-div").innerHTML = ""
  should "focus the first element", ->
    focusInput 1
    assert.equal "first", document.activeElement.id
  should "focus the nth element", ->
    focusInput 100
    assert.equal "third", document.activeElement.id
  should "activate insert mode on the first element", ->
    focusInput 1
    assert.isTrue InsertMode.permanentInstance.isActive()
  should "activate insert mode on the first element", ->
    focusInput 100
    assert.isTrue InsertMode.permanentInstance.isActive()
  should "activate the most recently-selected input if the count is 1", ->
    focusInput 3
    focusInput 1
    assert.equal "third", document.activeElement.id
  should "not trigger insert if there are no inputs", ->
    document.getElementById("test-div").innerHTML = ""
    focusInput 1
    assert.isFalse InsertMode.permanentInstance.isActive()
# TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has
# a tighter contract than goNext(), since they test minor aspects of goNext()'s link matching behavior, and we
# don't need to construct external state many times over just to test that.
# i.e. these tests should look something like:
# assert.equal(findLink(html("))[0].href, "first")
# These could then move outside of the dom_tests file.
context "Find prev / next links",
  setup ->
    initializeModeState()
    window.location.hash = ""
  should "find exact matches", ->
    document.getElementById("test-div").innerHTML = """
    nextcorrupted
    next page
    """
    stub settings.values, "nextPatterns", "next"
    goNext()
    assert.equal '#second', window.location.hash
  should "match against non-word patterns", ->
    document.getElementById("test-div").innerHTML = """
    >>
    """
    stub settings.values, "nextPatterns", ">>"
    goNext()
    assert.equal '#first', window.location.hash
  should "favor matches with fewer words", ->
    document.getElementById("test-div").innerHTML = """
    lorem ipsum next
    next!
    """
    stub settings.values, "nextPatterns", "next"
    goNext()
    assert.equal '#second', window.location.hash
  should "find link relation in header", ->
    document.getElementById("test-div").innerHTML = """
    
    """
    goNext()
    assert.equal '#first', window.location.hash
  should "favor link relation to text matching", ->
    document.getElementById("test-div").innerHTML = """
    
    next
    """
    goNext()
    assert.equal '#first', window.location.hash
  should "match mixed case link relation", ->
    document.getElementById("test-div").innerHTML = """
    
    """
    goNext()
    assert.equal '#first', window.location.hash
createLinks = (n) ->
  for i in [0...n] by 1
    link = document.createElement("a")
    link.textContent = "test"
    document.getElementById("test-div").appendChild link
context "Normal mode",
  setup ->
    initializeModeState()
  should "suppress mapped keys", ->
    sendKeyboardEvent "m"
    assert.equal pageKeyboardEventCount, 0
  should "not suppress unmapped keys", ->
    sendKeyboardEvent "u"
    assert.equal pageKeyboardEventCount, 3
  should "not suppress escape", ->
    sendKeyboardEvent "escape"
    assert.equal pageKeyboardEventCount, 2
  should "not suppress passKeys", ->
    sendKeyboardEvent "p"
    assert.equal pageKeyboardEventCount, 3
  should "suppress passKeys with a non-empty keyQueue", ->
    handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "p"
    sendKeyboardEvent "p"
    assert.equal pageKeyboardEventCount, 0
context "Insert mode",
  setup ->
    initializeModeState()
    @insertMode = new InsertMode global: true
  should "not suppress mapped keys in insert mode", ->
    sendKeyboardEvent "m"
    assert.equal pageKeyboardEventCount, 3
  should "exit on escape", ->
    assert.isTrue @insertMode.modeIsActive
    sendKeyboardEvent "escape"
    assert.isFalse @insertMode.modeIsActive
  should "resume normal mode after leaving insert mode", ->
    @insertMode.exit()
    sendKeyboardEvent "m"
    assert.equal pageKeyboardEventCount, 0
context "Triggering insert mode",
  setup ->
    initializeModeState()
    testContent = "