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

local clear, eq, ok = n.clear, t.eq, t.ok
local matches = t.matches
local exec = n.exec
local feed = n.feed
local api = n.api
local fn = n.fn
local request = n.request
local NIL = vim.NIL
local pcall_err = t.pcall_err
local command = n.command

describe('api/tabpage', function()
  before_each(clear)

  describe('list_wins and get_win', function()
    it('works', function()
      command('tabnew')
      command('vsplit')
      local tab1, tab2 = unpack(api.nvim_list_tabpages())
      local win1, win2, win3 = unpack(api.nvim_list_wins())
      eq({ win1 }, api.nvim_tabpage_list_wins(tab1))
      eq(win1, api.nvim_tabpage_get_win(tab1))
      eq({ win2, win3 }, api.nvim_tabpage_list_wins(tab2))
      eq(win2, api.nvim_tabpage_get_win(tab2))
      api.nvim_set_current_win(win3)
      eq(win3, api.nvim_tabpage_get_win(tab2))
      command('tabprev')
      eq(win1, api.nvim_tabpage_get_win(tab1))
      eq(win3, api.nvim_tabpage_get_win(tab2))
    end)

    it('validates args', function()
      eq('Invalid tabpage id: 23', pcall_err(api.nvim_tabpage_list_wins, 23))
    end)
  end)

  describe('set_win', function()
    it('works', function()
      command('tabnew')
      command('vsplit')
      local tab1, tab2 = unpack(api.nvim_list_tabpages())
      local win1, win2, win3 = unpack(api.nvim_list_wins())
      eq({ win1 }, api.nvim_tabpage_list_wins(tab1))
      eq({ win2, win3 }, api.nvim_tabpage_list_wins(tab2))
      eq(win2, api.nvim_tabpage_get_win(tab2))
      api.nvim_tabpage_set_win(tab2, win3)
      eq(win3, api.nvim_tabpage_get_win(tab2))
    end)

    it('works in non-current tabpages', function()
      command('tabnew')
      command('vsplit')
      local tab1, tab2 = unpack(api.nvim_list_tabpages())
      local win1, win2, win3 = unpack(api.nvim_list_wins())
      eq({ win1 }, api.nvim_tabpage_list_wins(tab1))
      eq({ win2, win3 }, api.nvim_tabpage_list_wins(tab2))
      eq(win2, api.nvim_tabpage_get_win(tab2))
      eq(win2, api.nvim_get_current_win())

      command('tabprev')

      eq(tab1, api.nvim_get_current_tabpage())

      eq(win2, api.nvim_tabpage_get_win(tab2))
      api.nvim_tabpage_set_win(tab2, win3)
      eq(win3, api.nvim_tabpage_get_win(tab2))

      command('tabnext')
      eq(win3, api.nvim_get_current_win())
    end)

    it('throws an error when the window does not belong to the tabpage', function()
      command('tabnew')
      command('vsplit')
      local tab1, tab2 = unpack(api.nvim_list_tabpages())
      local win1, win2, win3 = unpack(api.nvim_list_wins())
      eq({ win1 }, api.nvim_tabpage_list_wins(tab1))
      eq({ win2, win3 }, api.nvim_tabpage_list_wins(tab2))
      eq(win2, api.nvim_get_current_win())

      eq(
        string.format('Window does not belong to tabpage %d', tab2),
        pcall_err(api.nvim_tabpage_set_win, tab2, win1)
      )

      eq(
        string.format('Window does not belong to tabpage %d', tab1),
        pcall_err(api.nvim_tabpage_set_win, tab1, win3)
      )
    end)

    it('checks textlock, cmdwin restrictions', function()
      command('autocmd TextYankPost * ++once call nvim_open_tabpage(0, 0, {})')
      matches('E565:', pcall_err(command, 'yank'))
      eq(1, fn.tabpagenr('$'))

      local other_buf = api.nvim_get_current_buf()
      feed('q:')
      -- OK when not entering and not opening a tabpage with the cmdwin's buffer.
      matches('E11:', pcall_err(api.nvim_open_tabpage, 0, false, {}))
      eq(1, fn.tabpagenr('$'))
      matches('E11:', pcall_err(api.nvim_open_tabpage, other_buf, true, {}))
      eq(1, fn.tabpagenr('$'))
      local tp = api.nvim_open_tabpage(other_buf, false, {})
      eq(other_buf, api.nvim_win_get_buf(api.nvim_tabpage_get_win(tp)))
      eq('command', fn.win_gettype())
    end)

    it('does not switch window when textlocked or in the cmdwin', function()
      local target_win = api.nvim_get_current_win()
      feed('q:')
      local cur_win = api.nvim_get_current_win()
      eq(
        'Vim:E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
        pcall_err(api.nvim_tabpage_set_win, 0, target_win)
      )
      eq(cur_win, api.nvim_get_current_win())
      command('quit!')

      exec(([[
        new
        call setline(1, 'foo')
        setlocal debug=throw indentexpr=nvim_tabpage_set_win(0,%d)
      ]]):format(target_win))
      cur_win = api.nvim_get_current_win()
      eq(
        'Vim(normal):E5555: API call: Vim:E565: Not allowed to change text or change window',
        pcall_err(command, 'normal! ==')
      )
      eq(cur_win, api.nvim_get_current_win())
    end)
  end)

  describe('{get,set,del}_var', function()
    it('works', function()
      api.nvim_tabpage_set_var(0, 'lua', { 1, 2, { ['3'] = 1 } })
      eq({ 1, 2, { ['3'] = 1 } }, api.nvim_tabpage_get_var(0, 'lua'))
      eq({ 1, 2, { ['3'] = 1 } }, api.nvim_eval('t:lua'))
      eq(1, fn.exists('t:lua'))
      api.nvim_tabpage_del_var(0, 'lua')
      eq(0, fn.exists('t:lua'))
      eq('Key not found: lua', pcall_err(api.nvim_tabpage_del_var, 0, 'lua'))
      api.nvim_tabpage_set_var(0, 'lua', 1)
      command('lockvar t:lua')
      eq('Key is locked: lua', pcall_err(api.nvim_tabpage_del_var, 0, 'lua'))
      eq('Key is locked: lua', pcall_err(api.nvim_tabpage_set_var, 0, 'lua', 1))
    end)

    it('tabpage_set_var returns the old value', function()
      local val1 = { 1, 2, { ['3'] = 1 } }
      local val2 = { 4, 7 }
      eq(NIL, request('tabpage_set_var', 0, 'lua', val1))
      eq(val1, request('tabpage_set_var', 0, 'lua', val2))
    end)

    it('tabpage_del_var returns the old value', function()
      local val1 = { 1, 2, { ['3'] = 1 } }
      local val2 = { 4, 7 }
      eq(NIL, request('tabpage_set_var', 0, 'lua', val1))
      eq(val1, request('tabpage_set_var', 0, 'lua', val2))
      eq(val2, request('tabpage_del_var', 0, 'lua'))
    end)
  end)

  describe('get_number', function()
    it('works', function()
      local tabs = api.nvim_list_tabpages()
      eq(1, api.nvim_tabpage_get_number(tabs[1]))

      command('tabnew')
      local tab1, tab2 = unpack(api.nvim_list_tabpages())
      eq(1, api.nvim_tabpage_get_number(tab1))
      eq(2, api.nvim_tabpage_get_number(tab2))

      command('-tabmove')
      eq(2, api.nvim_tabpage_get_number(tab1))
      eq(1, api.nvim_tabpage_get_number(tab2))
    end)
  end)

  describe('is_valid', function()
    it('works', function()
      command('tabnew')
      local tab = api.nvim_list_tabpages()[2]
      api.nvim_set_current_tabpage(tab)
      ok(api.nvim_tabpage_is_valid(tab))
      command('tabclose')
      ok(not api.nvim_tabpage_is_valid(tab))
    end)
  end)

  describe('open_tabpage', function()
    it('works', function()
      local tabs = api.nvim_list_tabpages()
      eq(1, #tabs)
      local curtab = api.nvim_get_current_tabpage()
      local tab = api.nvim_open_tabpage(0, false, {})
      local newtabs = api.nvim_list_tabpages()
      eq(2, #newtabs)
      eq(tab, newtabs[2])
      eq(curtab, api.nvim_get_current_tabpage())

      local tab2 = api.nvim_open_tabpage(0, true, {})
      local newtabs2 = api.nvim_list_tabpages()
      eq(3, #newtabs2)
      eq({
        tabs[1],
        tab2, -- new tabs open after the current tab
        tab,
      }, newtabs2)
      eq(tab2, newtabs2[2])
      eq(tab, newtabs2[3])
      eq(tab2, api.nvim_get_current_tabpage())
    end)

    it('respects the `after` option', function()
      local tab1 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab2 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab3 = api.nvim_get_current_tabpage()

      local newtabs = api.nvim_list_tabpages()
      eq(3, #newtabs)
      eq(newtabs, {
        tab1,
        tab2,
        -- new_tab,
        tab3,
      })

      local new_tab = api.nvim_open_tabpage(0, false, { after = api.nvim_tabpage_get_number(tab2) })
      local newtabs2 = api.nvim_list_tabpages()
      eq(4, #newtabs2)
      eq({
        tab1,
        tab2,
        new_tab,
        tab3,
      }, newtabs2)
      eq(api.nvim_get_current_tabpage(), tab3)
    end)

    it('respects the `enter` argument', function()
      local screen = Screen.new(50, 8)
      eq(1, #api.nvim_list_tabpages())
      local tab1 = api.nvim_get_current_tabpage()

      local new_tab = api.nvim_open_tabpage(0, false, {})
      local newtabs = api.nvim_list_tabpages()
      eq(2, #newtabs)
      eq(newtabs, { tab1, new_tab })
      eq(api.nvim_get_current_tabpage(), tab1)
      -- Tabline redrawn when not entering.
      screen:expect([[
        {5: [No Name] }{24: [No Name] }{2:                           }{24:X}|
        ^                                                  |
        {1:~                                                 }|*5
                                                          |
      ]])

      local new_tab2 = api.nvim_open_tabpage(0, true, {})
      local newtabs2 = api.nvim_list_tabpages()
      eq(3, #newtabs2)
      eq(newtabs2, { tab1, new_tab2, new_tab })
      eq(api.nvim_get_current_tabpage(), new_tab2)
      -- Tabline redrawn. (when entering)
      screen:expect([[
        {24: [No Name] }{5: [No Name] }{24: [No Name] }{2:                }{24:X}|
        ^                                                  |
        {1:~                                                 }|*5
                                                          |
      ]])

      api.nvim_open_tabpage(0, false, {})
      -- Tabline redrawn when not entering, and when there's already one.
      screen:expect([[
        {24: [No Name] }{5: [No Name] }{24: [No Name]  [No Name] }{2:     }{24:X}|
        ^                                                  |
        {1:~                                                 }|*5
                                                          |
      ]])
    end)

    it('applies autocmds in the context of the new tabpage', function()
      exec([=[
        let g:events = []
        autocmd WinNew * let g:events += [['WinNew', nvim_get_current_tabpage(), win_getid()]]
        autocmd WinEnter * let g:events += [['WinEnter', nvim_get_current_tabpage(), win_getid()]]
        autocmd TabNew * let g:events += [['TabNew', nvim_get_current_tabpage(), win_getid()]]
        autocmd TabEnter * let g:events += [['TabEnter', nvim_get_current_tabpage(), win_getid()]]
        autocmd BufEnter * let g:events += [['BufEnter', nvim_get_current_tabpage(), win_getid()]]
        autocmd BufLeave * let g:events += [['BufLeave', nvim_get_current_tabpage(), win_getid()]]
        autocmd BufWinEnter * let g:events += [['BufWinEnter', nvim_get_current_tabpage(), win_getid()]]
      ]=])

      local new_tab = api.nvim_open_tabpage(0, true, {})
      local new_win = api.nvim_tabpage_get_win(new_tab)
      eq({
        { 'WinNew', new_tab, new_win },
        { 'WinEnter', new_tab, new_win },
        { 'TabNew', new_tab, new_win },
        { 'TabEnter', new_tab, new_win },
      }, api.nvim_get_var('events'))
      eq(new_win, api.nvim_get_current_win())

      api.nvim_set_var('events', {})
      new_tab = api.nvim_open_tabpage(api.nvim_create_buf(true, true), true, {})
      new_win = api.nvim_tabpage_get_win(new_tab)
      eq({
        { 'WinNew', new_tab, new_win },
        { 'WinEnter', new_tab, new_win },
        { 'TabNew', new_tab, new_win },
        { 'TabEnter', new_tab, new_win },
        { 'BufLeave', new_tab, new_win },
        { 'BufEnter', new_tab, new_win },
        { 'BufWinEnter', new_tab, new_win },
      }, api.nvim_get_var('events'))

      local curwin = new_win
      api.nvim_set_var('events', {})
      new_tab = api.nvim_open_tabpage(0, false, {})
      new_win = api.nvim_tabpage_get_win(new_tab)
      eq(
        { { 'WinNew', new_tab, new_win }, { 'TabNew', new_tab, new_win } },
        api.nvim_get_var('events')
      )
      eq(curwin, api.nvim_get_current_win())

      api.nvim_set_var('events', {})
      new_tab = api.nvim_open_tabpage(api.nvim_create_buf(true, true), false, {})
      new_win = api.nvim_tabpage_get_win(new_tab)
      eq({
        { 'WinNew', new_tab, new_win },
        { 'TabNew', new_tab, new_win },
        { 'BufWinEnter', new_tab, new_win },
      }, api.nvim_get_var('events'))
      eq(curwin, api.nvim_get_current_win())
    end)

    it('handles nasty autocmds', function()
      command('autocmd WinNewPre * ++once call nvim_open_tabpage(0, 0, {})')
      matches('E1312:', pcall_err(command, 'split'))

      command('autocmd TabNew * ++once quit')
      eq('Tabpage was closed immediately', pcall_err(api.nvim_open_tabpage, 0, false, {}))
      command('autocmd BufEnter * ++once quit')
      local buf = api.nvim_create_buf(true, true)
      eq('Tabpage was closed immediately', pcall_err(api.nvim_open_tabpage, buf, true, {}))

      -- No error if autocmds delete target buffer, if new tabpage is still valid to return.
      command('autocmd BufEnter * ++once buffer # | bwipeout! #')
      local new_tp = api.nvim_open_tabpage(buf, true, {})
      eq(false, api.nvim_buf_is_valid(buf))
      eq(new_tp, api.nvim_get_current_tabpage())
    end)

    it('handles edge cases for positioning', function()
      -- Start with 3 tabs
      local tab1 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab2 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab3 = api.nvim_get_current_tabpage()

      local initial_tabs = api.nvim_list_tabpages()
      eq(3, #initial_tabs)
      eq({ tab1, tab2, tab3 }, initial_tabs)

      -- Test after=0: should become first tab
      local first_tab = api.nvim_open_tabpage(0, false, { after = 0 })
      local tabs_after_first = api.nvim_list_tabpages()
      eq(4, #tabs_after_first)
      eq({ first_tab, tab1, tab2, tab3 }, tabs_after_first)

      -- Test after=-1: should insert after current tab (tab3)
      local explicit_after_current = api.nvim_open_tabpage(0, false, { after = -1 })
      local tabs_after_current = api.nvim_list_tabpages()
      eq(5, #tabs_after_current)
      eq({ first_tab, tab1, tab2, tab3, explicit_after_current }, tabs_after_current)

      -- Test inserting before a middle tab (before tab2, which is now number 3)
      local before_middle = api.nvim_open_tabpage(0, false, { after = 2 })
      local tabs_after_middle = api.nvim_list_tabpages()
      eq(6, #tabs_after_middle)
      eq({ first_tab, tab1, before_middle, tab2, tab3, explicit_after_current }, tabs_after_middle)

      eq(api.nvim_get_current_tabpage(), tab3)

      -- Test default behavior (after current)
      local default_after_current = api.nvim_open_tabpage(0, false, {})
      local final_tabs = api.nvim_list_tabpages()
      eq(7, #final_tabs)
      eq({
        first_tab,
        tab1,
        before_middle,
        tab2,
        tab3,
        default_after_current,
        explicit_after_current,
      }, final_tabs)
    end)

    it('handles position beyond last tab', function()
      -- Create a few tabs first
      local tab1 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab2 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab3 = api.nvim_get_current_tabpage()

      eq(3, #api.nvim_list_tabpages())
      eq({ tab1, tab2, tab3 }, api.nvim_list_tabpages())

      -- Test that requesting position beyond last tab still works
      -- (should place it at the end)
      local new_tab = api.nvim_open_tabpage(0, false, { after = 10 }) -- Way beyond the last tab
      local final_tabs = api.nvim_list_tabpages()
      eq(4, #final_tabs)
      -- Should append at the end
      eq({ tab1, tab2, tab3, new_tab }, final_tabs)
    end)

    it('works with specific buffer', function()
      local curbuf = api.nvim_get_current_buf()
      local new_tab = api.nvim_open_tabpage(0, false, {})
      api.nvim_set_current_tabpage(new_tab)
      eq(curbuf, api.nvim_get_current_buf())

      local buf = api.nvim_create_buf(false, false)
      api.nvim_buf_set_lines(buf, 0, -1, false, { 'test content' })

      local original_tab = api.nvim_get_current_tabpage()
      local original_buf = api.nvim_get_current_buf()
      new_tab = api.nvim_open_tabpage(buf, true, {}) -- Enter the tab to make testing easier

      -- Check that new tab has the specified buffer
      eq(new_tab, api.nvim_get_current_tabpage())
      eq(buf, api.nvim_get_current_buf())
      eq({ 'test content' }, api.nvim_buf_get_lines(buf, 0, -1, false))

      -- Switch back and check original tab still has original buffer
      api.nvim_set_current_tabpage(original_tab)
      eq(original_buf, api.nvim_get_current_buf())

      -- Test invalid buffer
      eq('Invalid buffer id: 999', pcall_err(api.nvim_open_tabpage, 999, true, {}))
    end)

    it('handles complex positioning scenarios', function()
      -- Create 5 tabs total
      local tabs = { api.nvim_get_current_tabpage() }
      for i = 2, 5 do
        command('tabnew')
        tabs[i] = api.nvim_get_current_tabpage()
      end
      eq(5, #api.nvim_list_tabpages())

      -- Go to middle tab (tab 3)
      api.nvim_set_current_tabpage(tabs[3])

      -- Insert after=0 (after current, which is tab 3)
      local new_after_current = api.nvim_open_tabpage(0, false, {})
      local result_tabs = api.nvim_list_tabpages()
      eq(6, #result_tabs)
      eq({
        tabs[1],
        tabs[2],
        tabs[3],
        new_after_current,
        tabs[4],
        tabs[5],
      }, result_tabs)

      -- Insert as number 2 (before tab2, which will become number 3)
      local new_at_pos2 = api.nvim_open_tabpage(0, false, { after = 1 })
      local final_result = api.nvim_list_tabpages()
      eq(7, #final_result)
      eq({
        tabs[1],
        new_at_pos2,
        tabs[2],
        tabs[3],
        new_after_current,
        tabs[4],
        tabs[5],
      }, final_result)
    end)

    it('preserves tab order when entering new tabs', function()
      local tab1 = api.nvim_get_current_tabpage()
      command('tabnew')
      local tab2 = api.nvim_get_current_tabpage()

      -- Create new tab with enter=true, should insert after current (tab2)
      local tab3 = api.nvim_open_tabpage(0, true, { after = -1 })
      local tabs = api.nvim_list_tabpages()
      eq(3, #tabs)
      eq({ tab1, tab2, tab3 }, tabs)
      eq(tab3, api.nvim_get_current_tabpage())

      -- Create another with enter=true and specific position
      api.nvim_set_current_tabpage(tab1)
      local tab4 = api.nvim_open_tabpage(0, true, { after = 0 }) -- Should become first tab
      local final_tabs = api.nvim_list_tabpages()
      eq(4, #final_tabs)
      eq({ tab4, tab1, tab2, tab3 }, final_tabs)
      eq(tab4, api.nvim_get_current_tabpage())
    end)
  end)
end)
