# frozen_string_literal: true

require "rails_helper"

describe ChatMessage do
  fab!(:message) { Fabricate(:chat_message, message: "hey friend, what's up?!") }

  describe ".cook" do
    it "does not support HTML tags" do
      cooked = ChatMessage.cook("<h1>test</h1>")

      expect(cooked).to eq("<p>&lt;h1&gt;test&lt;/h1&gt;</p>")
    end

    it "does not support headings" do
      cooked = ChatMessage.cook("## heading 2")

      expect(cooked).to eq("<p>## heading 2</p>")
    end

    it "does not support horizontal rules" do
      cooked = ChatMessage.cook("---")

      expect(cooked).to eq("<p>---</p>")
    end

    it "supports backticks rule" do
      cooked = ChatMessage.cook("`test`")

      expect(cooked).to eq("<p><code>test</code></p>")
    end

    it "supports fence rule" do
      cooked = ChatMessage.cook(<<~RAW)
      ```
      something = test
      ```
      RAW

      expect(cooked).to eq(<<~COOKED.chomp)
      <pre><code class="lang-auto">something = test
      </code></pre>
      COOKED
    end

    it "supports fence rule with language support" do
      cooked = ChatMessage.cook(<<~RAW)
      ```ruby
      Widget.triangulate(argument: "no u")
      ```
      RAW

      expect(cooked).to eq(<<~COOKED.chomp)
      <pre><code class="lang-ruby">Widget.triangulate(argument: "no u")
      </code></pre>
      COOKED
    end

    it "supports code rule" do
      cooked = ChatMessage.cook("    something = test")

      expect(cooked).to eq("<pre><code>something = test\n</code></pre>")
    end

    it "supports blockquote rule" do
      cooked = ChatMessage.cook("> a quote")

      expect(cooked).to eq("<blockquote>\n<p>a quote</p>\n</blockquote>")
    end

    it "supports quote bbcode" do
      topic = Fabricate(:topic, title: "Some quotable topic")
      post = Fabricate(:post, topic: topic)
      SiteSetting.external_system_avatars_enabled = false
      avatar_src =
        "//test.localhost#{User.system_avatar_template(post.user.username).gsub("{size}", "40")}"

      cooked = ChatMessage.cook(<<~RAW)
      [quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"]
      Mark me...this will go down in history.
      [/quote]
      RAW

      expect(cooked).to eq(<<~COOKED.chomp)
      <aside class="quote no-group" data-username="#{post.user.username}" data-post="#{post.post_number}" data-topic="#{topic.id}">
      <div class="title">
      <div class="quote-controls"></div>
      <img loading="lazy" alt="" width="20" height="20" src="#{avatar_src}" class="avatar"><a href="http://test.localhost/t/some-quotable-topic/#{topic.id}/#{post.post_number}">#{topic.title}</a>
      </div>
      <blockquote>
      <p>Mark me...this will go down in history.</p>
      </blockquote>
      </aside>
      COOKED
    end

    it "supports chat quote bbcode" do
      chat_channel = Fabricate(:category_channel, name: "testchannel")
      user = Fabricate(:user, username: "chatbbcodeuser")
      user2 = Fabricate(:user, username: "otherbbcodeuser")
      avatar_src =
        "//test.localhost#{User.system_avatar_template(user.username).gsub("{size}", "40")}"
      avatar_src2 =
        "//test.localhost#{User.system_avatar_template(user2.username).gsub("{size}", "40")}"
      msg1 =
        Fabricate(
          :chat_message,
          chat_channel: chat_channel,
          message: "this is the first message",
          user: user,
        )
      msg2 =
        Fabricate(
          :chat_message,
          chat_channel: chat_channel,
          message: "and another cool one",
          user: user2,
        )
      other_messages_to_quote = [msg1, msg2]
      cooked =
        ChatMessage.cook(
          ChatTranscriptService.new(
            chat_channel,
            Fabricate(:user),
            messages_or_ids: other_messages_to_quote.map(&:id),
          ).generate_markdown,
        )

      expect(cooked).to eq(<<~COOKED.chomp)
        <div class="chat-transcript chat-transcript-chained" data-message-id="#{msg1.id}" data-username="chatbbcodeuser" data-datetime="#{msg1.created_at.iso8601}" data-channel-name="testchannel" data-channel-id="#{chat_channel.id}">
        <div class="chat-transcript-meta">
        Originally sent in <a href="/chat/c/-/#{chat_channel.id}">testchannel</a>
        </div>
        <div class="chat-transcript-user">
        <div class="chat-transcript-user-avatar">
        <img loading="lazy" alt="" width="20" height="20" src="#{avatar_src}" class="avatar">
        </div>
        <div class="chat-transcript-username">
        chatbbcodeuser</div>
        <div class="chat-transcript-datetime">
        <a href="/chat/c/-/#{chat_channel.id}/#{msg1.id}" title="#{msg1.created_at.iso8601}"></a>
        </div>
        </div>
        <div class="chat-transcript-messages">
        <p>this is the first message</p>
        </div>
        </div>
        <div class="chat-transcript chat-transcript-chained" data-message-id="#{msg2.id}" data-username="otherbbcodeuser" data-datetime="#{msg2.created_at.iso8601}">
        <div class="chat-transcript-user">
        <div class="chat-transcript-user-avatar">
        <img loading="lazy" alt="" width="20" height="20" src="#{avatar_src2}" class="avatar">
        </div>
        <div class="chat-transcript-username">
        otherbbcodeuser</div>
        <div class="chat-transcript-datetime">
        <span title="#{msg2.created_at.iso8601}"></span>
        </div>
        </div>
        <div class="chat-transcript-messages">
        <p>and another cool one</p>
        </div>
        </div>
      COOKED
    end

    it "supports strikethrough rule" do
      cooked = ChatMessage.cook("~~test~~")

      expect(cooked).to eq("<p><s>test</s></p>")
    end

    it "supports emphasis rule" do
      cooked = ChatMessage.cook("**bold**")

      expect(cooked).to eq("<p><strong>bold</strong></p>")
    end

    it "supports link markdown rule" do
      chat_message = Fabricate(:chat_message, message: "[test link](https://www.example.com)")

      expect(chat_message.cooked).to eq(
        "<p><a href=\"https://www.example.com\" rel=\"noopener nofollow ugc\">test link</a></p>",
      )
    end

    it "supports table markdown plugin" do
      cooked = ChatMessage.cook(<<~RAW)
      | Command | Description |
      | --- | --- |
      | git status | List all new or modified files |
      RAW

      expected = <<~COOKED
      <div class="md-table">
      <table>
      <thead>
      <tr>
      <th>Command</th>
      <th>Description</th>
      </tr>
      </thead>
      <tbody>
      <tr>
      <td>git status</td>
      <td>List all new or modified files</td>
      </tr>
      </tbody>
      </table>
      </div>
      COOKED

      expect(cooked).to eq(expected.chomp)
    end

    it "supports onebox markdown plugin" do
      cooked = ChatMessage.cook("https://www.example.com")

      expect(cooked).to eq(
        "<p><a href=\"https://www.example.com\" class=\"onebox\" target=\"_blank\" rel=\"noopener nofollow ugc\">https://www.example.com</a></p>",
      )
    end

    it "supports emoji plugin" do
      cooked = ChatMessage.cook(":grin:")

      expect(cooked).to eq(
        "<p><img src=\"/images/emoji/twitter/grin.png?v=12\" title=\":grin:\" class=\"emoji only-emoji\" alt=\":grin:\" loading=\"lazy\" width=\"20\" height=\"20\"></p>",
      )
    end

    it "supports mentions plugin" do
      cooked = ChatMessage.cook("@mention")

      expect(cooked).to eq("<p><span class=\"mention\">@mention</span></p>")
    end

    it "supports category-hashtag plugin" do
      # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
      SiteSetting.enable_experimental_hashtag_autocomplete = false

      category = Fabricate(:category)

      cooked = ChatMessage.cook("##{category.slug}")

      expect(cooked).to eq(
        "<p><a class=\"hashtag\" href=\"#{category.url}\">#<span>#{category.slug}</span></a></p>",
      )
    end

    it "supports hashtag-autocomplete plugin" do
      SiteSetting.chat_enabled = true
      SiteSetting.enable_experimental_hashtag_autocomplete = true

      category = Fabricate(:category)
      user = Fabricate(:user)

      cooked = ChatMessage.cook("##{category.slug}", user_id: user.id)

      expect(cooked).to eq(
        "<p><a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"#{category.slug}\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>#{category.name}</span></a></p>",
      )
    end

    it "supports censored plugin" do
      watched_word = Fabricate(:watched_word, action: WatchedWord.actions[:censor])

      cooked = ChatMessage.cook(watched_word.word)

      expect(cooked).to eq("<p>■■■■■</p>")
    end

    it "includes links in pretty text excerpt if the raw message is a single link and the PrettyText excerpt is blank" do
      message =
        Fabricate.build(
          :chat_message,
          message: "https://twitter.com/EffinBirds/status/1518743508378697729",
        )
      expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729")
      message =
        Fabricate.build(
          :chat_message,
          message: "https://twitter.com/EffinBirds/status/1518743508378697729",
          cooked: <<~COOKED,
          <aside class=\"onebox twitterstatus\" data-onebox-src=\"https://twitter.com/EffinBirds/status/1518743508378697729\">\n  <header class=\"source\">\n\n      <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">twitter.com</a>\n  </header>\n\n  <article class=\"onebox-body\">\n    \n<h4><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">Effin' Birds</a></h4>\n<div class=\"twitter-screen-name\"><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">@EffinBirds</a></div>\n\n<div class=\"tweet\">\n  <span class=\"tweet-description\">https://t.co/LjlqMm9lck</span>\n</div>\n\n<div class=\"date\">\n  <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" class=\"timestamp\" target=\"_blank\" rel=\"nofollow ugc noopener\">5:07 PM - 25 Apr 2022</a>\n\n    <span class=\"like\">\n      <svg viewbox=\"0 0 512 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z\"></path>\n      </svg>\n      2.5K\n    </span>\n\n    <span class=\"retweet\">\n      <svg viewbox=\"0 0 640 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M629.657 343.598L528.971 444.284c-9.373 9.372-24.568 9.372-33.941 0L394.343 343.598c-9.373-9.373-9.373-24.569 0-33.941l10.823-10.823c9.562-9.562 25.133-9.34 34.419.492L480 342.118V160H292.451a24.005 24.005 0 0 1-16.971-7.029l-16-16C244.361 121.851 255.069 96 276.451 96H520c13.255 0 24 10.745 24 24v222.118l40.416-42.792c9.285-9.831 24.856-10.054 34.419-.492l10.823 10.823c9.372 9.372 9.372 24.569-.001 33.941zm-265.138 15.431A23.999 23.999 0 0 0 347.548 352H160V169.881l40.416 42.792c9.286 9.831 24.856 10.054 34.419.491l10.822-10.822c9.373-9.373 9.373-24.569 0-33.941L144.971 67.716c-9.373-9.373-24.569-9.373-33.941 0L10.343 168.402c-9.373 9.373-9.373 24.569 0 33.941l10.822 10.822c9.562 9.562 25.133 9.34 34.419-.491L96 169.881V392c0 13.255 10.745 24 24 24h243.549c21.382 0 32.09-25.851 16.971-40.971l-16.001-16z\"></path>\n      </svg>\n      499\n    </span>\n</div>\n\n  </article>\n\n  <div class=\"onebox-metadata\">\n    \n    \n  </div>\n\n  <div style=\"clear: both\"></div>\n</aside>\n
        COOKED
        )
      expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729")
      message =
        Fabricate.build(
          :chat_message,
          message:
            "wow check out these birbs https://twitter.com/EffinBirds/status/1518743508378697729",
        )
      expect(message.excerpt).to eq(
        "wow check out these birbs <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" class=\"inline-onebox-loading\" rel=\"noopener nofollow ugc\">https://twitter.com/Effi&hellip;</a>",
      )
    end

    it "returns an empty string if PrettyText.excerpt returns empty string" do
      message = Fabricate(:chat_message, message: <<~MSG)
      [quote="martin, post:30, topic:3179, full:true"]
      This is a real **quote** topic with some *markdown* in it I can quote.
      [/quote]
      MSG
      expect(message.excerpt).to eq("")
    end

    it "excerpts upload file name if message is empty" do
      gif =
        Fabricate(:upload, original_filename: "cat.gif", width: 400, height: 300, extension: "gif")
      message = Fabricate(:chat_message, message: "")
      UploadReference.create(target: message, upload: gif)

      expect(message.excerpt).to eq "cat.gif"
    end

    it "supports autolink with <>" do
      cooked = ChatMessage.cook("<https://github.com/discourse/discourse-chat/pull/468>")

      expect(cooked).to eq(
        "<p><a href=\"https://github.com/discourse/discourse-chat/pull/468\" rel=\"noopener nofollow ugc\">https://github.com/discourse/discourse-chat/pull/468</a></p>",
      )
    end

    it "supports lists" do
      cooked = ChatMessage.cook(<<~MSG)
      wow look it's a list

      * item 1
      * item 2
      MSG

      expect(cooked).to eq(<<~HTML.chomp)
      <p>wow look it's a list</p>
      <ul>
      <li>item 1</li>
      <li>item 2</li>
      </ul>
      HTML
    end

    it "supports inline emoji" do
      cooked = ChatMessage.cook(":D")
      expect(cooked).to eq(<<~HTML.chomp)
      <p><img src="/images/emoji/twitter/smiley.png?v=12" title=":smiley:" class="emoji only-emoji" alt=":smiley:" loading=\"lazy\" width=\"20\" height=\"20\"></p>
      HTML
    end

    it "supports emoji shortcuts" do
      cooked = ChatMessage.cook("this is a replace test :P :|")
      expect(cooked).to eq(<<~HTML.chomp)
        <p>this is a replace test <img src="/images/emoji/twitter/stuck_out_tongue.png?v=12" title=":stuck_out_tongue:" class="emoji" alt=":stuck_out_tongue:" loading=\"lazy\" width=\"20\" height=\"20\"> <img src="/images/emoji/twitter/expressionless.png?v=12" title=":expressionless:" class="emoji" alt=":expressionless:" loading=\"lazy\" width=\"20\" height=\"20\"></p>
      HTML
    end

    it "supports spoilers" do
      if SiteSetting.respond_to?(:spoiler_enabled) && SiteSetting.spoiler_enabled
        cooked = ChatMessage.cook("[spoiler]the planet of the apes was earth all along[/spoiler]")

        expect(cooked).to eq(
          "<div class=\"spoiler\">\n<p>the planet of the apes was earth all along</p>\n</div>",
        )
      end
    end

    context "when unicode usernames are enabled" do
      before { SiteSetting.unicode_usernames = true }

      it "cooks unicode mentions" do
        user = Fabricate(:unicode_user)
        cooked = ChatMessage.cook("<h1>@#{user.username}</h1>")

        expect(cooked).to eq("<p>&lt;h1&gt;@#{user.username}&lt;/h1&gt;</p>")
      end
    end
  end

  describe ".to_markdown" do
    it "renders the message without uploads" do
      expect(message.to_markdown).to eq("hey friend, what's up?!")
    end

    it "renders the message with uploads" do
      image =
        Fabricate(
          :upload,
          original_filename: "test_image.jpg",
          width: 400,
          height: 300,
          extension: "jpg",
        )
      image2 =
        Fabricate(:upload, original_filename: "meme.jpg", width: 10, height: 10, extension: "jpg")
      UploadReference.create!(target: message, upload: image)
      UploadReference.create!(target: message, upload: image2)
      expect(message.to_markdown).to eq(<<~MSG.chomp)
      hey friend, what's up?!

      ![test_image.jpg|400x300](#{image.short_url})
      ![meme.jpg|10x10](#{image2.short_url})
      MSG
    end
  end

  describe ".push_notification_excerpt" do
    it "truncates to 400 characters" do
      message = ChatMessage.new(message: "Hello, World!" * 40)
      expect(message.push_notification_excerpt.size).to eq(400)
    end

    it "encodes emojis" do
      message = ChatMessage.new(message: ":grinning:")
      expect(message.push_notification_excerpt).to eq("😀")
    end
  end

  describe "blocking duplicate messages" do
    fab!(:channel) { Fabricate(:chat_channel, user_count: 10) }
    fab!(:user1) { Fabricate(:user) }
    fab!(:user2) { Fabricate(:user) }

    before { SiteSetting.chat_duplicate_message_sensitivity = 1 }

    it "blocks duplicate messages for the message, channel user, and message age requirements" do
      Fabricate(:chat_message, message: "this is duplicate", chat_channel: channel, user: user1)
      message = ChatMessage.new(message: "this is duplicate", chat_channel: channel, user: user2)
      message.validate_message(has_uploads: false)
      expect(message.errors.full_messages).to include(I18n.t("chat.errors.duplicate_message"))
    end
  end

  describe "#destroy" do
    it "nullify messages with in_reply_to_id to this destroyed message" do
      message_1 = Fabricate(:chat_message)
      message_2 = Fabricate(:chat_message, in_reply_to_id: message_1.id)
      message_3 = Fabricate(:chat_message, in_reply_to_id: message_2.id)

      expect(message_2.in_reply_to_id).to eq(message_1.id)

      message_1.destroy!

      expect(message_2.reload.in_reply_to_id).to be_nil
      expect(message_3.reload.in_reply_to_id).to eq(message_2.id)
    end

    it "destroys chat_message_revisions" do
      message_1 = Fabricate(:chat_message)
      revision_1 = Fabricate(:chat_message_revision, chat_message: message_1)

      message_1.destroy!

      expect { revision_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    it "destroys chat_message_reactions" do
      message_1 = Fabricate(:chat_message)
      reaction_1 = Fabricate(:chat_message_reaction, chat_message: message_1)

      message_1.destroy!

      expect { reaction_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    it "destroys chat_mention" do
      message_1 = Fabricate(:chat_message)
      mention_1 = Fabricate(:chat_mention, chat_message: message_1)

      message_1.destroy!

      expect { mention_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    it "destroys chat_webhook_event" do
      message_1 = Fabricate(:chat_message)
      webhook_1 = Fabricate(:chat_webhook_event, chat_message: message_1)

      message_1.destroy!

      expect { webhook_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    it "destroys upload_references and chat_uploads" do
      message_1 = Fabricate(:chat_message)
      upload_reference_1 = Fabricate(:upload_reference, target: message_1)
      upload_1 = Fabricate(:upload)
      # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
      DB.exec(<<~SQL)
        INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at)
        VALUES(#{upload_1.id}, #{message_1.id}, NOW(), NOW())
      SQL

      message_1.destroy!

      expect(DB.query("SELECT * FROM chat_uploads WHERE upload_id = #{upload_1.id}")).to eq([])
      expect { upload_reference_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    describe "bookmarks" do
      before { Bookmark.register_bookmarkable(ChatMessageBookmarkable) }

      it "destroys bookmarks" do
        message_1 = Fabricate(:chat_message)
        bookmark_1 = Fabricate(:bookmark, bookmarkable: message_1)

        message_1.destroy!

        expect { bookmark_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end
  end

  describe "#rebake!" do
    fab!(:chat_message) { Fabricate(:chat_message) }

    describe "hashtags" do
      fab!(:category) { Fabricate(:category) }
      fab!(:group) { Fabricate(:group) }
      fab!(:secure_category) { Fabricate(:private_category, group: group) }

      before do
        SiteSetting.chat_enabled = true
        SiteSetting.enable_experimental_hashtag_autocomplete = true
        SiteSetting.suppress_secured_categories_from_admin = true
      end

      it "keeps the same hashtags the user has permission to after rebake" do
        group.add(chat_message.user)
        chat_message.update!(
          message:
            "this is the message ##{category.slug} ##{secure_category.slug} ##{chat_message.chat_channel.slug}",
        )
        chat_message.cook
        chat_message.save!

        expect(chat_message.reload.cooked).to include(secure_category.name)

        chat_message.rebake!
        expect(chat_message.reload.cooked).to include(secure_category.name)
      end
    end
  end

  describe "#attach_uploads" do
    fab!(:chat_message) { Fabricate(:chat_message) }
    fab!(:upload_1) { Fabricate(:upload) }
    fab!(:upload_2) { Fabricate(:upload) }

    it "creates an UploadReference record for the provided uploads" do
      chat_message.attach_uploads([upload_1, upload_2])
      upload_references = UploadReference.where(upload_id: [upload_1, upload_2])
      expect(chat_upload_count([upload_1, upload_2])).to eq(0)
      expect(upload_references.count).to eq(2)
      expect(upload_references.map(&:target_id).uniq).to eq([chat_message.id])
      expect(upload_references.map(&:target_type).uniq).to eq(["ChatMessage"])
    end

    it "does nothing if the message record is new" do
      expect { ChatMessage.new.attach_uploads([upload_1, upload_2]) }.to not_change {
        chat_upload_count
      }.and not_change { UploadReference.count }
    end

    it "does nothing for an empty uploads array" do
      expect { chat_message.attach_uploads([]) }.to not_change {
        chat_upload_count
      }.and not_change { UploadReference.count }
    end
  end

  # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
  def chat_upload_count(uploads = nil)
    return DB.query_single("SELECT COUNT(*) FROM chat_uploads").first if !uploads
    DB.query_single(
      "SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})",
    ).first
  end
end
