# frozen_string_literal: true

RSpec.describe YARD::DocstringParser do
  after(:all) do
    YARD::Registry.clear
  end

  def parse(content, object = nil, handler = nil)
    @library ||= Tags::Library.instance
    @parser = DocstringParser.new(@library)
    @parser.parse(content, object, handler)
    @parser
  end

  def docstring(content, object = nil, handler = nil)
    parse(content, object, handler).to_docstring
  end

  describe "#parse" do
    it "parses comments into tags" do
      doc = docstring(<<-eof)
@param name Hello world
  how are you?
@param name2
  this is a new line
@param name3 and this
  is a new paragraph:

  right here.
      eof
      tags = doc.tags(:param)
      expect(tags[0].name).to eq "name"
      expect(tags[0].text).to eq "Hello world\nhow are you?"
      expect(tags[1].name).to eq "name2"
      expect(tags[1].text).to eq "this is a new line"
      expect(tags[2].name).to eq "name3"
      expect(tags[2].text).to eq "and this\nis a new paragraph:\n\nright here."
    end

    it "ends parsing a tag on de-dent" do
      doc = docstring(<<-eof)
@note test
  one two three
rest of docstring
      eof
      expect(doc.tag(:note).text).to eq "test\none two three"
      expect(doc).to eq "rest of docstring"
    end

    it "parses examples embedded in doc" do
      doc = docstring(<<-eof)
test string here
@example code

  def foo(x, y, z)
  end

  class A; end

more stuff
eof
      expect(doc).to eq "test string here\nmore stuff"
      expect(doc.tag(:example).text).to eq "\ndef foo(x, y, z)\nend\n\nclass A; end"
    end

    it "removes only original indentation from beginning of line in tags" do
      doc = docstring(<<-eof)
@param name
  some value
  foo bar
   baz
eof
      expect(doc.tag(:param).text).to eq "some value\nfoo bar\n baz"
    end

    it "allows numbers in tags" do
      Tags::Library.define_tag(nil, :foo1)
      Tags::Library.define_tag(nil, :foo2)
      Tags::Library.define_tag(nil, :foo3)
      doc = docstring(<<-eof)
@foo1 bar1
@foo2 bar2
@foo3 bar3
eof
      expect(doc.tag(:foo1).text).to eq "bar1"
      expect(doc.tag(:foo2).text).to eq "bar2"
    end

    it "ends tag on newline if next line is not indented" do
      doc = docstring(<<-eof)
