require "danger/helpers/comments_helper"
require "danger/danger_core/messages/violation"
require "danger/danger_core/messages/markdown"
require "danger/danger_core/message_group"

SINGLE_TABLE_COMMENT = <<-EOS.freeze
  Other comment content
  <table>
    <thead><tr><th width="50"></th><th width="100%">1 Error</th></tr></thead>
    <tbody><tr>
    <td>
      <g-emoji alias="no_entry_sign" fallback-src="https://example.com/1f6ab.png">🚫</g-emoji
    </td>
    <td data-sticky="true"><p>Please include a CHANGELOG entry. You can find it at <a href="https://github.com/danger/danger/blob/master/CHANGELOG.md">CHANGELOG.md</a>.</p>
    </td></tr></tbody>
  </table>

  <p align="right" data-meta="generated_by_danger">
    Generated by :no_entry_sign: <a href="https://danger.systems/">Danger</a>
  </p>
EOS

MULTI_TABLE_COMMENT = <<-EOS.freeze
  Other comment content
  <table>
    <thead><tr><th width="50"></th><th width="100%">1 Error</th></tr></thead>
    <tbody><tr>
    <td>
      <g-emoji alias="no_entry_sign" fallback-src="https://example.com/1f6ab.png">🚫</g-emoji
    </td>
    <td data-sticky="true"><p>Please include a CHANGELOG entry. You can find it at <a href="https://github.com/danger/danger/blob/master/CHANGELOG.md">CHANGELOG.md</a>.</p>
    </td></tr></tbody>
  </table>
  <table>
    <thead><tr><th width="50"></th><th width="100%">1 Warning</th></tr></thead>
    <tbody><tr>
    <td>
      <g-emoji alias="warning" fallback-src="https://example.com/unicode/26a0.png">⚠️</g-emoji>
    </td>
    <td data-sticky="true"><p>External contributor has edited the Gemspec</p>
    </td></tr></tbody>
  </table>

  <p align="right" data-meta="generated_by_danger">
    Generated by :no_entry_sign: <a href="https://danger.systems/">Danger</a>
  </p>
EOS

GITHUB_MAX_COMMENT_LENGTH = 65_536

SINGLE_TABLE_COMMENT_NEW_TABLE_IDENTIFIER = <<-EOS.freeze
  Other comment content
  <table>
    <thead><tr><th width="50"></th><th data-danger-table="true" width="95%">1 Error</th></tr></thead>
    <tbody><tr>
    <td>
      <g-emoji alias="no_entry_sign" fallback-src="https://example.com/1f6ab.png">🚫</g-emoji
    </td>
    <td data-sticky="true"><p>Please include a CHANGELOG entry. You can find it at <a href="https://github.com/danger/danger/blob/master/CHANGELOG.md">CHANGELOG.md</a>.</p>
    </td></tr></tbody>
  </table>

  <p align="right" data-meta="generated_by_danger">
    Generated by :no_entry_sign: <a href="https://danger.systems/">Danger</a>
  </p>
EOS

class Dummy
end

