# frozen_string_literal: true

require_relative 'lib/common'

# Testing shell integration
module TestShell
  attr_reader :tmux

  def setup
    @tmux = Tmux.new(shell)
    tmux.prepare
  end

  def teardown
    @tmux.kill
  end

  def set_var(name, val)
    tmux.prepare
    tmux.send_keys "export #{name}='#{val}'", :Enter
    tmux.prepare
  end

  def unset_var(name)
    tmux.prepare
    tmux.send_keys "unset #{name}", :Enter
    tmux.prepare
  end

  def trigger
    '**'
  end

  def test_ctrl_t
    set_var('FZF_CTRL_T_COMMAND', 'seq 100')

    tmux.prepare
    tmux.send_keys 'C-t'
    tmux.until { |lines| assert_equal 100, lines.match_count }
    tmux.send_keys :Tab, :Tab, :Tab
    tmux.until { |lines| assert lines.any_include?(' (3)') }
    tmux.send_keys :Enter
    tmux.until { |lines| assert lines.any_include?('1 2 3') }
    tmux.send_keys 'C-c'
  end

  def test_ctrl_t_unicode
    writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
    set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")

    tmux.prepare
    tmux.send_keys 'echo ', 'C-t'
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys 'fzf-unicode'
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '1'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 1, lines.select_count }

    tmux.send_keys :BSpace
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '2'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 2, lines.select_count }

    tmux.send_keys :Enter
    tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
  end

  def test_alt_c
    tmux.prepare
    tmux.send_keys :Escape, :c
    lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
    tmux.send_keys :Enter
    tmux.prepare
    tmux.send_keys :pwd, :Enter
    tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
  end

  def test_alt_c_command
    set_var('FZF_ALT_C_COMMAND', 'echo /tmp')

    tmux.prepare
    tmux.send_keys 'cd /', :Enter

    tmux.prepare
    tmux.send_keys :Escape, :c
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter

    tmux.prepare
    tmux.send_keys :pwd, :Enter
    tmux.until { |lines| assert_equal '/tmp', lines[-1] }
  end

  def test_alt_c_symlink
    base = '/tmp/fzf-test-alt-c-symlink'
    FileUtils.rm_rf(base)
    FileUtils.mkdir_p("#{base}/real/subdir")
    FileUtils.ln_s("#{base}/real", "#{base}/link")

    tmux.prepare
    tmux.send_keys "cd #{base}/link", :Enter
    tmux.prepare
    tmux.send_keys :Escape, :c
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'subdir'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.prepare
    tmux.send_keys :pwd, :Enter
    tmux.until { |lines| assert_equal "#{base}/link/subdir", lines[-1] }
  ensure
    FileUtils.rm_rf(base)
  end

  def test_alt_c_absolute_cmd
    base = '/tmp/fzf-test-alt-c-absolute'
    FileUtils.rm_rf(base)
    FileUtils.mkdir_p(base)

    set_var('FZF_ALT_C_COMMAND', "echo #{base}")

    tmux.prepare
    tmux.send_keys 'cd /tmp', :Enter
    tmux.prepare
    tmux.send_keys :Escape, :c
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.prepare
    tmux.send_keys :pwd, :Enter
    tmux.until { |lines| assert_equal base, lines[-1] }
  ensure
    FileUtils.rm_rf(base)
  end

  def test_ctrl_r
    tmux.prepare
    tmux.send_keys 'echo 1st', :Enter
    tmux.prepare
    tmux.send_keys 'echo 2nd', :Enter
    tmux.prepare
    tmux.send_keys 'echo 3d', :Enter
    tmux.prepare
    3.times do
      tmux.send_keys 'echo 3rd', :Enter
      tmux.prepare
    end
    tmux.send_keys 'echo 4th', :Enter
    tmux.prepare
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'e3d'
    # Duplicates removed: 3d (1) + 3rd (1) => 2 matches
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal '3rd', lines[-1] }
  end

  def test_ctrl_r_multiline
    # NOTE: Current bash implementation shows an extra new line if there's
    # only entry in the history
    tmux.send_keys ':', :Enter
    tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
    tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
    tmux.prepare
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_equal '>', lines[-1] }
    tmux.send_keys 'foo bar'
    tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
    tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
    tmux.send_keys :Enter
    tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
  end

  def test_ctrl_r_abort
    skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
    %w[foo ' "].each do |query|
      tmux.prepare
      tmux.send_keys :Enter, query
      tmux.until { |lines| assert lines[-1]&.start_with?(query) }
      tmux.send_keys 'C-r'
      tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
      tmux.send_keys 'C-g'
      tmux.until { |lines| assert lines[-1]&.start_with?(query) }
    end
  end
end

module CompletionTest
  def test_file_completion
    FileUtils.mkdir_p('/tmp/fzf-test')
    FileUtils.mkdir_p('/tmp/fzf test')
    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
    ['no~such~user', '/tmp/fzf test/foobar'].each do |f|
      FileUtils.touch(File.expand_path(f))
    end
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'cat /tmp/fzf-test/10', 'C-t'
    else
      tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
    end
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys ' !d'
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys :Tab, :Tab
    tmux.until { |lines| assert_equal 2, lines.select_count }
    tmux.send_keys :Enter
    tmux.until(true) do |lines|
      assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
    end

    # ~USERNAME**<TAB>
    user = `whoami`.chomp
    tmux.send_keys 'C-u'
    if shell == :fish
      tmux.send_keys "cat ~#{user}", 'C-t'
    else
      tmux.send_keys "cat ~#{user}#{trigger}", :Tab
    end
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys "/#{user}"
    tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
    tmux.send_keys :Enter
    tmux.until(true) do |lines|
      assert_match %r{cat .*/#{user}}, lines[-1]
    end

    # ~INVALID_USERNAME**<TAB>
    tmux.send_keys 'C-u'
    if shell == :fish
      tmux.send_keys 'cat ~such', 'C-t'
    else
      tmux.send_keys "cat ~such#{trigger}", :Tab
    end
    tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
    tmux.send_keys :Enter
    tmux.until(true) do |lines|
      if shell == :fish
        # Fish's string escape quotes filenames with ~ to prevent tilde expansion
        assert_equal 'cat no\\~such\\~user', lines[-1]
      else
        assert_equal 'cat no~such~user', lines[-1]
      end
    end

    # /tmp/fzf\ test**<TAB>
    tmux.send_keys 'C-u'
    if shell == :fish
      tmux.send_keys 'cat /tmp/fzf\\ test/', 'C-t'
    else
      tmux.send_keys "cat /tmp/fzf\\ test/#{trigger}", :Tab
    end
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'foobar$'
    tmux.until do |lines|
      assert_equal 1, lines.match_count
      assert lines.any_include?('> /tmp/fzf test/foobar')
    end
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }

    # Should include hidden files
    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
    tmux.send_keys 'C-u'
    if shell == :fish
      tmux.send_keys 'cat /tmp/fzf-test/hidden', 'C-t'
    else
      tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
    end
    tmux.until(true) do |lines|
      assert_equal 100, lines.match_count
      assert lines.any_include?('/tmp/fzf-test/.hidden-')
    end
    tmux.send_keys :Enter
  ensure
    ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
      FileUtils.rm_rf(File.expand_path(f))
    end
  end

  def test_file_completion_root
    if shell == :fish
      tmux.send_keys 'ls /', 'C-t'
    else
      tmux.send_keys "ls /#{trigger}", :Tab
    end
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys :Enter
  end

  def test_dir_completion
    FileUtils.mkdir_p('/tmp/fzf-test-dir')
    (1..100).each do |idx|
      FileUtils.mkdir_p("/tmp/fzf-test-dir/d#{idx}")
    end
    FileUtils.touch('/tmp/fzf-test-dir/d55/xxx')
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'cd /tmp/fzf-test-dir/', 'C-t'
    else
      tmux.send_keys "cd /tmp/fzf-test-dir/#{trigger}", :Tab
    end
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    # Tab selects items in C-t's --multi mode, so skip for fish
    tmux.send_keys :Tab, :Tab unless shell == :fish # Tab does not work here
    tmux.send_keys '55/$'
    tmux.until do |lines|
      assert_equal 1, lines.match_count
      assert_includes lines, '> 55/$'
      assert_includes lines, '> /tmp/fzf-test-dir/d55/'
    end
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
    # C-t appends a trailing space after the result
    tmux.send_keys :BSpace if shell == :fish
    tmux.send_keys :xx
    tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }

    # Should not match regular files (bash-only)
    if instance_of?(TestBash)
      tmux.send_keys :Tab
      tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
    end

    # Fail back to plusdirs
    tmux.send_keys :BSpace, :BSpace, :BSpace
    tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55', lines[-1] }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-dir')
  end

  def test_process_completion
    skip('fish background job format differs') if shell == :fish

    begin
      tmux.send_keys 'sleep 12345 &', :Enter
      lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
      pid = lines[-1]&.split&.last
      tmux.prepare
      tmux.send_keys 'C-L'
      tmux.send_keys "kill #{trigger}", :Tab
      tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
      tmux.send_keys 'sleep12345'
      tmux.until { |lines| assert lines.any_include?('sleep 12345') }
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
    ensure
      if pid
        begin
          Process.kill('KILL', pid.to_i)
        rescue StandardError
          nil
        end
      end
    end
  end

  def test_custom_completion
    skip('fish does not use _fzf_compgen_path; path completion is via ctrl-t') if shell == :fish
    tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
    tmux.prepare
    tmux.send_keys "ls /tmp/#{trigger}", :Tab
    tmux.until { |lines| assert_equal 11, lines.match_count }
    tmux.send_keys :Tab, :Tab, :Tab
    tmux.until { |lines| assert_equal 3, lines.select_count }
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
  end

  def test_unset_completion
    skip('fish has native completion for set and unset variables') if shell == :fish
    tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
    tmux.prepare

    # Using tmux
    tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
    tmux.send_keys 'C-c'

    # FZF_TMUX=1
    new_shell
    tmux.focus
    tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
  end

  def test_completion_in_command_sequence
    if shell == :fish
      FileUtils.mkdir_p('/tmp/fzf-test-seq')
      FileUtils.touch('/tmp/fzf-test-seq/fzffoobar')
      tmux.prepare
      # Fish uses Shift-Tab for fzf completion (no trigger system)
      command = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobr'
      expected = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobar'
      tmux.send_keys command, :BTab
      tmux.until { |lines| assert_equal 1, lines.match_count }
      tmux.send_keys :Enter
      tmux.until { |lines| assert_equal expected, lines[-1] }
    else
      tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
      tmux.prepare

      triggers = ['**', '~~', '++', 'ff', '/']
      triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)

      triggers.each do |trigger|
        set_var('FZF_COMPLETION_TRIGGER', trigger)
        command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
        expected = 'echo foo; QUX=THUD unset FZFFOOBAR'
        tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
        tmux.until { |lines| assert_equal 1, lines.match_count }
        tmux.send_keys :Enter
        tmux.until { |lines| assert_equal expected, lines[-1] }
      end
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-seq') if shell == :fish
  end

  def test_file_completion_unicode
    FileUtils.mkdir_p('/tmp/fzf-test')
    # Shell-agnostic file creation
    File.write('/tmp/fzf-test/fzf-unicode 테스트1', "test3\n")
    File.write('/tmp/fzf-test/fzf-unicode 테스트2', "test4\n")
    tmux.send_keys 'cd /tmp/fzf-test', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'cat fzf-unicode', 'C-t'
    else
      tmux.send_keys "cat fzf-unicode#{trigger}", :Tab
    end
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '1'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 1, lines.select_count }

    tmux.send_keys :BSpace
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '2'
    tmux.until { |lines| assert_equal 1, lines.select_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 2, lines.select_count }

    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
  end

  def test_custom_completion_api
    skip('bash-specific _comprun/declare syntax') if shell == :fish

    begin
      tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
      %w[f g].each do |command|
        tmux.prepare
        tmux.send_keys "#{command} b#{trigger}", :Tab
        tmux.until do |lines|
          assert_equal 2, lines.item_count
          assert_equal 1, lines.match_count
          assert lines.any_include?("prompt-#{command}")
          assert lines.any_include?("preview-#{command}-bar")
        end
        tmux.send_keys :Enter
        tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
        tmux.send_keys 'C-u'
      end
    ensure
      tmux.prepare
      tmux.send_keys 'unset -f _fzf_comprun', :Enter
    end
  end

  def test_ssh_completion
    skip('fish uses native ssh completion') if shell == :fish
    (1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }

    tmux.send_keys "ssh jg@localhost#{trigger}", :Tab
    tmux.until do |lines|
      assert_operator lines.match_count, :>=, 1
    end

    tmux.send_keys :Enter
    tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
    tmux.send_keys " -i /tmp/fzf-test-ssh#{trigger}", :Tab
    tmux.until do |lines|
      assert_operator lines.match_count, :>=, 5
      assert_equal 0, lines.select_count
    end
    tmux.send_keys :Tab, :Tab, :Tab
    tmux.until do |lines|
      assert_equal 3, lines.select_count
    end
    tmux.send_keys :Enter
    tmux.until { |lines| assert lines.any_include?('ssh jg@localhost  -i /tmp/fzf-test-ssh-') }

    tmux.send_keys "localhost#{trigger}", :Tab
    tmux.until do |lines|
      assert_operator lines.match_count, :>=, 1
    end
  end

  def test_option_equals_long_option
    FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long')
    FileUtils.touch('/tmp/fzf-test-opt-eq-long/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command --opt=SECURI', 'C-t'
    else
      tmux.send_keys "some-command --opt=SECURI#{trigger}", :Tab
    end

    case shell
    when :bash
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert_includes lines, '> SECURI'
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
    when :fish
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert lines.any_include?('SECURITY.md')
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
    when :zsh
      tmux.until do |lines|
        assert_equal 0, lines.match_count
        assert_includes lines, '> --opt=SECURI'
      end
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long')
  end

  def test_option_equals_long_option_after_double_dash
    FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long-ddash')
    FileUtils.touch('/tmp/fzf-test-opt-eq-long-ddash/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long-ddash', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -- --opt=SECURI', 'C-t'
    else
      tmux.send_keys "some-command -- --opt=SECURI#{trigger}", :Tab
    end

    case shell
    when :bash
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert_includes lines, '> SECURI'
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command -- --opt=SECURITY.md', lines[-1] }
    when :fish, :zsh
      tmux.until do |lines|
        assert_equal 0, lines.match_count
        assert_includes lines, '> --opt=SECURI'
      end
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long-ddash')
  end

  def test_option_equals_short_option
    FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short')
    FileUtils.touch('/tmp/fzf-test-opt-eq-short/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -o=SECURI', 'C-t'
    else
      tmux.send_keys "some-command -o=SECURI#{trigger}", :Tab
    end

    case shell
    when :bash, :fish
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert lines.any_include?('> SECURITY.md')
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command -o=SECURITY.md', lines[-1] }
    when :zsh
      tmux.until do |lines|
        assert_equal 0, lines.match_count
        assert_includes lines, '> -o=SECURI'
      end
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short')
  end

  def test_option_equals_short_option_after_double_dash
    FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short-ddash')
    FileUtils.touch('/tmp/fzf-test-opt-eq-short-ddash/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short-ddash', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -- -o=SECURI', 'C-t'
    else
      tmux.send_keys "some-command -- -o=SECURI#{trigger}", :Tab
    end

    case shell
    when :bash
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert_includes lines, '> SECURITY.md'
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command -- -o=SECURITY.md', lines[-1] }
    when :fish, :zsh
      tmux.until do |lines|
        assert_equal 0, lines.match_count
        assert_includes lines, '> -o=SECURI'
      end
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short-ddash')
  end

  def test_option_no_equals_long_option
    FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long')
    FileUtils.touch('/tmp/fzf-test-opt-no-eq-long/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command --optSECURI', 'C-t'
    else
      tmux.send_keys "some-command --optSECURI#{trigger}", :Tab
    end

    tmux.until do |lines|
      assert_equal 0, lines.match_count
      assert_includes lines, '> --optSECURI'
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long')
  end

  def test_option_no_equals_long_option_after_double_dash
    FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long-ddash')
    FileUtils.touch('/tmp/fzf-test-opt-no-eq-long-ddash/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long-ddash', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -- --optSECURI', 'C-t'
    else
      tmux.send_keys "some-command -- --optSECURI#{trigger}", :Tab
    end

    tmux.until do |lines|
      assert_equal 0, lines.match_count
      assert_includes lines, '> --optSECURI'
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long-ddash')
  end

  def test_option_no_equals_short_option
    FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short')
    FileUtils.touch('/tmp/fzf-test-opt-no-eq-short/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -oSECURI', 'C-t'
    else
      tmux.send_keys "some-command -oSECURI#{trigger}", :Tab
    end

    case shell
    when :bash, :zsh
      tmux.until do |lines|
        assert_equal 0, lines.match_count
        assert_includes lines, '> -oSECURI'
      end
    when :fish
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert lines.any_include?('> SECURITY.md')
      end
      tmux.send_keys :Enter
      tmux.until(true) { |lines| assert_equal 'some-command -oSECURITY.md', lines[-1] }
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short')
  end

  def test_option_no_equals_short_option_after_double_dash
    FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short-ddash')
    FileUtils.touch('/tmp/fzf-test-opt-no-eq-short-ddash/SECURITY.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short-ddash', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'some-command -- -oSECURI', 'C-t'
    else
      tmux.send_keys "some-command -- -oSECURI#{trigger}", :Tab
    end

    tmux.until do |lines|
      assert_equal 0, lines.match_count
      assert_includes lines, '> -oSECURI'
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short-ddash')
  end

  def test_filename_with_newline
    FileUtils.mkdir_p('/tmp/fzf-test-newline')
    FileUtils.touch("/tmp/fzf-test-newline/xyz\nwith\nnewlines")
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-newline', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'cat xyz', 'C-t'
    else
      tmux.send_keys "cat xyz#{trigger}", :Tab
    end

    case shell
    when :fish
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert_includes lines, '> xyz'
      end
      tmux.send_keys :Enter
      # fish escapes newlines in filenames
      tmux.until(true) { |lines| assert_equal 'cat xyz\\nwith\\nnewlines', lines[-1] }
    when :bash, :zsh
      tmux.until do |lines|
        assert_equal 1, lines.match_count
        assert_includes lines, '> xyz'
      end
      tmux.send_keys :Enter
      # bash and zsh replace newlines with spaces in filenames
      tmux.until(true) { |lines| assert_equal 'cat xyz with newlines', lines[-1] }
    end
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-newline')
  end

  def test_path_with_special_chars
    FileUtils.mkdir_p('/tmp/fzf-test-[special]')
    FileUtils.touch('/tmp/fzf-test-[special]/xyz123')
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'ls /tmp/fzf-test-\[special\]/xyz', 'C-t'
    else
      tmux.send_keys "ls /tmp/fzf-test-\\[special\\]/xyz#{trigger}", :Tab
    end
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'ls /tmp/fzf-test-\\[special\\]/xyz123', lines[-1] }
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-[special]')
  end

  def test_query_with_dollar_anchor
    FileUtils.mkdir_p('/tmp/fzf-test-dollar-anchor')
    FileUtils.touch('/tmp/fzf-test-dollar-anchor/file.txt')
    FileUtils.touch('/tmp/fzf-test-dollar-anchor/filetxt.md')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-dollar-anchor', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'ls txt$', 'C-t'
    else
      tmux.send_keys "ls txt$#{trigger}", :Tab
    end
    tmux.until do |lines|
      assert_equal 1, lines.match_count
      assert_includes lines, '> txt$'
    end
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'ls file.txt', lines[-1] }
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-dollar-anchor')
  end

  def test_single_flag_completion
    FileUtils.mkdir_p('/tmp/fzf-test-single-flag')
    FileUtils.touch('/tmp/fzf-test-single-flag/-testfile.txt')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-single-flag', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'ls -', 'C-t'
    else
      tmux.send_keys "ls -#{trigger}", :Tab
    end

    tmux.until do |lines|
      assert_equal 1, lines.match_count
      assert_includes lines, '> -'
    end
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'ls -testfile.txt', lines[-1] }
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-single-flag')
  end

  def test_double_flag_completion
    FileUtils.mkdir_p('/tmp/fzf-test-double-flag')
    FileUtils.touch('/tmp/fzf-test-double-flag/--testfile.txt')
    tmux.prepare
    tmux.send_keys 'cd /tmp/fzf-test-double-flag', :Enter
    tmux.prepare
    if shell == :fish
      tmux.send_keys 'ls --', 'C-t'
    else
      tmux.send_keys "ls --#{trigger}", :Tab
    end

    tmux.until do |lines|
      assert_equal 1, lines.match_count
      assert_includes lines, '> --'
    end
    tmux.send_keys :Enter
    tmux.until(true) { |lines| assert_equal 'ls --testfile.txt', lines[-1] }
  ensure
    FileUtils.rm_rf('/tmp/fzf-test-double-flag')
  end
end

class TestBash < TestBase
  include TestShell
  include CompletionTest

  def shell
    :bash
  end

  def new_shell
    tmux.prepare
    tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
    tmux.prepare
  end

  def test_ctrl_r_delete
    tmux.prepare
    tmux.send_keys 'echo to-keep', :Enter
    tmux.prepare
    tmux.send_keys 'echo to-delete-1', :Enter
    tmux.prepare
    tmux.send_keys 'echo to-delete-2', :Enter
    tmux.prepare
    tmux.send_keys 'echo to-delete-3', :Enter
    tmux.prepare
    tmux.send_keys 'echo another-keeper', :Enter
    tmux.prepare

    # Open Ctrl-R and delete one entry
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'to-delete'
    tmux.until { |lines| assert_equal 3, lines.match_count }
    tmux.send_keys 'S-Delete'
    tmux.until { |lines| assert_equal 2, lines.match_count }

    # Multi-select remaining two and delete them at once
    tmux.send_keys :BTab, :BTab
    tmux.until { |lines| assert_includes lines[-2], '(2)' }
    tmux.send_keys 'S-Delete'
    tmux.until { |lines| assert_equal 0, lines.match_count }

    # Exit without selecting
    tmux.send_keys :Escape
    tmux.prepare

    # Verify deleted entries are gone from history
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'to-delete'
    tmux.until { |lines| assert_equal 0, lines.match_count }
    tmux.send_keys :Escape
    tmux.prepare

    # Verify kept entries are still there
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'to-keep'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'echo to-keep', lines[-1] }
    tmux.send_keys 'C-c'
  end

  def test_dynamic_completion_loader
    tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
    tmux.paste '_completion_loader() { complete -o default fake; }'
    tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
    tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys 'C-c'

    tmux.prepare
    tmux.send_keys 'fake /tmp/foo'
    tmux.send_keys :Tab, 'C-u'

    tmux.prepare
    tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
  end
end

class TestZsh < TestBase
  include TestShell
  include CompletionTest

  def shell
    :zsh
  end

  def new_shell
    tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
    tmux.prepare
  end

  def test_complete_quoted_command
    tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
    ['unset', '\unset', "'unset'"].each do |command|
      tmux.prepare
      tmux.send_keys "#{command} FZFFOOBR#{trigger}", :Tab
      tmux.until { |lines| assert_equal 1, lines.match_count }
      tmux.send_keys :Enter
      tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
      tmux.send_keys 'C-c'
    end
  end

  # Helper function to run test with Perl and again with Awk
  def self.test_perl_and_awk(name, &block)
    define_method(:"test_#{name}") do
      instance_eval(&block)
    end

    define_method(:"test_#{name}_awk") do
      tmux.send_keys "unset 'commands[perl]'", :Enter
      tmux.prepare
      # Verify perl is actually unset (0 = not found)
      tmux.send_keys 'echo ${+commands[perl]}', :Enter
      tmux.until { |lines| assert_equal '0', lines[-1] }
      tmux.prepare
      instance_eval(&block)
    end
  end

  def prepare_ctrl_r_test
    tmux.send_keys ':', :Enter
    tmux.send_keys 'echo match-collision', :Enter
    tmux.prepare
    tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
    tmux.prepare
    tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
    tmux.prepare
    tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
    tmux.prepare
    tmux.send_keys 'echo "trailing_space "', :Enter
    tmux.prepare
    tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
    tmux.prepare
    tmux.send_keys 'C-l', 'C-r'
  end

  test_perl_and_awk 'ctrl_r_accept_or_print_query' do
    set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
    prepare_ctrl_r_test
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys '1 foobar'
    tmux.until { |lines| assert_equal 0, lines.match_count }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
  end

  test_perl_and_awk 'ctrl_r_multiline_index_collision' do
    tmux.send_keys 'setopt sh_glob', :Enter
    # Leading number in multi-line history content is not confused with index
    prepare_ctrl_r_test
    tmux.send_keys "'line 1"
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until do |lines|
      assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
    end
  end

  test_perl_and_awk 'ctrl_r_multi_selection' do
    prepare_ctrl_r_test
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys :BTab, :BTab, :BTab
    tmux.until { |lines| assert_includes lines[-2], '(3)' }
    tmux.send_keys :Enter
    tmux.until do |lines|
      assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
    end
  end

  test_perl_and_awk 'ctrl_r_no_multi_selection' do
    set_var('FZF_CTRL_R_OPTS', '--no-multi')
    prepare_ctrl_r_test
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys :BTab, :BTab, :BTab
    tmux.until { |lines| refute_includes lines[-2], '(3)' }
    tmux.send_keys :Enter
    tmux.until do |lines|
      assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
    end
  end

  # NOTE: 'Perl/$history' won't see foreign cmds immediately, unlike 'awk/fc'.
  # Perl passes only because another cmd runs between mocking and triggering C-r
  # https://github.com/junegunn/fzf/issues/4061
  # https://zsh.org/mla/users/2024/msg00692.html
  test_perl_and_awk 'ctrl_r_foreign_commands' do
    histfile = "#{tempname}-foreign-hist"
    tmux.send_keys "HISTFILE=#{histfile}", :Enter
    tmux.prepare
    # SHARE_HISTORY picks up foreign commands; marked with * in fc
    tmux.send_keys 'setopt SHARE_HISTORY', :Enter
    tmux.prepare
    tmux.send_keys 'fzf_cmd_local', :Enter
    tmux.prepare
    # Mock foreign command (for testing only; don't edit your HISTFILE this way)
    tmux.send_keys "echo ': 0:0;fzf_cmd_foreign' >> $HISTFILE", :Enter
    tmux.prepare
    # Verify fc shows foreign command with asterisk
    tmux.send_keys 'fc -rl -1', :Enter
    tmux.until { |lines| assert(lines.any? { |l| l.match?(/^\s*\d+\* fzf_cmd_foreign/) }) }
    tmux.prepare
    # Test ctrl-r correctly extracts the foreign command
    tmux.send_keys 'C-r'
    tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
    tmux.send_keys '^fzf_cmd_'
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys :BTab, :BTab
    tmux.until { |lines| assert_includes lines[-2], '(2)' }
    tmux.send_keys :Enter
    tmux.until do |lines|
      assert_equal %w[fzf_cmd_foreign fzf_cmd_local], lines[-2..]
    end
  ensure
    FileUtils.rm_f(histfile)
  end
end

class TestFish < TestBase
  include TestShell
  include CompletionTest

  def shell
    :fish
  end

  def trigger
    '++'
  end

  def new_shell
    tmux.send_keys 'env FZF_TMUX=1 XDG_CONFIG_HOME=/tmp/fzf-fish fish', :Enter
    tmux.send_keys 'function fish_prompt; end; clear', :Enter
    tmux.until { |lines| assert_empty lines }
  end

  def set_var(name, val)
    tmux.prepare
    tmux.send_keys "set -g #{name} '#{val}'", :Enter
    tmux.prepare
  end

  def test_ctrl_r_multi
    tmux.send_keys ':', :Enter
    tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
    tmux.prepare
    tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
    tmux.prepare
    tmux.send_keys 'C-l', 'C-r'
    offset = -6
    block = <<~BLOCK
      echo "foo
      bar"
      echo "bar
      foo"
    BLOCK
    if shell == :fish
      offset = -4
      block = <<~FISH
        echo "foo␊bar"
        echo "bar␊foo"
      FISH
    end
    tmux.until do |lines|
      block.lines.each_with_index do |line, idx|
        assert_includes lines[idx + offset], line.chomp
      end
    end
    tmux.send_keys :BTab, :BTab
    tmux.until { |lines| assert_includes lines[-2], '(2)' }
    tmux.send_keys :Enter
    block = <<~BLOCK
      echo "bar
      foo"
      echo "foo
      bar"
    BLOCK
    tmux.until do |lines|
      assert_equal block.lines.map(&:chomp), lines
    end
  end
end

class TestNushell < TestBase
  include TestShell

  def teardown
    @tmux&.kill
  end

  def shell
    :nushell
  end

  def set_var(name, val)
    tmux.prepare
    tmux.send_keys "$env.#{name} = '#{val}'", :Enter
    tmux.prepare
  end

  def unset_var(name)
    tmux.prepare
    tmux.send_keys "hide-env -i #{name}", :Enter
    tmux.prepare
  end

  def new_shell
    tmux.send_keys 'FZF_TMUX=1 nu', :Enter
    tmux.prepare
  end

  # Override: Nushell's builtin `echo` outputs structured data, so we need
  # `^echo` (external echo) for plain text output on the command line.
  def test_ctrl_t_unicode
    writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
    set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")

    tmux.prepare
    tmux.send_keys '^echo ', 'C-t'
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys 'fzf-unicode'
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '1'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 1, lines.select_count }

    tmux.send_keys :BSpace
    tmux.until { |lines| assert_equal 2, lines.match_count }

    tmux.send_keys '2'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Tab
    tmux.until { |lines| assert_equal 2, lines.select_count }

    tmux.send_keys :Enter
    tmux.until { |lines| assert_match(/\^echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
    tmux.send_keys :Enter
    tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
  end

  # Override: Nushell's external completer replaces the entire token,
  # so we use assert_includes instead of assert_equal for the result.
  # ~USERNAME expansion and backslash-escaped spaces are not applicable.
  def test_file_completion
    FileUtils.mkdir_p('/tmp/fzf-test')
    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
    tmux.prepare

    # Multi-selection
    tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys :Tab, :Tab
    tmux.until { |lines| assert_equal 2, lines.select_count }
    tmux.send_keys :Enter
    tmux.until(true) do |lines|
      assert_includes lines[-1].to_s, '/tmp/fzf-test/10'
      assert_includes lines[-1].to_s, '/tmp/fzf-test/100'
    end

    # Single selection
    tmux.prepare
    tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
    tmux.until { |lines| assert_equal 2, lines.match_count }
    tmux.send_keys '0'
    tmux.until { |lines| assert_equal 1, lines.match_count }
    tmux.send_keys :Enter
    tmux.until(true) do |lines|
      assert_includes lines[-1].to_s, '/tmp/fzf-test/100'
    end

    # Should include hidden files
    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
    tmux.prepare
    tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
    tmux.until(true) do |lines|
      assert_equal 100, lines.match_count
      assert lines.any_include?('/tmp/fzf-test/.hidden-')
    end
    tmux.send_keys :Enter
  ensure
    FileUtils.rm_rf('/tmp/fzf-test')
  end

  # Nushell does not support multiline command recall the same way
  # as bash/zsh/fish, so test_ctrl_r_multiline is omitted.

  # Override: only test with 'foo' -- single and double quotes cause
  # issues in Nushell's line editor.
  def test_ctrl_r_abort
    %w[foo].each do |query|
      tmux.prepare
      tmux.send_keys :Enter, query
      tmux.until { |lines| assert lines[-1]&.start_with?(query) }
      tmux.send_keys 'C-r'
      tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
      tmux.send_keys 'C-g'
      tmux.until { |lines| assert lines[-1]&.start_with?(query) }
    end
  end
end
