local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')

local clear, feed, api = n.clear, n.feed, n.api
local insert, feed_command = n.insert, n.feed_command
local eq, fn = t.eq, n.fn
local poke_eventloop = n.poke_eventloop
local command = n.command
local exec = n.exec

describe('ui/mouse/input', function()
  local function with_ext_multigrid(multigrid)
    local screen

    before_each(function()
      clear()
      api.nvim_set_option_value('mouse', 'a', {})
      api.nvim_set_option_value('list', true, {})
      -- NB: this is weird, but mostly irrelevant to the test
      -- So I didn't bother to change it
      command('set listchars=eol:$')
      command('setl listchars=nbsp:x')

      screen = Screen.new(25, 5, { ext_multigrid = multigrid })
      screen:add_extra_attr_ids {
        [100] = {
          bold = true,
          background = Screen.colors.LightGrey,
          foreground = Screen.colors.Blue1,
        },
      }
      command('set mousemodel=extend')
      feed('itesting<cr>mouse<cr>support and selection<esc>')
      screen:expect({
        any = {
          'testing                  ',
          'mouse                    ',
          'support and selectio%^n    ',
        },
      })
    end)

    it('single left click moves cursor', function()
      feed('<LeftMouse><2,1>')
      screen:expect({
        any = 'mo%^use',
        mouse_enabled = true,
      })
      feed('<LeftMouse><0,0>')
      screen:expect({
        any = '%^testing',
      })
    end)

    it("in external ui works with unset 'mouse'", function()
      api.nvim_set_option_value('mouse', '', {})
      feed('<LeftMouse><2,1>')
      screen:expect({
        any = 'mo%^use',
        mouse_enabled = false,
      })
      feed('<LeftMouse><0,0>')
      screen:expect({
        any = '%^testing',
      })
    end)

    it('double left click enters visual mode', function()
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      screen:expect({
        any = {
          '{17:testin}%^g',
          'VISUAL',
        },
      })
    end)

    it('triple left click enters visual line mode', function()
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      screen:expect({
        any = {
          '%^t{17:esting}',
          'VISUAL LINE',
        },
      })
    end)

    it('quadruple left click enters visual block mode', function()
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      feed('<LeftMouse><0,0>')
      feed('<LeftRelease><0,0>')
      screen:expect({
        any = {
          '%^testing',
          'VISUAL BLOCK',
        },
      })
    end)

    describe('tab drag', function()
      it('in tabline on filler space moves tab to the end', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftDrag><14,0>')
        screen:expect({
          any = {
            '{24: %+ bar }{5: %+ foo }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
      end)

      it('in tabline to the left moves tab left', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><11,0>')
        -- Prevent the case where screen:expect() with "unchanged" returns too early,
        -- causing the click position to be overwritten by the next drag.
        poke_eventloop()
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
          unchanged = true,
        })
        feed('<LeftDrag><6,0>')
        screen:expect({
          any = {
            '{5: %+ bar }{24: %+ foo }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
      end)

      it('in tabline to the right moves tab right', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftDrag><7,0>')
        screen:expect({
          any = {
            '{24: %+ bar }{5: %+ foo }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
      end)

      it('out of tabline under filler space moves tab to the end', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftDrag><4,1>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
          unchanged = true,
        })
        feed('<LeftDrag><14,1>')
        screen:expect({
          any = {
            '{24: %+ bar }{5: %+ foo }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
      end)

      it('out of tabline to the left moves tab left', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><11,0>')
        -- Prevent the case where screen:expect() with "unchanged" returns too early,
        -- causing the click position to be overwritten by the next drag.
        poke_eventloop()
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
          unchanged = true,
        })
        feed('<LeftDrag><11,1>')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
          unchanged = true,
        })
        feed('<LeftDrag><6,1>')
        screen:expect({
          any = {
            '{5: %+ bar }{24: %+ foo }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
      end)

      it('out of tabline to the right moves tab right', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftDrag><4,1>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
          unchanged = true,
        })
        feed('<LeftDrag><7,1>')
        screen:expect({
          any = {
            '{24: %+ bar }{5: %+ foo }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
      end)
    end)

    describe('tabline', function()
      it('left click in default tabline (tabpage label) switches to tab', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftMouse><6,0>')
        screen:expect_unchanged()
        feed('<LeftMouse><10,0>')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><12,0>')
        screen:expect_unchanged()
      end)

      it('left click in default tabline (blank space) switches tab', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><20,0>')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}',
            'this is fo%^o',
          },
        })
        feed('<LeftMouse><22,0>')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
      end)

      it('left click in default tabline (close label) closes tab', function()
        api.nvim_set_option_value('hidden', true, {})
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<LeftMouse><24,0>')
        screen:expect({
          any = {
            'this is fo%^o',
          },
          none = {
            '{24:X}',
            '%+ foo',
            '%+ bar',
          },
        })
      end)

      it('double click in default tabline opens new tab before', function()
        feed_command('%delete')
        insert('this is foo')
        feed_command('silent file foo | tabnew | file bar')
        insert('this is bar')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:%$}',
          },
        })
        feed('<2-LeftMouse><4,0>')
        screen:expect({
          any = {
            '{5:  Name] }{24: %+ foo  %+ bar }{2:  }{24:X}|',
            '{1:%^$}',
          },
        })
        command('tabclose')
        screen:expect({
          any = {
            '{5: %+ foo }{24: %+ bar }{2:          }{24:X}|',
            'this is fo%^o',
          },
        })
        feed('<2-LeftMouse><20,0>')
        screen:expect({
          any = {
            '{24: %+ foo  %+ bar }{5:  Name] }{2:  }{24:X}',
            '{1:%^$}',
          },
        })
        command('tabclose')
        screen:expect({
          any = {
            '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
            'this is ba%^r{1:$}',
          },
        })
        feed('<2-LeftMouse><10,0>')
        screen:expect({
          any = {
            '{24: %+ foo }{5:  Name] }{24: %+ bar }{2:  }{24:X}',
            '{1:%^$}',
          },
        })
      end)

      describe('%@ label', function()
        before_each(function()
          feed_command([[
            function Test(...)
              let g:reply = a:000
              return copy(a:000)  " Check for memory leaks: return should be freed
            endfunction
          ]])
          feed_command([[
            function Test2(...)
              return call('Test', a:000 + [2])
            endfunction
          ]])
          api.nvim_set_option_value('tabline', '%@Test@test%X-%5@Test2@test2', {})
          api.nvim_set_option_value('showtabline', 2, {})
          screen:expect({
            any = {
              '{2:test%-test2               }',
              'testing',
              'mouse',
              'support and selectio%^n',
            },
          })
          api.nvim_set_var('reply', {})
        end)

        local check_reply = function(expected)
          eq(expected, api.nvim_get_var('reply'))
          api.nvim_set_var('reply', {})
        end

        local test_click = function(name, click_str, click_num, mouse_button, modifiers)
          local function doit(do_click)
            eq(1, fn.has('tablineat'))
            do_click(0, 3)
            check_reply({ 0, click_num, mouse_button, modifiers })
            do_click(0, 4)
            check_reply({})
            do_click(0, 6)
            check_reply({ 5, click_num, mouse_button, modifiers, 2 })
            do_click(0, 13)
            check_reply({ 5, click_num, mouse_button, modifiers, 2 })
          end

          it(name .. ' works (pseudokey)', function()
            doit(function(row, col)
              feed(click_str .. '<' .. col .. ',' .. row .. '>')
            end)
          end)

          it(name .. ' works (nvim_input_mouse)', function()
            doit(function(row, col)
              local buttons = { l = 'left', m = 'middle', r = 'right' }
              local modstr = (click_num > 1) and tostring(click_num) or ''
              for char in string.gmatch(modifiers, '%w') do
                modstr = modstr .. char .. '-' -- - not needed but should be accepted
              end
              api.nvim_input_mouse(buttons[mouse_button], 'press', modstr, 0, row, col)
            end)
          end)
        end

        test_click('single left click', '<LeftMouse>', 1, 'l', '    ')
        test_click('shifted single left click', '<S-LeftMouse>', 1, 'l', 's   ')
        test_click('shifted single left click with alt modifier', '<S-A-LeftMouse>', 1, 'l', 's a ')
        test_click(
          'shifted single left click with alt and ctrl modifiers',
          '<S-C-A-LeftMouse>',
          1,
          'l',
          'sca '
        )
        -- <C-RightMouse> does not work
        test_click(
          'shifted single right click with alt modifier',
          '<S-A-RightMouse>',
          1,
          'r',
          's a '
        )
        -- Modifiers do not work with MiddleMouse
        test_click(
          'shifted single middle click with alt and ctrl modifiers',
          '<MiddleMouse>',
          1,
          'm',
          '    '
        )
        -- Modifiers do not work with N-*Mouse
        test_click('double left click', '<2-LeftMouse>', 2, 'l', '    ')
        test_click('triple left click', '<3-LeftMouse>', 3, 'l', '    ')
        test_click('quadruple left click', '<4-LeftMouse>', 4, 'l', '    ')
        test_click('double right click', '<2-RightMouse>', 2, 'r', '    ')
        test_click('triple right click', '<3-RightMouse>', 3, 'r', '    ')
        test_click('quadruple right click', '<4-RightMouse>', 4, 'r', '    ')
        test_click('double middle click', '<2-MiddleMouse>', 2, 'm', '    ')
        test_click('triple middle click', '<3-MiddleMouse>', 3, 'm', '    ')
        test_click('quadruple middle click', '<4-MiddleMouse>', 4, 'm', '    ')
      end)
    end)

    it('left drag changes visual selection', function()
      -- drag events must be preceded by a click
      feed('<LeftMouse><2,1>')
      screen:expect({
        any = {
          'testing',
          'mo%^use',
          'support and selection',
        },
      })
      feed('<LeftDrag><4,1>')
      screen:expect({
        any = {
          'testing',
          'mo{17:us}%^e',
          'support and selection',
          'VISUAL',
        },
      })
      feed('<LeftDrag><2,2>')
      screen:expect({
        any = {
          'testing',
          'mo{17:use}',
          '{17:su}%^pport and selection',
          'VISUAL',
        },
      })
      feed('<LeftDrag><0,0>')
      screen:expect({
        any = {
          '%^t{17:esting}',
          '{17:mou}se ',
          'support and selection',
          'VISUAL',
        },
      })
    end)

    it('left drag changes visual selection after tab click', function()
      feed_command('silent file foo | tabnew | file bar')
      insert('this is bar')
      feed_command('tabprevious') -- go to first tab
      screen:expect({
        any = {
          '{5: %+ foo }{24: %+ bar }{2:          }{24:X}|',
          'testing',
          'mouse',
          'support and selectio%^n',
          ':tabprevious',
        },
      })
      feed('<LeftMouse><10,0><LeftRelease>') -- go to second tab
      n.poke_eventloop()
      feed('<LeftMouse><0,1>')
      -- This text is hidden when using multigrid
      local none = {
        'mouse',
        'support and selection',
      }
      if multigrid then
        none = nil
      end
      screen:expect({
        any = {
          '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
          '%^this is bar{1:%$}',
          ':tabprevious',
        },
        none = none,
      })
      feed('<LeftDrag><4,1>')
      screen:expect({
        any = {
          '{24: %+ foo }{5: %+ bar }{2:          }{24:X}',
          '{17:this}%^ is bar{1:%$}',
          'VISUAL',
        },
      })
    end)

    it('left drag changes visual selection in split layout', function()
      screen:try_resize(53, 14)
      command('set mouse=a')
      command('vsplit')
      command('wincmd l')
      command('below split')
      command('enew')
      feed('ifoo\nbar<esc>')
      screen:expect({
        any = {
          'foo{1:%$}',
          'ba%^r{1:%$}',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 6, 27)
      screen:expect({
        any = {
          '%^foo{1:%$}',
          'bar{1:%$}',
        },
      })
      api.nvim_input_mouse('left', 'drag', '', 0, 7, 30)
      screen:expect({
        any = {
          '{17:foo}{100:%$}',
          '{17:bar}{1:%^%$}',
          'VISUAL',
        },
      })
    end)

    it('two clicks will enter VISUAL and dragging selects words', function()
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          '{17:suppor}%^t and selection',
          'VISUAL',
        },
      })
      feed('<LeftDrag><0,1>')
      screen:expect({
        any = {
          'testing',
          '%^m{17:ouse}',
          '{17:support} and selection',
          'VISUAL',
        },
      })
      feed('<LeftDrag><4,0>')
      screen:expect({
        any = {
          '%^t{17:esting}',
          '{17:mouse}',
          '{17:support} and selection',
          'VISUAL',
        },
      })
      feed('<LeftDrag><14,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          '{17:support and selectio}%^n',
          'VISUAL',
        },
      })
    end)

    it('three clicks will enter VISUAL LINE and dragging selects lines', function()
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          '{17:su}%^p{17:port and selection}',
          'VISUAL LINE',
        },
      })
      feed('<LeftDrag><0,1>')
      screen:expect({
        any = {
          'testing',
          '%^m{17:ouse}',
          '{17:support and selection}',
          'VISUAL LINE',
        },
      })
      feed('<LeftDrag><4,0>')
      screen:expect({
        any = {
          '{17:test}%^i{17:ng}',
          '{17:mouse}',
          '{17:support and selection}',
          'VISUAL LINE',
        },
      })
      feed('<LeftDrag><14,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          '{17:support and se}%^l{17:ection}',
          'VISUAL LINE',
        },
      })
    end)

    it('four clicks will enter VISUAL BLOCK and dragging selects blockwise', function()
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      feed('<LeftRelease><2,2>')
      feed('<LeftMouse><2,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          'su%^pport and selection',
          'VISUAL BLOCK',
        },
      })
      feed('<LeftDrag><0,1>')
      screen:expect({
        any = {
          'testing',
          '%^m{17:ou}se',
          '{17:sup}port and selection',
          'VISUAL BLOCK',
        },
      })
      feed('<LeftDrag><4,0>')
      screen:expect({
        any = {
          'te{17:st}%^ing',
          'mo{17:use}',
          'su{17:ppo}rt and selection',
          'VISUAL BLOCK',
        },
      })
      feed('<LeftDrag><14,2>')
      screen:expect({
        any = {
          'testing',
          'mouse',
          'su{17:pport and se}%^lection',
          'VISUAL BLOCK',
        },
      })
    end)

    it('right click extends visual selection to the clicked location', function()
      feed('<LeftMouse><0,0>')
      screen:expect({
        any = {
          '%^testing',
          'mouse',
          'support and selection',
        },
      })
      feed('<RightMouse><2,2>')
      screen:expect({
        any = {
          '{17:testing}',
          '{17:mouse}',
          '{17:su}^pport and selection',
          'VISUAL',
        },
      })
    end)

    it('ctrl + left click will search for a tag', function()
      api.nvim_set_option_value('tags', './non-existent-tags-file', {})
      feed('<C-LeftMouse><0,0>')
      screen:expect({
        any = {
          '{9:E433: No tags file}',
          '{9:E426: Tag not found: test}',
          '{9:ing}',
          '{6:Press ENTER or type comma}',
          '{6:nd to continue}%^',
        },
      })
      feed('<cr>')
    end)

    it('x1 and x2 can be triggered by api', function()
      api.nvim_set_var('x1_pressed', 0)
      api.nvim_set_var('x1_released', 0)
      api.nvim_set_var('x2_pressed', 0)
      api.nvim_set_var('x2_released', 0)
      command('nnoremap <X1Mouse> <Cmd>let g:x1_pressed += 1<CR>')
      command('nnoremap <X1Release> <Cmd>let g:x1_released += 1<CR>')
      command('nnoremap <X2Mouse> <Cmd>let g:x2_pressed += 1<CR>')
      command('nnoremap <X2Release> <Cmd>let g:x2_released += 1<CR>')
      api.nvim_input_mouse('x1', 'press', '', 0, 0, 0)
      api.nvim_input_mouse('x1', 'release', '', 0, 0, 0)
      api.nvim_input_mouse('x2', 'press', '', 0, 0, 0)
      api.nvim_input_mouse('x2', 'release', '', 0, 0, 0)
      eq(1, api.nvim_get_var('x1_pressed'), 'x1 pressed once')
      eq(1, api.nvim_get_var('x1_released'), 'x1 released once')
      eq(1, api.nvim_get_var('x2_pressed'), 'x2 pressed once')
      eq(1, api.nvim_get_var('x2_released'), 'x2 released once')
    end)

    it('dragging vertical separator', function()
      screen:try_resize(45, 5)
      command('setlocal nowrap')
      local oldwin = api.nvim_get_current_win()
      command('rightbelow vnew')
      screen:expect({
        any = {
          'testing               ',
          'mouse                 ',
          'support and selection ',
          '{1:%^%$}                     ',
          '{2:%[No Name%] %[%+%]          }{3:%[No Name%]             }',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 0, 22)
      poke_eventloop()
      api.nvim_input_mouse('left', 'drag', '', 0, 1, 12)
      screen:expect({
        any = {
          'testing     ',
          'mouse       ',
          'support and ',
          '{1:%^$}                               ',
          '{2:< Name%] %[%+%]  }{3:%[No Name%]                       }',
        },
      })
      api.nvim_input_mouse('left', 'drag', '', 0, 2, 2)
      screen:expect({
        any = {
          'te',
          'mo',
          'su',
          '{1:%^%$}                                         ',
          '{2:<  }{3:%[No Name%]                                 }',
        },
      })
      api.nvim_input_mouse('left', 'release', '', 0, 2, 2)
      api.nvim_set_option_value('statuscolumn', 'foobar', { win = oldwin })
      screen:expect({
        any = {
          '{8:fo}',
          '{1:%^%$}                                         ',
          '{2:<  }{3:%[No Name%]                                 }',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 0, 2)
      poke_eventloop()
      api.nvim_input_mouse('left', 'drag', '', 0, 1, 12)
      screen:expect({
        any = {
          '{8:foobar}testin',
          '{8:foobar}mouse ',
          '{8:foobar}suppor',
          '{1:%^%$}                               ',
          '{2:< Name%] %[%+%]  }{3:%[No Name%]                       }',
        },
      })
      api.nvim_input_mouse('left', 'drag', '', 0, 2, 22)
      screen:expect({
        any = {
          '{8:foobar}testing         ',
          '{8:foobar}mouse           ',
          '{8:foobar}support and sele',
          '{1:%^%$}                     ',
          '{2:%[No Name%] %[%+%]          }{3:%[No Name%]             }',
        },
      })
      api.nvim_input_mouse('left', 'release', '', 0, 2, 22)
    end)

    local function wheel(use_api)
      feed('ggdG')
      insert([[
      Inserting
      text
      with
      many
      lines
      to
      test
      mouse scrolling
      ]])
      screen:try_resize(53, 14)
      feed('k')
      api.nvim_set_option_value('statuscolumn', 'C', { win = api.nvim_get_current_win() })
      feed_command('sp')
      api.nvim_set_option_value('statuscolumn', 'B', { win = api.nvim_get_current_win() })
      feed_command('vsp')
      api.nvim_set_option_value('statuscolumn', 'A', { win = api.nvim_get_current_win() })
      screen:expect({
        any = {
          '{8:A}lines                    ',
          '{8:A}to                       ',
          '{8:A}test                     ',
          '{8:A}^mouse scrolling          ',
          '{8:B}lines                    ',
          '{8:B}to                       ',
          '{8:B}test                     ',
          '{8:B}mouse scrolling          ',
          '{3:%[No Name%] %[%+%]              }{2:%[No Name%] %[%+%]             }',
          '{8:C}to                                                  ',
          '{8:C}test                                                ',
          '{8:C}mouse scrolling                                     ',
          '{2:%[No Name%] %[%+%]                                        }',
        },
        none = {
          '{8:A}Inserting',
          '{8:A}text',
          '{8:A}with',
          '{8:A}many',
          '{8:B}Inserting',
          '{8:B}text',
          '{8:B}with',
          '{8:B}many',
          '{8:C}Inserting',
          '{8:C}text',
          '{8:C}with',
          '{8:C}many',
          '{8:C}lines',
        },
      })
      if use_api then
        api.nvim_input_mouse('wheel', 'down', '', 0, 0, 0)
      else
        feed('<ScrollWheelDown><0,0>')
      end
      screen:expect({
        any = {
          '{8:A}^mouse scrolling          ',
          '{8:B}lines                    ',
          '{8:B}to                       ',
          '{8:B}test                     ',
          '{8:B}mouse scrolling          ',
          '{3:%[No Name%] %[%+%]              }{2:%[No Name%] %[%+%]             }',
          '{8:C}to                                                  ',
          '{8:C}test                                                ',
          '{8:C}mouse scrolling                                     ',
          '{2:%[No Name%] %[%+%]                                        }',
        },
        none = {
          '{8:A}lines',
          '{8:A}to',
          '{8:A}test',
          '{8:B}Inserting',
          '{8:B}text',
          '{8:B}with',
          '{8:B}many',
          '{8:C}Inserting',
          '{8:C}text',
          '{8:C}with',
          '{8:C}many',
          '{8:C}lines',
        },
      })
      if use_api then
        api.nvim_input_mouse('wheel', 'up', '', 0, 0, 27)
      else
        feed('<ScrollWheelUp><27,0>')
      end
      screen:expect({
        any = {
          '{8:A}^mouse scrolling          ',
          '{8:B}lines                    ',
          '{8:B}to                       ',
          '{8:B}test                     ',
          '{3:%[No Name%] %[%+%]              }{2:%[No Name%] %[%+%]             }',
          '{8:C}to                                                  ',
          '{8:C}test                                                ',
          '{8:C}mouse scrolling                                     ',
          '{2:%[No Name%] %[%+%]                                        }',
        },
        none = {
          '{8:A}Inserting',
          '{8:A}text',
          '{8:A}with',
          '{8:A}many',
          '{8:A}lines',
          '{8:A}to',
          '{8:A}test',
          '{8:B}Inserting',
          '{8:B}mouse scrolling',
          '{8:C}Inserting',
          '{8:C}text',
          '{8:C}with',
          '{8:C}many',
          '{8:C}lines',
        },
      })
      if use_api then
        api.nvim_input_mouse('wheel', 'up', '', 0, 7, 27)
        api.nvim_input_mouse('wheel', 'up', '', 0, 7, 27)
      else
        feed('<ScrollWheelUp><27,7><ScrollWheelUp>')
      end
      screen:expect({
        any = {
          '{8:A}^mouse scrolling          ',
          '{8:B}lines                    ',
          '{8:B}to                       ',
          '{8:B}test                     ',
          '{3:%[No Name%] %[%+%]              }{2:%[No Name%] %[%+%]             }',
          '{8:C}Inserting                                           ',
          '{8:C}text                                                ',
          '{8:C}with                                                ',
          '{8:C}many                                                ',
          '{8:C}lines                                               ',
          '{2:%[No Name%] %[%+%]                                        }',
        },
        none = {
          '{8:A}Inserting',
          '{8:A}text',
          '{8:A}with',
          '{8:A}many',
          '{8:A}lines',
          '{8:A}to',
          '{8:A}test',
          '{8:B}Inserting',
          '{8:B}mouse scrolling',
          '{8:C}to',
          '{8:C}test',
          '{8:C}mouse scrolling',
        },
      })
    end

    it('mouse wheel will target the hovered window (pseudokey)', function()
      wheel(false)
    end)

    it('mouse wheel will target the hovered window (nvim_input_mouse)', function()
      wheel(true)
    end)

    it('horizontal scrolling (pseudokey)', function()
      command('set sidescroll=0')
      feed('<esc>:set nowrap<cr>')

      feed('a <esc>17Ab<esc>3Ab<esc>')
      screen:expect({
        any = {
          'bbbbbbbbbbbbbbb%^b        ',
        },
      })

      feed('<ScrollWheelLeft><0,0>')
      screen:expect({
        any = {
          'n bbbbbbbbbbbbbbbbbbb%^b   ',
        },
      })

      feed('^<ScrollWheelRight><0,0>')
      screen:expect({
        any = {
          'g                        ',
          '%^t and selection bbbbbbbbb',
        },
      })
    end)

    it('horizontal scrolling (nvim_input_mouse)', function()
      command('set sidescroll=0')
      feed('<esc>:set nowrap<cr>')

      feed('a <esc>17Ab<esc>3Ab<esc>')
      screen:expect({
        any = {
          'bbbbbbbbbbbbbbb%^b        ',
        },
      })

      api.nvim_input_mouse('wheel', 'left', '', 0, 0, 27)
      screen:expect({
        any = {
          'n bbbbbbbbbbbbbbbbbbb%^b   ',
        },
      })

      feed('^')
      api.nvim_input_mouse('wheel', 'right', '', 0, 0, 0)
      screen:expect({
        any = {
          'g                        ',
          '%^t and selection bbbbbbbbb',
        },
      })
    end)

    it("'sidescrolloff' applies to horizontal scrolling", function()
      command('set nowrap')
      command('set sidescrolloff=4')

      feed('I <esc>020ib<esc>0')
      screen:expect({
        any = {
          'testing                  ',
          'mouse                    ',
          '%^bbbbbbbbbbbbbbbbbbbb supp',
        },
      })

      api.nvim_input_mouse('wheel', 'right', '', 0, 0, 27)
      screen:expect({
        any = {
          'g                        ',
          '                         ',
          'bbbb%^bbbbbbbbbb support an',
        },
      })

      -- window-local 'sidescrolloff' should override global value. #21162
      command('setlocal sidescrolloff=2')
      feed('0')
      screen:expect({
        any = {
          'testing                  ',
          'mouse                    ',
          '%^bbbbbbbbbbbbbbbbbbbb supp',
        },
      })

      api.nvim_input_mouse('wheel', 'right', '', 0, 0, 27)
      screen:expect({
        any = {
          'g                        ',
          '                         ',
          'bb%^bbbbbbbbbbbb support an',
        },
      })
    end)

    local function test_mouse_click_conceal()
      it('(level 1) click on non-wrapped lines', function()
        feed_command('let &conceallevel=1', 'echo')

        feed('<esc><LeftMouse><0,0>')
        screen:expect({
          any = {
            '%^Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:>} 私は猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><1,0>')
        screen:expect({
          any = {
            'S%^ection{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:>} 私は猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><21,0>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }%^t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:>} 私は猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><21,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t%^3{14: } {14: }',
            '{14:>} 私は猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><0,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:%^>} 私は猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><7,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:>} 私は%^猫が大好き{1:>---}{14: X } {1:>}',
          },
        })

        feed('<esc><LeftMouse><21,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '{14:>} 私は猫が大好き{1:>---}{14: %^X } {1:>}',
          },
        })
      end) -- level 1 - non wrapped

      it('(level 1) click on wrapped lines', function()
        feed_command('let &conceallevel=1', 'let &wrap=1', 'echo')

        feed('<esc><LeftMouse><24,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14:%^ }',
            't4{14: }                      ',
            '{14:>} 私は猫が大好き{1:>---}{14: X}   ',
            '{14: } ✨🐈✨                 ',
          },
        })

        feed('<esc><LeftMouse><0,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            '%^t4{14: }                      ',
            '{14:>} 私は猫が大好き{1:>---}{14: X}   ',
            '{14: } ✨🐈✨                 ',
          },
        })

        feed('<esc><LeftMouse><8,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            't4{14: }                      ',
            '{14:>} 私は猫%^が大好き{1:>---}{14: X}   ',
            '{14: } ✨🐈✨                 ',
          },
        })

        feed('<esc><LeftMouse><21,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            't4{14: }                      ',
            '{14:>} 私は猫が大好き{1:>---}{14: %^X}   ',
            '{14: } ✨🐈✨                 ',
          },
        })

        feed('<esc><LeftMouse><4,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}{14: }t1{14: } ',
            '{1:>--->--->---}  {14: }t2{14: } {14: }t3{14: } {14: }',
            't4{14: }                      ',
            '{14:>} 私は猫が大好き{1:>---}{14: X}   ',
            '{14: } ✨%^🐈✨                 ',
          },
        })
      end) -- level 1 - wrapped

      it('(level 2) click on non-wrapped lines', function()
        feed_command('let &conceallevel=2', 'echo')

        feed('<esc><LeftMouse><20,0>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}%^t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<esc><LeftMouse><14,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  %^t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<esc><LeftMouse><18,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t%^3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<esc><LeftMouse><0,2>') -- Weirdness
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:%^>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<esc><LeftMouse><8,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫%^が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<esc><LeftMouse><20,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:%^X} ✨{1:>}',
          },
        })
      end) -- level 2 - non wrapped

      it('(level 2) click on non-wrapped lines (insert mode)', function()
        feed_command('let &conceallevel=2', 'echo')

        feed('<esc>i<LeftMouse><20,0>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}%^t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<LeftMouse><14,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  %^t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<LeftMouse><18,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t%^3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<LeftMouse><0,2>') -- Weirdness
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:%^>} 私は猫が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<LeftMouse><8,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫%^が大好き{1:>---}{14:X} ✨{1:>}',
          },
        })

        feed('<LeftMouse><20,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '{14:>} 私は猫が大好き{1:>---}{14:%^X} ✨{1:>}',
          },
        })
      end) -- level 2 - non wrapped (insert mode)

      it('(level 2) click on wrapped lines', function()
        feed_command('let &conceallevel=2', 'let &wrap=1', 'echo')

        feed('<esc><LeftMouse><20,0>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}%^t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><14,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  %^t2 t3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><18,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t%^3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        -- NOTE: The click would ideally be on the 't' in 't4', but wrapping
        -- caused the invisible '*' right before 't4' to remain on the previous
        -- screen line.  This is being treated as expected because fixing this is
        -- out of scope for mouse clicks.  Should the wrapping behavior of
        -- concealed characters change in the future, this case should be
        -- reevaluated.
        feed('<esc><LeftMouse><0,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 %^     ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><1,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't%^4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><0,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '{14:%^>} 私は猫が大好き{1:>---}{14:X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><20,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:%^X}    ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><1,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            '%^✨🐈✨                 ',
          },
        })

        feed('<esc><LeftMouse><5,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '{14:>} 私は猫が大好き{1:>---}{14:X}    ',
            '✨🐈%^✨                  ',
          },
        })
      end) -- level 2 - wrapped

      it('(level 3) click on non-wrapped lines', function()
        feed_command('let &conceallevel=3', 'echo')

        feed('<esc><LeftMouse><0,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            '%^ 私は猫が大好き{1:>----} ✨🐈',
          },
        })

        feed('<esc><LeftMouse><1,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            ' %^私は猫が大好き{1:>----} ✨🐈',
          },
        })

        feed('<esc><LeftMouse><13,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            ' 私は猫が大好%^き{1:>----} ✨🐈',
          },
        })

        feed('<esc><LeftMouse><20,2>')
        feed('zH') -- FIXME: unnecessary horizontal scrolling
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3 t4   ',
            ' 私は猫が大好き{1:>----}%^ ✨🐈',
          },
        })
      end) -- level 3 - non wrapped

      it('(level 3) click on wrapped lines', function()
        feed_command('let &conceallevel=3', 'let &wrap=1', 'echo')

        feed('<esc><LeftMouse><14,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  %^t2 t3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><18,1>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t%^3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><1,2>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't%^4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><0,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            '%^ 私は猫が大好き{1:>----}     ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><20,3>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}%^     ',
            ' ✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><1,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' %^✨🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><3,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' ✨%^🐈✨                  ',
          },
        })

        feed('<esc><LeftMouse><5,4>')
        screen:expect({
          any = {
            'Section{1:>>--->--->---}t1   ',
            '{1:>--->--->---}  t2 t3      ',
            't4                       ',
            ' 私は猫が大好き{1:>----}     ',
            ' ✨🐈%^✨                  ',
          },
        })
      end) -- level 3 - wrapped
    end

    describe('on concealed text', function()
      -- Helpful for reading the test expectations:
      -- :match Error /\^/

      before_each(function()
        screen:try_resize(25, 7)
        feed('ggdG')

        command([[setlocal concealcursor=ni nowrap shiftwidth=2 tabstop=4 list listchars=tab:>-]])
        command([[highlight link X0 Normal]])
        command([[highlight link X1 NonText]])
        command([[highlight link X2 NonText]])
        command([[highlight link X3 NonText]])

        -- First column is there to retain the tabs.
        insert([[
        |Section				*t1*
        |			  *t2* *t3* *t4*
        |x 私は猫が大好き	*cats* ✨🐈✨
        ]])

        feed('gg<c-v>Gxgg')
      end)

      describe('(syntax)', function()
        before_each(function()
          command([[syntax region X0 matchgroup=X1 start=/\*/ end=/\*/ concealends contains=X2]])
          command([[syntax match X2 /cats/ conceal cchar=X contained]])
          command([[syntax match X3 /\n\@<=x/ conceal cchar=>]])
        end)
        test_mouse_click_conceal()
      end)

      describe('(matchadd())', function()
        before_each(function()
          fn.matchadd('Conceal', [[\*]])
          fn.matchadd('Conceal', [[cats]], 10, -1, { conceal = 'X' })
          fn.matchadd('Conceal', [[\n\@<=x]], 10, -1, { conceal = '>' })
        end)
        test_mouse_click_conceal()
      end)

      describe('(extmarks)', function()
        before_each(function()
          local ns = api.nvim_create_namespace('conceal')
          api.nvim_buf_set_extmark(0, ns, 0, 11, { end_col = 12, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 0, 14, { end_col = 15, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 5, { end_col = 6, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 8, { end_col = 9, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 10, { end_col = 11, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 13, { end_col = 14, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 15, { end_col = 16, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 1, 18, { end_col = 19, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 2, 24, { end_col = 25, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 2, 29, { end_col = 30, conceal = '' })
          api.nvim_buf_set_extmark(0, ns, 2, 25, { end_col = 29, conceal = 'X' })
          api.nvim_buf_set_extmark(0, ns, 2, 0, { end_col = 1, conceal = '>' })
        end)
        test_mouse_click_conceal()
      end)
    end)

    it('virtual text does not change cursor placement on concealed line', function()
      command('%delete')
      insert('aaaaaaaaaa|hidden|bbbbbbbbbb|hidden|cccccccccc')
      command('syntax match test /|hidden|/ conceal cchar=X')
      command('set conceallevel=2 concealcursor=n virtualedit=all')
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbbb       ',
          'bbb{14:X}ccccccccc%^c           ',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 0, 22)
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbb%^b       ',
          'bbb{14:X}cccccccccc           ',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 1, 16)
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbbb       ',
          'bbb{14:X}cccccccccc  %^         ',
        },
      })

      api.nvim_buf_set_extmark(0, api.nvim_create_namespace(''), 0, 0, {
        virt_text = { { '?', 'ErrorMsg' } },
        virt_text_pos = 'right_align',
        virt_text_repeat_linebreak = true,
      })
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbbb      {9:%?}',
          'bbb{14:X}cccccccccc  %^        {9:%?}',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 0, 22)
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbb%^b      {9:%?}',
          'bbb{14:X}cccccccccc          {9:%?}',
        },
      })
      api.nvim_input_mouse('left', 'press', '', 0, 1, 16)
      screen:expect({
        any = {
          'aaaaaaaaaa{14:X}bbbbbbb      {9:%?}',
          'bbb{14:X}cccccccccc  %^        {9:%?}',
        },
      })
    end)

    it("mouse click on window separator in statusline doesn't crash", function()
      api.nvim_set_option_value('winwidth', 1, {})
      api.nvim_set_option_value('statusline', '%f', {})

      command('vsplit')
      command('redraw')

      local lines = api.nvim_get_option_value('lines', {})
      local columns = api.nvim_get_option_value('columns', {})

      api.nvim_input_mouse('left', 'press', '', 0, lines - 1, math.floor(columns / 2))
      command('redraw')
    end)

    it('getmousepos() works correctly', function()
      local winwidth = api.nvim_get_option_value('winwidth', {})
      -- Set winwidth=1 so that window sizes don't change.
      api.nvim_set_option_value('winwidth', 1, {})
      command('tabedit')
      local tabpage = api.nvim_get_current_tabpage()
      insert('hello')
      command('vsplit')
      local opts = {
        relative = 'editor',
        width = 12,
        height = 1,
        col = 8,
        row = 1,
        anchor = 'NW',
        style = 'minimal',
        border = 'single',
        focusable = 1,
      }
      local float = api.nvim_open_win(api.nvim_get_current_buf(), false, opts)
      command('redraw')
      local lines = api.nvim_get_option_value('lines', {})
      local columns = api.nvim_get_option_value('columns', {})

      -- Test that screenrow and screencol are set properly for all positions.
      for row = 0, lines - 1 do
        for col = 0, columns - 1 do
          -- Skip the X button that would close the tab.
          if row ~= 0 or col ~= columns - 1 then
            api.nvim_input_mouse('left', 'press', '', 0, row, col)
            api.nvim_set_current_tabpage(tabpage)
            local mousepos = fn.getmousepos()
            eq(row + 1, mousepos.screenrow)
            eq(col + 1, mousepos.screencol)
            -- All other values should be 0 when clicking on the command line.
            if row == lines - 1 then
              eq(0, mousepos.winid)
              eq(0, mousepos.winrow)
              eq(0, mousepos.wincol)
              eq(0, mousepos.line)
              eq(0, mousepos.column)
              eq(0, mousepos.coladd)
            end
          end
        end
      end

      -- Test that mouse position values are properly set for the floating window
      -- with a border. 1 is added to the height and width to account for the
      -- border.
      for win_row = 0, opts.height + 1 do
        for win_col = 0, opts.width + 1 do
          local row = win_row + opts.row
          local col = win_col + opts.col
          api.nvim_input_mouse('left', 'press', '', 0, row, col)
          local mousepos = fn.getmousepos()
          eq(float, mousepos.winid)
          eq(win_row + 1, mousepos.winrow)
          eq(win_col + 1, mousepos.wincol)
          local line = 0
          local column = 0
          local coladd = 0
          if
            win_row > 0
            and win_row < opts.height + 1
            and win_col > 0
            and win_col < opts.width + 1
          then
            -- Because of border, win_row and win_col don't need to be
            -- incremented by 1.
            line = math.min(win_row, fn.line('$'))
            column = math.min(win_col, #fn.getline(line) + 1)
            coladd = win_col - column
          end
          eq(line, mousepos.line)
          eq(column, mousepos.column)
          eq(coladd, mousepos.coladd)
        end
      end

      -- Test that mouse position values are properly set for the floating
      -- window, after removing the border.
      opts.border = 'none'
      api.nvim_win_set_config(float, opts)
      command('redraw')
      for win_row = 0, opts.height - 1 do
        for win_col = 0, opts.width - 1 do
          local row = win_row + opts.row
          local col = win_col + opts.col
          api.nvim_input_mouse('left', 'press', '', 0, row, col)
          local mousepos = fn.getmousepos()
          eq(float, mousepos.winid)
          eq(win_row + 1, mousepos.winrow)
          eq(win_col + 1, mousepos.wincol)
          local line = math.min(win_row + 1, fn.line('$'))
          local column = math.min(win_col + 1, #fn.getline(line) + 1)
          local coladd = win_col + 1 - column
          eq(line, mousepos.line)
          eq(column, mousepos.column)
          eq(coladd, mousepos.coladd)
        end
      end

      -- Test that mouse position values are properly set for ordinary windows.
      -- Set the float to be unfocusable instead of closing, to additionally test
      -- that getmousepos() does not consider unfocusable floats. (see discussion
      -- in PR #14937 for details).
      opts.focusable = false
      api.nvim_win_set_config(float, opts)
      command('redraw')
      for nr = 1, 2 do
        for win_row = 0, fn.winheight(nr) - 1 do
          for win_col = 0, fn.winwidth(nr) - 1 do
            local row = win_row + fn.win_screenpos(nr)[1] - 1
            local col = win_col + fn.win_screenpos(nr)[2] - 1
            api.nvim_input_mouse('left', 'press', '', 0, row, col)
            local mousepos = fn.getmousepos()
            eq(fn.win_getid(nr), mousepos.winid)
            eq(win_row + 1, mousepos.winrow)
            eq(win_col + 1, mousepos.wincol)
            local line = math.min(win_row + 1, fn.line('$'))
            local column = math.min(win_col + 1, #fn.getline(line) + 1)
            local coladd = win_col + 1 - column
            eq(line, mousepos.line)
            eq(column, mousepos.column)
            eq(coladd, mousepos.coladd)
          end
        end
      end

      -- Restore state and release mouse.
      command('tabclose!')
      api.nvim_set_option_value('winwidth', winwidth, {})
      api.nvim_input_mouse('left', 'release', '', 0, 0, 0)
    end)

    it('scroll keys are not translated into multiclicks and can be mapped #6211 #6989', function()
      api.nvim_set_var('mouse_up', 0)
      api.nvim_set_var('mouse_up2', 0)
      command('nnoremap <ScrollWheelUp> <Cmd>let g:mouse_up += 1<CR>')
      command('nnoremap <2-ScrollWheelUp> <Cmd>let g:mouse_up2 += 1<CR>')
      feed('<ScrollWheelUp><0,0>')
      feed('<ScrollWheelUp><0,0>')
      api.nvim_input_mouse('wheel', 'up', '', 0, 0, 0)
      api.nvim_input_mouse('wheel', 'up', '', 0, 0, 0)
      eq(4, api.nvim_get_var('mouse_up'))
      eq(0, api.nvim_get_var('mouse_up2'))
    end)

    it('<MouseMove> to different locations can be mapped', function()
      api.nvim_set_var('mouse_move', 0)
      api.nvim_set_var('mouse_move2', 0)
      command('nnoremap <MouseMove> <Cmd>let g:mouse_move += 1<CR>')
      command('nnoremap <2-MouseMove> <Cmd>let g:mouse_move2 += 1<CR>')
      feed('<MouseMove><1,0>')
      feed('<MouseMove><2,0>')
      api.nvim_input_mouse('move', '', '', 0, 0, 3)
      api.nvim_input_mouse('move', '', '', 0, 0, 4)
      eq(4, api.nvim_get_var('mouse_move'))
      eq(0, api.nvim_get_var('mouse_move2'))
    end)

    it('<MouseMove> to same location does not generate events #31103', function()
      api.nvim_set_var('mouse_move', 0)
      api.nvim_set_var('mouse_move2', 0)
      command('nnoremap <MouseMove> <Cmd>let g:mouse_move += 1<CR>')
      command('nnoremap <2-MouseMove> <Cmd>let g:mouse_move2 += 1<CR>')
      api.nvim_input_mouse('move', '', '', 0, 0, 3)
      eq(1, api.nvim_get_var('mouse_move'))
      eq(0, api.nvim_get_var('mouse_move2'))
      feed('<MouseMove><3,0>')
      feed('<MouseMove><3,0>')
      api.nvim_input_mouse('move', '', '', 0, 0, 3)
      api.nvim_input_mouse('move', '', '', 0, 0, 3)
      eq(1, api.nvim_get_var('mouse_move'))
      eq(0, api.nvim_get_var('mouse_move2'))
      eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
      feed('<MouseMove><3,0><Insert>')
      eq(1, api.nvim_get_var('mouse_move'))
      eq(0, api.nvim_get_var('mouse_move2'))
      eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    end)

    it('feeding <MouseMove> in Normal mode does not use uninitialized memory #19480', function()
      feed('<MouseMove>')
      n.poke_eventloop()
      n.assert_alive()
    end)

    it('mousemodel=popup_setpos', function()
      screen:try_resize(80, 24)
      exec([[
        5new
        call setline(1, ['the dish ran away with the spoon',
              \ 'the cow jumped over the moon' ])

        set mouse=a mousemodel=popup_setpos

        aunmenu PopUp
        nmenu PopUp.foo :let g:menustr = 'foo'<CR>
        nmenu PopUp.bar :let g:menustr = 'bar'<CR>
        nmenu PopUp.baz :let g:menustr = 'baz'<CR>
        vmenu PopUp.foo y:<C-U>let g:menustr = 'foo'<CR>
        vmenu PopUp.bar y:<C-U>let g:menustr = 'bar'<CR>
        vmenu PopUp.baz y:<C-U>let g:menustr = 'baz'<CR>
      ]])

      api.nvim_win_set_cursor(0, { 1, 0 })
      api.nvim_input_mouse('right', 'press', '', 0, 0, 4)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 4)
      feed('<Down><Down><CR>')
      eq('bar', api.nvim_get_var('menustr'))
      eq({ 1, 4 }, api.nvim_win_get_cursor(0))

      -- Test for right click in visual mode inside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 9 })
      feed('vee')
      api.nvim_input_mouse('right', 'press', '', 0, 0, 11)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 11)
      feed('<Down><CR>')
      eq({ 1, 9 }, api.nvim_win_get_cursor(0))
      eq('ran away', fn.getreg('"'))

      -- Test for right click in visual mode right before the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 9 })
      feed('vee')
      api.nvim_input_mouse('right', 'press', '', 0, 0, 8)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 8)
      feed('<Down><CR>')
      eq({ 1, 8 }, api.nvim_win_get_cursor(0))
      eq('', fn.getreg('"'))

      -- Test for right click in visual mode right after the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 9 })
      feed('vee')
      api.nvim_input_mouse('right', 'press', '', 0, 0, 17)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 17)
      feed('<Down><CR>')
      eq({ 1, 17 }, api.nvim_win_get_cursor(0))
      eq('', fn.getreg('"'))

      -- Test for right click in block-wise visual mode inside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 15 })
      feed('<C-V>j3l')
      api.nvim_input_mouse('right', 'press', '', 0, 1, 16)
      api.nvim_input_mouse('right', 'release', '', 0, 1, 16)
      feed('<Down><CR>')
      eq({ 1, 15 }, api.nvim_win_get_cursor(0))
      eq('\0224', fn.getregtype('"'))

      -- Test for right click in block-wise visual mode outside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 15 })
      feed('<C-V>j3l')
      api.nvim_input_mouse('right', 'press', '', 0, 1, 1)
      api.nvim_input_mouse('right', 'release', '', 0, 1, 1)
      feed('<Down><CR>')
      eq({ 2, 1 }, api.nvim_win_get_cursor(0))
      eq('v', fn.getregtype('"'))
      eq('', fn.getreg('"'))

      -- Test for right click in line-wise visual mode inside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 15 })
      feed('V')
      api.nvim_input_mouse('right', 'press', '', 0, 0, 9)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 9)
      feed('<Down><CR>')
      eq({ 1, 0 }, api.nvim_win_get_cursor(0)) -- After yanking, the cursor goes to 1,1
      eq('V', fn.getregtype('"'))
      eq(1, #fn.getreg('"', 1, true))

      -- Test for right click in multi-line line-wise visual mode inside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 15 })
      feed('Vj')
      api.nvim_input_mouse('right', 'press', '', 0, 1, 19)
      api.nvim_input_mouse('right', 'release', '', 0, 1, 19)
      feed('<Down><CR>')
      eq({ 1, 0 }, api.nvim_win_get_cursor(0)) -- After yanking, the cursor goes to 1,1
      eq('V', fn.getregtype('"'))
      eq(2, #fn.getreg('"', 1, true))

      -- Test for right click in line-wise visual mode outside the selection
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 15 })
      feed('V')
      api.nvim_input_mouse('right', 'press', '', 0, 1, 9)
      api.nvim_input_mouse('right', 'release', '', 0, 1, 9)
      feed('<Down><CR>')
      eq({ 2, 9 }, api.nvim_win_get_cursor(0))
      eq('', fn.getreg('"'))

      -- Try clicking outside the window
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 2, 1 })
      feed('vee')
      api.nvim_input_mouse('right', 'press', '', 0, 6, 1)
      api.nvim_input_mouse('right', 'release', '', 0, 6, 1)
      feed('<Down><CR>')
      eq(2, fn.winnr())
      eq('', fn.getreg('"'))

      -- Test for right click in visual mode inside the selection with vertical splits
      command('wincmd t')
      command('rightbelow vsplit')
      fn.setreg('"', '')
      api.nvim_win_set_cursor(0, { 1, 9 })
      feed('vee')
      api.nvim_input_mouse('right', 'press', '', 0, 0, 52)
      api.nvim_input_mouse('right', 'release', '', 0, 0, 52)
      feed('<Down><CR>')
      eq({ 1, 9 }, api.nvim_win_get_cursor(0))
      eq('ran away', fn.getreg('"'))

      -- Test for right click inside visual selection at bottom of window with winbar
      command('setlocal winbar=WINBAR')
      feed('2yyP')
      fn.setreg('"', '')
      feed('G$vbb')
      api.nvim_input_mouse('right', 'press', '', 0, 4, 61)
      api.nvim_input_mouse('right', 'release', '', 0, 4, 61)
      feed('<Down><CR>')
      eq({ 4, 20 }, api.nvim_win_get_cursor(0))
      eq('the moon', fn.getreg('"'))

      -- Try clicking in the cmdline
      api.nvim_input_mouse('right', 'press', '', 0, 23, 0)
      api.nvim_input_mouse('right', 'release', '', 0, 23, 0)
      feed('<Down><Down><Down><CR>')
      eq('baz', api.nvim_get_var('menustr'))

      -- Try clicking in horizontal separator with global statusline
      command('set laststatus=3')
      api.nvim_input_mouse('right', 'press', '', 0, 5, 0)
      api.nvim_input_mouse('right', 'release', '', 0, 5, 0)
      feed('<Down><CR>')
      eq('foo', api.nvim_get_var('menustr'))

      -- Try clicking in the cmdline with global statusline
      api.nvim_input_mouse('right', 'press', '', 0, 23, 0)
      api.nvim_input_mouse('right', 'release', '', 0, 23, 0)
      feed('<Down><Down><CR>')
      eq('bar', api.nvim_get_var('menustr'))
    end)

    it('below a concealed line #33450', function()
      api.nvim_set_option_value('conceallevel', 2, {})
      api.nvim_buf_set_extmark(0, api.nvim_create_namespace(''), 1, 0, { conceal_lines = '' })
      api.nvim_input_mouse('left', 'press', '', 0, 1, 0)
      api.nvim_input_mouse('left', 'release', '', 0, 1, 0)
      eq(3, fn.line('.'))
      -- No error when clicking below last line that is concealed.
      screen:try_resize(80, 10) -- Prevent hit-enter
      api.nvim_set_option_value('cmdheight', 3, {})
      local count = api.nvim_buf_line_count(0)
      api.nvim_buf_set_extmark(0, 1, count - 1, 0, { conceal_lines = '' })
      api.nvim_input_mouse('left', 'press', '', 0, count, 0)
      eq('', api.nvim_get_vvar('errmsg'))
    end)
  end

  describe('with ext_multigrid', function()
    with_ext_multigrid(true)
  end)

  describe('without ext_multigrid', function()
    with_ext_multigrid(false)
  end)
end)