RSpec.describe Danger::Helpers::CommentsHelper do
  let(:dummy) do
    Dummy.new.tap { |dummy| dummy.extend(described_class) }
  end

  describe "#markdown_parser" do
    it "is a Kramdown::Document instance" do
      parser = dummy.markdown_parser("")
      expect(parser).to be_an_instance_of(Kramdown::Document)
    end
  end

  describe "#parse_tables_from_comment" do
    it "splits a single table comment" do
      result = dummy.parse_tables_from_comment(SINGLE_TABLE_COMMENT)
      expect(result.size).to be(2)
      expect(result[0]).to include("<table>")
      expect(result[0]).to include("<thead>")
      expect(result[0]).to include("<tbody>")

      expect(result[1]).not_to include("<table>")
      expect(result[1]).not_to include("<thead>")
      expect(result[1]).not_to include("<tbody>")
    end

    it "splits a multi table comment" do
      result = dummy.parse_tables_from_comment(MULTI_TABLE_COMMENT)
      expect(result.size).to be(3)
      expect(result[0]).to include("<table>")
      expect(result[0]).to include("<thead>")
      expect(result[0]).to include("<tbody>")
      expect(result[0]).to include("Error")
      expect(result[0]).not_to include("Warning")

      expect(result[1]).to include("<table>")
      expect(result[1]).to include("<thead>")
      expect(result[1]).to include("<tbody>")
      expect(result[1]).not_to include("Error")
      expect(result[1]).to include("Warning")

      expect(result[2]).not_to include("<table>")
      expect(result[2]).not_to include("<thead>")
      expect(result[2]).not_to include("<tbody>")
    end
  end

  describe "#violations_from_table" do
    it "finds violations" do
      violations = dummy.violations_from_table(SINGLE_TABLE_COMMENT)

      expect(violations.size).to be(1)
      expect(violations.first).to eq(
        violation_factory("<p>Please include a CHANGELOG entry. You can find it at <a href=\"https://github.com/danger/danger/blob/master/CHANGELOG.md\">CHANGELOG.md</a>.</p>", sticky: true)
      )
    end
  end

  describe "#parse_comment" do
    it "parse violations by kind" do
      violations = dummy.parse_comment(MULTI_TABLE_COMMENT)

      expect(violations[:error].size).to be(1)
      expect(violations[:warning].size).to be(1)
      expect(violations[:message]).to be_nil

      expect(violations[:error][0]).to eq(
        violation_factory("<p>Please include a CHANGELOG entry. You can find it at <a href=\"https://github.com/danger/danger/blob/master/CHANGELOG.md\">CHANGELOG.md</a>.</p>", sticky: true)
      )
      expect(violations[:warning][0]).to eq(violation_factory("<p>External contributor has edited the Gemspec</p>", sticky: true))
    end

    it "handles data-danger-table to identify danger tables" do
      violations = dummy.parse_comment(SINGLE_TABLE_COMMENT_NEW_TABLE_IDENTIFIER)

      expect(violations[:error].size).to be(1)
      expect(violations[:warning]).to be_nil
      expect(violations[:message]).to be_nil

      expect(violations[:error].first.message).to include("Please include a CHANGELOG")
    end

    it "parses a comment with error" do
      comment = comment_fixture("comment_with_error")
      results = dummy.parse_comment(comment)
      expect(results[:error].map(&:message)).to eq(["Some error"])
    end

    it "parses a comment with error and warnings" do
      comment = comment_fixture("comment_with_error_and_warnings")
      results = dummy.parse_comment(comment)

      expect(results[:error].map(&:message)).to eq(["Some error"])
      expect(results[:warning].map(&:message)).to eq(["First warning", "Second warning"])
    end

    it "ignores non-sticky violations when parsing a comment" do
      comment = comment_fixture("comment_with_non_sticky")
      results = dummy.parse_comment(comment)
      expect(results[:warning].map(&:message)).to eq(["First warning"])
    end

    it "parses a comment with error and warnings removing strike tag" do
      comment = comment_fixture("comment_with_resolved_violation")
      results = dummy.parse_comment(comment)

      expect(results[:error].map(&:message)).to eq(["Some error"])
      expect(results[:warning].map(&:message)).to eq(["First warning", "Second warning"])
    end
  end

  describe "#table" do
    let(:violation_1) { violation_factory("**Violation 1**") }
    let(:violation_2) do
      violation_factory("A [link](https://example.com)", sticky: true)
    end
    let(:violation_3) { violation_factory(%(with "double quote" and 'single quote')) }

    it "produces table data" do
      table_data = dummy.table("3 Errors", "no_entry_sign", [violation_1, violation_2, violation_3], {})

      expect(table_data[:name]).to eq("3 Errors")
      expect(table_data[:emoji]).to eq("no_entry_sign")
      expect(table_data[:content].size).to be(3)
      expect(table_data[:content][0].message).to eq("<strong>Violation 1</strong>")
      expect(table_data[:content][0].sticky).to eq(false)

      expect(table_data[:content][1].message).to eq(
        "A <a href=\"https://example.com\">link</a>"
      )
      expect(table_data[:content][1].sticky).to eq(true)
      expect(table_data[:content][2].message).to eq(%(with "double quote" and 'single quote'))
      expect(table_data[:resolved]).to be_empty
      expect(table_data[:count]).to be(3)
    end

    shared_examples "violation text as heredoc" do
      it "produces table data" do
        table_data = dummy.table("1 Error", "no_entry_sign", [violation], {})

        expect(table_data[:name]).to eq("1 Error")
        expect(table_data[:emoji]).to eq("no_entry_sign")
        expect(table_data[:content].size).to be(1)
        expect(table_data[:content][0].message).to eq(heredoc_text.strip.to_s)
        expect(table_data[:content][0].sticky).to eq(false)

        expect(table_data[:resolved]).to be_empty
        expect(table_data[:count]).to be(1)
      end
    end

    context "with a heredoc text with a newline at the end" do
      let(:heredoc_text) { "You have made some app changes, but did not add any tests." }
      let(:violation) { violation_factory(heredoc) }

      context "with a heredoc text with a newline at the end" do
        let(:heredoc) do
          <<~MSG
          #{heredoc_text}

          MSG
        end

        it_behaves_like "violation text as heredoc"
      end

      context "with a heredoc text with two newlines at the end" do
        let(:heredoc) do
          <<~MSG
          #{heredoc_text}


          MSG
        end

        it_behaves_like "violation text as heredoc"
      end

      context "with a heredoc text with a newline at the start and end" do
        let(:heredoc) do
          <<~MSG

          #{heredoc_text}

          MSG
        end

        it_behaves_like "violation text as heredoc"
      end

      context "with a heredoc text with a newline at the start and two newlines at the end" do
        let(:heredoc) do
          <<~MSG

          #{heredoc_text}


          MSG
        end

        it_behaves_like "violation text as heredoc"
      end
    end
  end

  describe "#table_kind_from_title" do
    [
      { title: "errors", singular: "Error", plural: "Errors", expected: :error },
      { title: "warnings", singular: "Warning", plural: "Warnings", expected: :warning },
      { title: "messages", singular: "Message", plural: "Messages", expected: :message }
    ].each do |option|
      describe option[:title] do
        it "handles singular" do
          kind = dummy.table_kind_from_title("1 #{option[:singular]}")
          expect(kind).to eq(option[:expected])
        end

        it "handles plural" do
          kind = dummy.table_kind_from_title("42 #{option[:plural]}")
          expect(kind).to eq(option[:expected])
        end

        it "handles lowercase" do
          kind = dummy.table_kind_from_title("42 #{option[:plural].downcase}")
          expect(kind).to eq(option[:expected])
        end
      end
    end
  end

  describe "#generate_comment" do
    it "produces the expected comment" do
      comment = dummy.generate_comment(
        warnings: [violation_factory("This is a warning")],
        errors: [violation_factory("This is an error", sticky: true)],
        messages: [violation_factory("This is a message")],
        markdowns: [markdown_factory("*Raw markdown*")],
        danger_id: "my_danger_id",
        template: "github"
      )

      expect(comment).to include('data-meta="generated_by_my_danger_id"')

      expect(comment).to include('<td data-sticky="true">This is an error</td>')
      expect(comment).to include("<td>:no_entry_sign:</td>")

      expect(comment).to include('<td data-sticky="false">This is a warning</td>')
      expect(comment).to include("<td>:warning:</td>")

      expect(comment).to include('<td data-sticky="false">This is a message</td>')
      expect(comment).to include("<td>:warning:</td>")

      expect(comment).to include("*Raw markdown*")
    end

    it "produces HTML that a CommonMark parser will accept inline" do
      ["github", "github_inline"].each do |template|
        comment = dummy.generate_comment(
          warnings: [violation_factory("This is a warning")],
          errors: [violation_factory("This is an error", sticky: true)],
          messages: [violation_factory("This is a message")],
          markdowns: [markdown_factory("*Raw markdown*")],
          danger_id: "my_danger_id",
          template: template
        )

        # There should be no indented HTML tag after 2 or more newlines.
        expect(comment).not_to match(/(\r?\n){2}[ \t]+</)
      end
    end

    it "produces the expected comment when there are newlines" do
      comment = dummy.generate_comment(
        warnings: [violation_factory("This is a warning\nin two lines")],
        errors: [],
        messages: [],
        markdowns: [],
        danger_id: "my_danger_id",
        template: "github"
      )

      expect(comment).to include('data-meta="generated_by_my_danger_id"')

      expect(comment).to include("<td data-sticky=\"false\">This is a warning<br />\nin two lines</td>")
      expect(comment).to include("<td>:warning:</td>")
    end

    it "produces a comment containing a summary" do
      comment = dummy.generate_comment(
        warnings: [violation_factory("Violations that are very very very very long should be truncated")],
        errors: [violation_factory("This is an error", sticky: true)],
        messages: [violation_factory("This is a message")],
        markdowns: [markdown_factory("*Raw markdown*")],
        danger_id: "my_danger_id",
        template: "github"
      )

      summary = <<COMMENT
