# frozen_string_literal: false
require "pathname"
require "irb"

require_relative "helper"

module TestIRB
  class CompletionTest < TestCase
    def setup
      # make sure require completion candidates are not cached
      IRB::InputCompletor.class_variable_set(:@@files_from_load_path, nil)
    end

    class MethodCompletionTest < CompletionTest
      def test_complete_string
        assert_include(IRB::InputCompletor.retrieve_completion_data("'foo'.up", bind: binding), "'foo'.upcase")
        # completing 'foo bar'.up
        assert_include(IRB::InputCompletor.retrieve_completion_data("bar'.up", bind: binding), "bar'.upcase")
        assert_equal("String.upcase", IRB::InputCompletor.retrieve_completion_data("'foo'.upcase", bind: binding, doc_namespace: true))
      end

      def test_complete_regexp
        assert_include(IRB::InputCompletor.retrieve_completion_data("/foo/.ma", bind: binding), "/foo/.match")
        # completing /foo bar/.ma
        assert_include(IRB::InputCompletor.retrieve_completion_data("bar/.ma", bind: binding), "bar/.match")
        assert_equal("Regexp.match", IRB::InputCompletor.retrieve_completion_data("/foo/.match", bind: binding, doc_namespace: true))
      end

      def test_complete_array
        assert_include(IRB::InputCompletor.retrieve_completion_data("[].an", bind: binding), "[].any?")
        assert_equal("Array.any?", IRB::InputCompletor.retrieve_completion_data("[].any?", bind: binding, doc_namespace: true))
      end

      def test_complete_hash_and_proc
        # hash
        assert_include(IRB::InputCompletor.retrieve_completion_data("{}.an", bind: binding), "{}.any?")
        assert_equal(["Proc.any?", "Hash.any?"], IRB::InputCompletor.retrieve_completion_data("{}.any?", bind: binding, doc_namespace: true))

        # proc
        assert_include(IRB::InputCompletor.retrieve_completion_data("{}.bin", bind: binding), "{}.binding")
        assert_equal(["Proc.binding", "Hash.binding"], IRB::InputCompletor.retrieve_completion_data("{}.binding", bind: binding, doc_namespace: true))
      end

      def test_complete_numeric
        assert_include(IRB::InputCompletor.retrieve_completion_data("1.positi", bind: binding), "1.positive?")
        assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true))

        assert_include(IRB::InputCompletor.retrieve_completion_data("1r.positi", bind: binding), "1r.positive?")
        assert_equal("Rational.positive?", IRB::InputCompletor.retrieve_completion_data("1r.positive?", bind: binding, doc_namespace: true))

        assert_include(IRB::InputCompletor.retrieve_completion_data("0xFFFF.positi", bind: binding), "0xFFFF.positive?")
        assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("0xFFFF.positive?", bind: binding, doc_namespace: true))

        assert_empty(IRB::InputCompletor.retrieve_completion_data("1i.positi", bind: binding))
      end

      def test_complete_symbol
        assert_include(IRB::InputCompletor.retrieve_completion_data(":foo.to_p", bind: binding), ":foo.to_proc")
        assert_equal("Symbol.to_proc", IRB::InputCompletor.retrieve_completion_data(":foo.to_proc", bind: binding, doc_namespace: true))
      end

      def test_complete_class
        assert_include(IRB::InputCompletor.retrieve_completion_data("String.ne", bind: binding), "String.new")
        assert_equal("String.new", IRB::InputCompletor.retrieve_completion_data("String.new", bind: binding, doc_namespace: true))
      end
    end

    class RequireComepletionTest < CompletionTest
      def test_complete_require
        candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "")
        %w['irb/init 'irb/ruby-lex].each do |word|
          assert_include candidates, word
        end
        # Test cache
        candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "")
        %w['irb/init 'irb/ruby-lex].each do |word|
          assert_include candidates, word
        end
      end

      def test_complete_require_with_pathname_in_load_path
        temp_dir = Dir.mktmpdir
        File.write(File.join(temp_dir, "foo.rb"), "test")
        test_path = Pathname.new(temp_dir)
        $LOAD_PATH << test_path

        candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
        assert_include candidates, "'foo"
      ensure
        $LOAD_PATH.pop if test_path
        FileUtils.remove_entry(temp_dir) if temp_dir
      end

      def test_complete_require_with_string_convertable_in_load_path
        temp_dir = Dir.mktmpdir
        File.write(File.join(temp_dir, "foo.rb"), "test")
        object = Object.new
        object.define_singleton_method(:to_s) { temp_dir }
        $LOAD_PATH << object

        candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
        assert_include candidates, "'foo"
      ensure
        $LOAD_PATH.pop if object
        FileUtils.remove_entry(temp_dir) if temp_dir
      end

      def test_complete_require_with_malformed_object_in_load_path
        object = Object.new
        def object.to_s; raise; end
        $LOAD_PATH << object

        assert_nothing_raised do
          IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
        end
      ensure
        $LOAD_PATH.pop if object
      end

      def test_complete_require_library_name_first
        candidates = IRB::InputCompletor::CompletionProc.("'csv", "require ", "")
        assert_equal "'csv", candidates.first
      end

      def test_complete_require_relative
        candidates = Dir.chdir(__dir__ + "/../..") do
          IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "")
        end
        %w['lib/irb/init 'lib/irb/ruby-lex].each do |word|
          assert_include candidates, word
        end
        # Test cache
        candidates = Dir.chdir(__dir__ + "/../..") do
          IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "")
        end
        %w['lib/irb/init 'lib/irb/ruby-lex].each do |word|
          assert_include candidates, word
        end
      end
    end

    class VariableCompletionTest < CompletionTest
      def test_complete_variable
        # Bug fix issues https://github.com/ruby/irb/issues/368
        # Variables other than `str_example` and `@str_example` are defined to ensure that irb completion does not cause unintended behavior
        str_example = ''
        @str_example = ''
        private_methods = ''
        methods = ''
        global_variables = ''
        local_variables = ''
        instance_variables = ''

        # suppress "assigned but unused variable" warning
        str_example.clear
        @str_example.clear
        private_methods.clear
        methods.clear
        global_variables.clear
        local_variables.clear
        instance_variables.clear

        assert_include(IRB::InputCompletor.retrieve_completion_data("str_examp", bind: binding), "str_example")
        assert_equal("String", IRB::InputCompletor.retrieve_completion_data("str_example", bind: binding, doc_namespace: true))
        assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("str_example.to_s", bind: binding, doc_namespace: true))

        assert_include(IRB::InputCompletor.retrieve_completion_data("@str_examp", bind: binding), "@str_example")
        assert_equal("String", IRB::InputCompletor.retrieve_completion_data("@str_example", bind: binding, doc_namespace: true))
        assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("@str_example.to_s", bind: binding, doc_namespace: true))
      end

      def test_complete_sort_variables
        xzy, xzy_1, xzy2 = '', '', ''

        xzy.clear
        xzy_1.clear
        xzy2.clear

        candidates = IRB::InputCompletor.retrieve_completion_data("xz", bind: binding, doc_namespace: false)
        assert_equal(%w[xzy xzy2 xzy_1], candidates)
      end
    end

    class ConstantCompletionTest < CompletionTest
      class Foo
        B3 = 1
        B1 = 1
        B2 = 1
      end

      def test_complete_constants
        assert_equal(["Foo"], IRB::InputCompletor.retrieve_completion_data("Fo", bind: binding))
        assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], IRB::InputCompletor.retrieve_completion_data("Foo::B", bind: binding))
        assert_equal(["Foo::B1.positive?"], IRB::InputCompletor.retrieve_completion_data("Foo::B1.pos", bind: binding))

        assert_equal(["::Forwardable"], IRB::InputCompletor.retrieve_completion_data("::Fo", bind: binding))
        assert_equal("Forwardable", IRB::InputCompletor.retrieve_completion_data("::Forwardable", bind: binding, doc_namespace: true))
      end
    end

    class PerfectMatchingTest < CompletionTest
      def setup
        # trigger PerfectMatchedProc to set up RDocRIDriver constant
        IRB::InputCompletor::PerfectMatchedProc.("foo", bind: binding)

        @original_use_stdout = IRB::InputCompletor::RDocRIDriver.use_stdout
        # force the driver to use stdout so it doesn't start a pager and interrupt tests
        IRB::InputCompletor::RDocRIDriver.use_stdout = true
      end

      def teardown
        IRB::InputCompletor::RDocRIDriver.use_stdout = @original_use_stdout
      end

      def test_perfectly_matched_namespace_triggers_document_display
        omit unless has_rdoc_content?

        out, err = capture_output do
          IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding)
        end

        assert_empty(err)

        assert_include(out, " S\bSt\btr\bri\bin\bng\bg")
      end

      def test_perfectly_matched_multiple_namespaces_triggers_document_display
        result = nil
        out, err = capture_output do
          result = IRB::InputCompletor::PerfectMatchedProc.("{}.nil?", bind: binding)
        end

        assert_empty(err)

        # check if there're rdoc contents (e.g. CI doesn't generate them)
        if has_rdoc_content?
          # if there's rdoc content, we can verify by checking stdout
          # rdoc generates control characters for formatting method names
          assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil?
          assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil?
        else
          # this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content
          # if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document
          # which always returns the bytes rendered, even if it's 0
          assert_equal(0, result)
        end
      end

      def test_not_matched_namespace_triggers_nothing
        result = nil
        out, err = capture_output do
          result = IRB::InputCompletor::PerfectMatchedProc.("Stri", bind: binding)
        end

        assert_empty(err)
        assert_empty(out)
        assert_nil(result)
      end

      def test_perfect_matching_stops_without_rdoc
        result = nil

        out, err = capture_output do
          without_rdoc do
            result = IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding)
          end
        end

        assert_empty(err)
        assert_not_match(/from ruby core/, out)
        assert_nil(result)
      end

      def test_perfect_matching_handles_nil_namespace
        out, err = capture_output do
          # symbol literal has `nil` doc namespace so it's a good test subject
          assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding))
        end

        assert_empty(err)
        assert_empty(out)
      end

      private

      def has_rdoc_content?
        File.exist?(RDoc::RI::Paths::BASE)
      end
    end

    def test_complete_symbol
      symbols = %w"UTF-16LE UTF-7".map do |enc|
        "K".force_encoding(enc).to_sym
      rescue
      end
      symbols += [:aiueo, :"aiu eo"]
      candidates = IRB::InputCompletor.retrieve_completion_data(":a", bind: binding)
      assert_include(candidates, ":aiueo")
      assert_not_include(candidates, ":aiu eo")
      assert_empty(IRB::InputCompletor.retrieve_completion_data(":irb_unknown_symbol_abcdefg", bind: binding))
      # Do not complete empty symbol for performance reason
      assert_empty(IRB::InputCompletor.retrieve_completion_data(":", bind: binding))
    end

    def test_complete_invalid_three_colons
      assert_empty(IRB::InputCompletor.retrieve_completion_data(":::A", bind: binding))
      assert_empty(IRB::InputCompletor.retrieve_completion_data(":::", bind: binding))
    end

    def test_complete_absolute_constants_with_special_characters
      assert_empty(IRB::InputCompletor.retrieve_completion_data("::A:", bind: binding))
      assert_empty(IRB::InputCompletor.retrieve_completion_data("::A.", bind: binding))
      assert_empty(IRB::InputCompletor.retrieve_completion_data("::A(", bind: binding))
      assert_empty(IRB::InputCompletor.retrieve_completion_data("::A)", bind: binding))
      assert_empty(IRB::InputCompletor.retrieve_completion_data("::A[", bind: binding))
    end

    def test_complete_reserved_words
      candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding)
      %w[def defined?].each do |word|
        assert_include candidates, word
      end

      candidates = IRB::InputCompletor.retrieve_completion_data("__", bind: binding)
      %w[__ENCODING__ __LINE__ __FILE__].each do |word|
        assert_include candidates, word
      end
    end

    def test_complete_methods
      obj = Object.new
      obj.singleton_class.class_eval {
        def public_hoge; end
        private def private_hoge; end

        # Support for overriding #methods etc.
        def methods; end
        def private_methods; end
        def global_variables; end
        def local_variables; end
        def instance_variables; end
      }
      bind = obj.instance_exec { binding }

      assert_include(IRB::InputCompletor.retrieve_completion_data("public_hog", bind: bind), "public_hoge")
      assert_include(IRB::InputCompletor.retrieve_completion_data("public_hoge", bind: bind, doc_namespace: true), "public_hoge")

      assert_include(IRB::InputCompletor.retrieve_completion_data("private_hog", bind: bind), "private_hoge")
      assert_include(IRB::InputCompletor.retrieve_completion_data("private_hoge", bind: bind, doc_namespace: true), "private_hoge")
    end
  end
end