@author bar1
@api bar2
Hello world
eof
      expect(doc.tag(:author).text).to eq "bar1"
      expect(doc.tag(:api).text).to eq "bar2"
    end

    it "warns about unknown tag" do
      expect(log).to receive(:warn).with(/Unknown tag @hello$/)
      docstring("@hello world")
    end

    it "does not add trailing whitespace to freeform tags" do
      doc = docstring("@api private   \t   ")
      expect(doc.tag(:api).text).to eq "private"
    end
  end

  describe "#parse with custom tag library" do
    class TestLibrary < Tags::Library; end

    before { @library = TestLibrary.new }

    it "accepts valid tags" do
      valid = %w(testing valid is_a is_A __)
      valid.each do |tag|
        TestLibrary.define_tag("Tag", tag)
        doc = docstring('@' + tag + ' foo bar')
        expect(doc.tag(tag).text).to eq 'foo bar'
      end
    end

    it "does not parse invalid tag names" do
      invalid = %w(@ @return@ @p,aram @x-y @.x.y.z)
      invalid.each do |tag|
        expect(docstring(tag + ' foo bar')).to eq tag + ' foo bar'
      end
    end

    it "allows namespaced tags in the form @x.y.z" do
      TestLibrary.define_tag("Tag", 'x.y.z')
      doc = docstring("@x.y.z foo bar")
      expect(doc.tag('x.y.z').text).to eq 'foo bar'
    end

    it "ignores new directives without @! prefix syntax" do
      TestLibrary.define_directive('dir1', Tags::ScopeDirective)
      expect(log).to receive(:warn).with(/@dir1/)
      docstring("@dir1")
    end

    %w(attribute endgroup group macro method scope visibility).each do |tag|
      it "handles non prefixed @#{tag} syntax as directive, not tag" do
        TestLibrary.define_directive(tag, Tags::ScopeDirective)
        parse("@#{tag}")
        expect(@parser.directives.first).to be_a(Tags::ScopeDirective)
      end
    end

    it "handles directives with @! prefix syntax" do
      TestLibrary.define_directive('dir2', Tags::ScopeDirective)
      docstring("@!dir2 class")
      expect(@parser.state.scope).to eq :class
    end
  end

  describe "#text" do
    it "only returns text data" do
      parse("Foo\n@param foo x y z\nBar")
      expect(@parser.text).to eq "Foo\nBar"
    end
  end

  describe "#raw_text" do
    it "returns the entire original data" do
      data = "Foo\n@param foo x y z\nBar"
      parse(data)
      expect(@parser.raw_text).to eq data
    end
  end

  describe "#tags" do
    it "returns the parsed tags" do
      data = "Foo\n@param foo x y z\nBar"
      parse(data)
      expect(@parser.tags.size).to eq 1
      expect(@parser.tags.first.tag_name).to eq 'param'
    end
  end

  describe "#directives" do
    it "groups all processed directives" do
      data = "Foo\n@!scope class\n@!visibility private\nBar"
      parse(data)
      dirs = @parser.directives
      expect(dirs[0]).to be_a(Tags::ScopeDirective)
      expect(dirs[0].tag.text).to eq 'class'
      expect(dirs[1]).to be_a(Tags::VisibilityDirective)
      expect(dirs[1].tag.text).to eq 'private'
    end
  end

  describe "#state" do
    it "handles modified state" do
      parse("@!scope class")
      expect(@parser.state.scope).to eq :class
    end
  end

  describe "after_parse (param)" do
    it "allows specifying of callbacks" do
      parser = DocstringParser.new
      the_yielded_obj = nil
      DocstringParser.after_parse {|obj| the_yielded_obj = obj }
      parser.parse("Some text")
      expect(the_yielded_obj).to eq parser
    end

    it "warns about invalid named parameters" do
      expect(log).to receive(:warn).with(/@param tag has unknown parameter name: notaparam/)
      YARD.parse_string <<-eof
        # @param notaparam foo
        def foo(a) end
      eof
    end

    it "warns about invalid named parameters on @!method directives" do
      expect(log).to receive(:warn).with(/@param tag has unknown parameter name: notaparam/)
      YARD.parse_string <<-eof
        # @!method foo(a)
        #   @param notaparam foo
        test
      eof
    end

    it "warns about duplicate named parameters" do
      expect(log).to receive(:warn).with(/@param tag has duplicate parameter name: a/)
      YARD.parse_string <<-eof
        # @param a foo
        # @param a foo
        def foo(a) end
      eof
    end

    it "does not warn on aliases" do
      expect(log).to_not receive(:warn)
      YARD.parse_string <<-eof
        # @param a foo
        def foo(a) end
        alias bar foo
      eof
    end

    it "does not warn on matching param with inline method modifier" do
      expect(log).to_not receive(:warn)
      YARD.parse_string <<-eof
        # @param [Numeric] a
        # @return [Numeric]
        private_class_method def self.foo(a); a + 1; end
      eof
    end

    it "does not warn on constant values" do
      expect(log).to_not receive(:warn)
      YARD.parse_string <<-eof
        # @param [false, true, nil, 4, :foo] a
        # @return [void]
        def self.bar(a); end
      eof
    end

    it "warns on mismatching param with inline method modifier" do
      expect(log).to receive(:warn).with(/@param tag has unknown parameter name: notaparam/)
      YARD.parse_string <<-eof
        # @param [Numeric] notaparam
        # @return [Numeric]
        private_class_method def self.foo(a); a + 1; end
      eof
    end
  end

  describe "after_parse (see)" do
    it "does not warn on valid see tag" do
      expect(log).to_not receive(:warn)
      YARD.parse_string "# @see valid\nclass Foo;end"
    end

    it "warns if {} wraps single name" do
      expect(log).to receive(:warn).with(/@see tag \(#1\) should not be wrapped in \{\}/)
      YARD.parse_string "# @see {invalid}\nclass Foo;end"
    end

    it "warns if {} wraps across name and text" do
      expect(log).to receive(:warn).with(/@see tag \(#1\) should not be wrapped in \{\}/)
      YARD.parse_string "# @see {invalid tag}\nclass Foo;end"
    end
  end
end