<!--
  1 Error: This is an error
  1 Warning: Violations that are very very ...
  1 Message: This is a message
  1 Markdown
-->
COMMENT

      expect(comment).to include(summary)
    end

    it "no warnings, no errors, no messages" do
      result = dummy.generate_comment(warnings: [], errors: [], messages: [])
      expect(result.gsub(/\s+/, "")).to end_with(
        '<palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
    end

    it "supports markdown code below the summary table" do
      result = dummy.generate_comment(warnings: violations_factory(["ups"]), markdowns: violations_factory(["### h3"]))
      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Warning">1Warning</th></tr></thead><tbody><tr><td>:warning:</td><tddata-sticky="false">ups</td></tr></tbody></table>###h3<palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
    end

    it "supports markdown only without a table" do
      result = dummy.generate_comment(markdowns: violations_factory(["### h3"]))
      expect(result.gsub(/\s+/, "")).to end_with(
        '###h3<palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
    end

    it "some warnings, no errors" do
      result = dummy.generate_comment(warnings: violations_factory(["my warning", "second warning"]), errors: [], messages: [])
      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Warning">2Warnings</th></tr></thead><tbody><tr><td>:warning:</td><tddata-sticky="false">mywarning</td></tr><tr><td>:warning:</td><tddata-sticky="false">secondwarning</td></tr></tbody></table><palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
    end

    it "some warnings with markdown, no errors" do
      warnings = violations_factory(["a markdown [link to danger](https://github.com/danger/danger)", "second **warning**"])
      result = dummy.generate_comment(warnings: warnings, errors: [], messages: [])
      # rubocop:disable Layout/LineLength
      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Warning">2Warnings</th></tr></thead><tbody><tr><td>:warning:</td><tddata-sticky="false">amarkdown<ahref="https://github.com/danger/danger">linktodanger</a></td></tr><tr><td>:warning:</td><tddata-sticky="false">second<strong>warning</strong></td></tr></tbody></table><palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
      # rubocop:enable Layout/LineLength
    end

    it "a multiline warning with markdown, no errors" do
      warnings = violations_factory(["a markdown [link to danger](https://github.com/danger/danger)\n\n```\nsomething\n```\n\nHello"])
      result = dummy.generate_comment(warnings: warnings, errors: [], messages: [])
      # rubocop:disable Layout/LineLength
      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Warning">1Warning</th></tr></thead><tbody><tr><td>:warning:</td><tddata-sticky="false">amarkdown<ahref="https://github.com/danger/danger">linktodanger</a></p><pre><code>something</code></pre><p>Hello</td></tr></tbody></table><palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
      # rubocop:enable Layout/LineLength
    end

    it "some warnings, some errors" do
      result = dummy.generate_comment(warnings: violations_factory(["my warning"]), errors: violations_factory(["some error"]), messages: [])
      # rubocop:disable Layout/LineLength
      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Error">1Error</th></tr></thead><tbody><tr><td>:no_entry_sign:</td><tddata-sticky="false">someerror</td></tr></tbody></table><table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Warning">1Warning</th></tr></thead><tbody><tr><td>:warning:</td><tddata-sticky="false">mywarning</td></tr></tbody></table><palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
      # rubocop:enable Layout/LineLength
    end

    it "deduplicates previous violations" do
      previous_violations = { error: violations_factory(["an error", "an error"]) }
      result = dummy.generate_comment(warnings: [], errors: violations_factory([]), messages: [], previous_violations: previous_violations)
      expect(result.scan("an error").size).to eq(1)
    end

    it "includes a random compliment" do
      previous_violations = { error: violations_factory(["an error"]) }
      result = dummy.generate_comment(warnings: [], errors: violations_factory([]), messages: [], previous_violations: previous_violations)
      expect(result).to match(/:white_check_mark: \w+?/)
    end

    it "crosses resolved violations and changes the title" do
      previous_violations = { error: violations_factory(["an error"]) }
      result = dummy.generate_comment(warnings: [], errors: [], messages: [], previous_violations: previous_violations)
      expect(result.gsub(/\s+/, "")).to include('<thwidth="100%"data-danger-table="true"data-kind="Error">:white_check_mark:')
      expect(result.gsub(/\s+/, "")).to include('<td>:white_check_mark:</td><tddata-sticky="true"><del>anerror</del></td>')
    end

    it "uncrosses violations that were on the list and happened again" do
      previous_violations = { error: violations_factory(["an error"]) }
      result = dummy.generate_comment(
        warnings: [],
        errors: violations_factory(["an error"]),
        messages: [],
        previous_violations: previous_violations
      )

      expect(result.gsub(/\s+/, "")).to end_with(
        '<table><thead><tr><thwidth="50"></th><thwidth="100%"data-danger-table="true"data-kind="Error">1Error</th></tr></thead><tbody><tr><td>:no_entry_sign:</td><tddata-sticky="false">anerror</td></tr></tbody></table><palign="right"data-meta="generated_by_danger">Generatedby:no_entry_sign:<ahref="https://danger.systems/">Danger</a></p>'
      )
    end

    it "counts only unresolved violations on the title" do
      previous_violations = { error: violations_factory(["an error"]) }
      result = dummy.generate_comment(warnings: [], errors: violations_factory(["another error"]),
                                      messages: [], previous_violations: previous_violations)
      expect(result.gsub(/\s+/, "")).to include('<thwidth="100%"data-danger-table="true"data-kind="Error">1Error</th>')
    end

    it "needs to include generated_by_danger" do
      result = dummy.generate_comment(warnings: violations_factory(["my warning"]), errors: violations_factory(["some error"]), messages: [])
      expect(result.gsub(/\s+/, "")).to include("generated_by_danger")
    end

    it "handles a custom danger_id" do
      result = dummy.generate_comment(warnings: violations_factory(["my warning"]), errors: violations_factory(["some error"]),
                                      messages: [], danger_id: "another_danger")
      expect(result.gsub(/\s+/, "")).to include("generated_by_another_danger")
    end

    it "sets data-sticky to true when a violation is sticky" do
      sticky_warning = Danger::Violation.new("my warning", true)
      result = dummy.generate_comment(warnings: [sticky_warning], errors: [], messages: [])
      expect(result.gsub(/\s+/, "")).to include('tddata-sticky="true"')
    end

    it "sets data-sticky to false when a violation is not sticky" do
      non_sticky_warning = Danger::Violation.new("my warning", false)
      result = dummy.generate_comment(warnings: [non_sticky_warning], errors: [], messages: [])
      expect(result.gsub(/\s+/, "")).to include('tddata-sticky="false"')
    end

    it "truncates comments which would exceed githubs maximum comment length" do
      warnings = (1..900).map { |i| "single long warning" * rand(1..10) + i.to_s }
      result = dummy.generate_comment(warnings: violations_factory(warnings), errors: violations_factory([]), messages: [])
      expect(result.length).to be <= GITHUB_MAX_COMMENT_LENGTH
      expect(result).to include("has been truncated")
    end
  end

  describe "#generate_message_group_comment" do
    subject do
      dummy.generate_message_group_comment(message_group: message_group,
                                           danger_id: danger_id,
                                           resolved: resolved,
                                           template: template)
    end

    let(:message_group) { Danger::MessageGroup.new(file: file, line: line) }
    let(:file) { nil }
    let(:line) { nil }
    let(:resolved) { [] }
    let(:danger_id) { Base64.encode64(Random.new.bytes(10)).chomp }

    context "when template is bitbucket_server_message_group" do
      let(:template) { "bitbucket_server_message_group" }

      context "with one of each type of message" do
        before do
          message_group << Danger::Violation.new("Hello!", false, file, line, type: :error)
          message_group << Danger::Violation.new("World!", false, file, line, type: :warning)
          message_group << Danger::Violation.new("HOW R", false, file, line, type: :message)
          message_group << Danger::Markdown.new("U DOING?", file, line)
        end

        it "spits out a beautiful comment" do
          expect(subject).to eq <<~COMMENT
            :no_entry_sign: Hello!

            :warning: World!

            :blue_book: HOW R


            U DOING?

            Generated by :no_entry_sign: [Danger](https://danger.systems/ "generated_by_#{danger_id}")
          COMMENT
        end
      end

      context "with two markdowns" do
        before do
          message_group << Danger::Markdown.new("markdown one", file, line)
          message_group << Danger::Markdown.new("markdown two", file, line)
        end

        it "spits out a beautiful comment" do
          expect(subject).to eq <<~COMMENT
            markdown one

            markdown two

            Generated by :no_entry_sign: [Danger](https://danger.systems/ "generated_by_#{danger_id}")
          COMMENT
        end
      end

      context "without any messages" do
        it "outputs just the Generated line" do
          expect(subject).to eq <<~COMMENT
            Generated by :no_entry_sign: [Danger](https://danger.systems/ "generated_by_#{danger_id}")
          COMMENT
        end
      end
    end
  end

  describe "#generate_description" do
    it "Handles no errors or warnings" do
      message = dummy.generate_description(warnings: [], errors: [])
      expect(message).to include("All green.")
    end

    it "handles a single error and a single warning" do
      message = dummy.generate_description(warnings: [1], errors: [1])

      expect(message).to include("⚠️ ")
      expect(message).to include("Error")
      expect(message).to include("Warning")
      expect(message).to include("Don't worry, everything is fixable.")
    end

    it "handles multiple errors and warning with pluralisation" do
      message = dummy.generate_description(warnings: [1, 2], errors: [1, 2])

      expect(message).to include("⚠️ ")
      expect(message).to include("Errors")
      expect(message).to include("Warnings")
      expect(message).to include("Don't worry, everything is fixable.")
    end
  end
end
