# frozen_string_literal: true

require "imap/sync"

RSpec.describe Imap::Sync do
  before do
    SiteSetting.tagging_enabled = true
    SiteSetting.pm_tags_allowed_for_groups = "1|2|3"

    SiteSetting.enable_imap = true

    Jobs.run_immediately!
  end

  let(:group) do
    Fabricate(
      :group,
      imap_server: "imap.gmail.com",
      imap_port: 993,
      email_username: "groupemailusername@example.com",
      email_password: "password",
      imap_mailbox_name: "[Gmail]/All Mail",
    )
  end

  let(:sync_handler) { Imap::Sync.new(group) }

  before do
    mocked_imap_provider =
      MockedImapProvider.new(
        group.imap_server,
        port: group.imap_port,
        ssl: group.imap_ssl,
        username: group.email_username,
        password: group.email_password,
      )
    Imap::Providers::Detector.stubs(:init_with_detected_provider).returns(mocked_imap_provider)
  end

  describe "no previous sync" do
    let(:from) { "john@free.fr" }
    let(:email_subject) { "Testing email post" }
    let(:message_id) { "#{SecureRandom.hex}@example.com" }

    let(:email) do
      EmailFabricator(
        from: from,
        to: group.email_username,
        subject: email_subject,
        message_id: message_id,
      )
    end

    before do
      provider = MockedImapProvider.any_instance
      provider.stubs(:open_mailbox).returns(uid_validity: 1)
      provider.stubs(:uids).with.returns([100])
      provider.stubs(:uids).with(to: 100).returns([100])
      provider.stubs(:uids).with(from: 101).returns([])
      provider.stubs(:emails).returns(
        [
          {
            "UID" => 100,
            "LABELS" => %w[\\Important test-label],
            "FLAGS" => %i[Seen],
            "RFC822" => email,
          },
        ],
      )
    end

    it "creates a topic from an incoming email" do
      expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(1).and change { IncomingEmail.count }.by(1)

      expect(IncomingEmail.last.created_via).to eq(IncomingEmail.created_via_types[:imap])

      expect(group.imap_uid_validity).to eq(1)
      expect(group.imap_last_uid).to eq(100)

      topic = Topic.last
      expect(topic.title).to eq(email_subject)
      expect(topic.user.email).to eq(from)
      expect(topic.tags.pluck(:name)).to contain_exactly("seen", "important", "test-label")

      post = topic.first_post
      expect(post.raw).to eq("This is an email *body*. :smile:")

      incoming_email = post.incoming_email
      expect(incoming_email.raw.lines.map(&:strip)).to eq(email.lines.map(&:strip))
      expect(incoming_email.message_id).to eq(message_id)
      expect(incoming_email.from_address).to eq(from)
      expect(incoming_email.to_addresses).to eq(group.email_username)
      expect(incoming_email.imap_uid_validity).to eq(1)
      expect(incoming_email.imap_uid).to eq(100)
      expect(incoming_email.imap_sync).to eq(false)
      expect(incoming_email.imap_group_id).to eq(group.id)
    end

    context "when tagging not enabled" do
      before { SiteSetting.tagging_enabled = false }

      it "creates a topic from an incoming email but with no tags added" do
        expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
                Post.where(post_type: Post.types[:regular]).count
              }.by(1).and change { IncomingEmail.count }.by(1)

        expect(group.imap_uid_validity).to eq(1)
        expect(group.imap_last_uid).to eq(100)

        topic = Topic.last
        expect(topic.title).to eq(email_subject)
        expect(topic.user.email).to eq(from)
        expect(topic.tags).to eq([])
      end
    end

    it "does not duplicate topics" do
      expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(1).and change { IncomingEmail.count }.by(1)

      expect { sync_handler.process }.to not_change { Topic.count }.and not_change {
              Post.where(post_type: Post.types[:regular]).count
            }.and not_change { IncomingEmail.count }
    end

    it "creates a new incoming email if the message ID does not match the receiver post id regex" do
      incoming_email = Fabricate(:incoming_email, message_id: message_id)

      expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(1).and change { IncomingEmail.count }.by(1)

      last_incoming = IncomingEmail.where(message_id: message_id).last
      expect(last_incoming.message_id).to eq(message_id)
      expect(last_incoming.imap_uid_validity).to eq(1)
      expect(last_incoming.imap_uid).to eq(100)
      expect(last_incoming.imap_sync).to eq(false)
      expect(last_incoming.imap_group_id).to eq(group.id)
    end

    context "when the message id matches the receiver post id regex" do
      let(:message_id) { "topic/999/324@test.localhost" }
      it "does not duplicate incoming email" do
        incoming_email = Fabricate(:incoming_email, message_id: message_id)

        expect { sync_handler.process }.to not_change { Topic.count }.and not_change {
                Post.where(post_type: Post.types[:regular]).count
              }.and not_change { IncomingEmail.count }

        incoming_email.reload
        expect(incoming_email.message_id).to eq(message_id)
        expect(incoming_email.imap_uid_validity).to eq(1)
        expect(incoming_email.imap_uid).to eq(100)
        expect(incoming_email.imap_sync).to eq(false)
        expect(incoming_email.imap_group_id).to eq(group.id)
      end
    end
  end

  describe "previous sync" do
    let(:email_subject) { "Testing email post" }

    let(:first_from) { "john@free.fr" }
    let(:first_message_id) { SecureRandom.hex }
    let(:first_body) { "This is the first message of this exchange." }

    let(:second_from) { "sam@free.fr" }
    let(:second_message_id) { SecureRandom.hex }
    let(:second_body) { "<p>This is an <b>answer</b> to this message.</p>" }

    it "continues with new emails" do
      provider = MockedImapProvider.any_instance
      provider.stubs(:open_mailbox).returns(uid_validity: 1)

      provider.stubs(:uids).with.returns([100])
      provider
        .stubs(:emails)
        .with([100], %w[UID FLAGS LABELS RFC822], anything)
        .returns(
          [
            {
              "UID" => 100,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Seen],
              "RFC822" =>
                EmailFabricator(
                  message_id: first_message_id,
                  from: first_from,
                  to: group.email_username,
                  cc: second_from,
                  subject: email_subject,
                  body: first_body,
                ),
            },
          ],
        )

      expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(1).and change { IncomingEmail.count }.by(1)

      topic = Topic.last
      expect(topic.title).to eq(email_subject)
      expect(GroupArchivedMessage.where(topic_id: topic.id).exists?).to eq(false)

      post = Post.where(post_type: Post.types[:regular]).last
      expect(post.user.email).to eq(first_from)
      expect(post.raw).to eq(first_body)
      expect(group.imap_uid_validity).to eq(1)
      expect(group.imap_last_uid).to eq(100)

      provider.stubs(:uids).with(to: 100).returns([100])
      provider.stubs(:uids).with(from: 101).returns([200])
      provider
        .stubs(:emails)
        .with([100], %w[UID FLAGS LABELS ENVELOPE], anything)
        .returns([{ "UID" => 100, "LABELS" => %w[\\Inbox], "FLAGS" => %i[Seen] }])
      provider
        .stubs(:emails)
        .with([200], %w[UID FLAGS LABELS RFC822], anything)
        .returns(
          [
            {
              "UID" => 200,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Recent],
              "RFC822" =>
                EmailFabricator(
                  message_id: SecureRandom.hex,
                  in_reply_to: first_message_id,
                  from: second_from,
                  to: group.email_username,
                  subject: "Re: #{email_subject}",
                  body: second_body,
                ),
            },
          ],
        )

      expect { sync_handler.process }.to not_change { Topic.count }.and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(1).and change { IncomingEmail.count }.by(1)

      post = Post.where(post_type: Post.types[:regular]).last
      expect(post.user.email).to eq(second_from)
      expect(post.raw).to eq(second_body)
      expect(group.imap_uid_validity).to eq(1)
      expect(group.imap_last_uid).to eq(200)

      provider.stubs(:uids).with(to: 200).returns([100, 200])
      provider.stubs(:uids).with(from: 201).returns([])
      provider
        .stubs(:emails)
        .with([100, 200], %w[UID FLAGS LABELS ENVELOPE], anything)
        .returns(
          [
            { "UID" => 100, "LABELS" => %w[], "FLAGS" => %i[Seen] },
            { "UID" => 200, "LABELS" => %w[], "FLAGS" => %i[Recent] },
          ],
        )

      expect { sync_handler.process }.to not_change { Topic.count }.and not_change {
              Post.where(post_type: Post.types[:regular]).count
            }.and not_change { IncomingEmail.count }

      topic = Topic.last
      expect(topic.title).to eq(email_subject)
      expect(GroupArchivedMessage.where(topic_id: topic.id).exists?).to eq(true)

      expect(Topic.last.posts.where(post_type: Post.types[:regular]).count).to eq(2)
    end

    describe "detecting deleted emails and deleting the topic in discourse" do
      let(:provider) { MockedImapProvider.any_instance }
      before do
        provider.stubs(:open_mailbox).returns(uid_validity: 1)

        provider.stubs(:uids).with.returns([100])
        provider
          .stubs(:emails)
          .with([100], %w[UID FLAGS LABELS RFC822], anything)
          .returns(
            [
              {
                "UID" => 100,
                "LABELS" => %w[\\Inbox],
                "FLAGS" => %i[Seen],
                "RFC822" =>
                  EmailFabricator(
                    message_id: first_message_id,
                    from: first_from,
                    to: group.email_username,
                    cc: second_from,
                    subject: email_subject,
                    body: first_body,
                  ),
              },
            ],
          )
      end

      it "detects previously synced UIDs are missing and deletes the posts if they are in the trash mailbox" do
        sync_handler.process
        incoming_100 = IncomingEmail.find_by(imap_uid: 100)
        provider.stubs(:uids).with.returns([])

        provider.stubs(:uids).with(to: 100).returns([])
        provider.stubs(:uids).with(from: 101).returns([])
        provider.stubs(:find_spam_by_message_ids).returns(stub(spam_emails: []))
        provider.stubs(:find_trashed_by_message_ids).returns(
          stub(
            trashed_emails: [stub(uid: 10, message_id: incoming_100.message_id)],
            trash_uid_validity: 99,
          ),
        )
        sync_handler.process

        incoming_100.reload
        expect(incoming_100.imap_uid_validity).to eq(99)
        expect(incoming_100.imap_uid).to eq(10)
        expect(Post.with_deleted.find(incoming_100.post_id).deleted_at).not_to eq(nil)
        expect(Topic.with_deleted.find(incoming_100.topic_id).deleted_at).not_to eq(nil)
      end

      it "detects previously synced UIDs are missing and deletes the posts if they are in the spam/junk mailbox" do
        sync_handler.process
        incoming_100 = IncomingEmail.find_by(imap_uid: 100)
        provider.stubs(:uids).with.returns([])

        provider.stubs(:uids).with(to: 100).returns([])
        provider.stubs(:uids).with(from: 101).returns([])
        provider.stubs(:find_trashed_by_message_ids).returns(stub(trashed_emails: []))
        provider.stubs(:find_spam_by_message_ids).returns(
          stub(
            spam_emails: [stub(uid: 10, message_id: incoming_100.message_id)],
            spam_uid_validity: 99,
          ),
        )
        sync_handler.process

        incoming_100.reload
        expect(incoming_100.imap_uid_validity).to eq(99)
        expect(incoming_100.imap_uid).to eq(10)
        expect(Post.with_deleted.find(incoming_100.post_id).deleted_at).not_to eq(nil)
        expect(Topic.with_deleted.find(incoming_100.topic_id).deleted_at).not_to eq(nil)
      end

      it "marks the incoming email as IMAP missing if it cannot be found in spam or trash" do
        sync_handler.process
        incoming_100 = IncomingEmail.find_by(imap_uid: 100)
        provider.stubs(:uids).with.returns([])

        provider.stubs(:uids).with(to: 100).returns([])
        provider.stubs(:uids).with(from: 101).returns([])
        provider.stubs(:find_trashed_by_message_ids).returns(stub(trashed_emails: []))
        provider.stubs(:find_spam_by_message_ids).returns(stub(spam_emails: []))
        sync_handler.process

        incoming_100.reload
        expect(incoming_100.imap_missing).to eq(true)
      end

      it "detects the topic being deleted on the discourse site and deletes on the IMAP server and
      does not attempt to delete again on discourse site when deleted already by us on the IMAP server" do
        SiteSetting.enable_imap_write = true
        sync_handler.process
        incoming_100 = IncomingEmail.find_by(imap_uid: 100)
        provider.stubs(:uids).with.returns([100])

        provider.stubs(:uids).with(to: 100).returns([100])
        provider.stubs(:uids).with(from: 101).returns([])

        PostDestroyer.new(Discourse.system_user, incoming_100.post).destroy
        provider
          .stubs(:emails)
          .with([100], %w[UID FLAGS LABELS ENVELOPE], anything)
          .returns(
            [
              {
                "UID" => 100,
                "LABELS" => %w[\\Inbox],
                "FLAGS" => %i[Seen],
                "RFC822" =>
                  EmailFabricator(
                    message_id: first_message_id,
                    from: first_from,
                    to: group.email_username,
                    cc: second_from,
                    subject: email_subject,
                    body: first_body,
                  ),
              },
            ],
          )
        provider
          .stubs(:emails)
          .with(100, %w[FLAGS LABELS])
          .returns([{ "LABELS" => %w[\\Inbox], "FLAGS" => %i[Seen] }])

        provider.expects(:trash).with(100)
        sync_handler.process

        provider.stubs(:uids).with.returns([])

        provider.stubs(:uids).with(to: 100).returns([])
        provider.stubs(:uids).with(from: 101).returns([])
        provider.stubs(:find_spam_by_message_ids).returns(stub(spam_emails: []))
        provider.stubs(:find_trashed_by_message_ids).returns(
          stub(
            trashed_emails: [stub(uid: 10, message_id: incoming_100.message_id)],
            trash_uid_validity: 99,
          ),
        )
        PostDestroyer.expects(:new).never

        sync_handler.process

        incoming_100.reload
        expect(incoming_100.imap_uid_validity).to eq(99)
        expect(incoming_100.imap_uid).to eq(10)
      end
    end

    describe "archiving emails" do
      let(:provider) { MockedImapProvider.any_instance }
      before do
        SiteSetting.enable_imap_write = true
        provider.stubs(:open_mailbox).returns(uid_validity: 1)

        provider.stubs(:uids).with.returns([100])
        provider
          .stubs(:emails)
          .with([100], %w[UID FLAGS LABELS RFC822], anything)
          .returns(
            [
              {
                "UID" => 100,
                "LABELS" => %w[\\Inbox],
                "FLAGS" => %i[Seen],
                "RFC822" =>
                  EmailFabricator(
                    message_id: first_message_id,
                    from: first_from,
                    to: group.email_username,
                    cc: second_from,
                    subject: email_subject,
                    body: first_body,
                  ),
              },
            ],
          )

        sync_handler.process
        @incoming_email = IncomingEmail.find_by(message_id: first_message_id)
        @topic = @incoming_email.topic

        provider.stubs(:uids).with(to: 100).returns([100])
        provider.stubs(:uids).with(from: 101).returns([101])
        provider
          .stubs(:emails)
          .with([100], %w[UID FLAGS LABELS ENVELOPE], anything)
          .returns([{ "UID" => 100, "LABELS" => %w[\\Inbox], "FLAGS" => %i[Seen] }])
        provider.stubs(:emails).with([101], %w[UID FLAGS LABELS RFC822], anything).returns([])
        provider
          .stubs(:emails)
          .with(100, %w[FLAGS LABELS])
          .returns([{ "LABELS" => %w[\\Inbox], "FLAGS" => %i[Seen] }])
      end

      it "archives an email on the IMAP server when archived in discourse" do
        GroupArchivedMessage.archive!(group.id, @topic, skip_imap_sync: false)
        @incoming_email.update(imap_sync: true)

        provider.stubs(:store).with(100, "FLAGS", anything, anything)
        provider.stubs(:store).with(100, "LABELS", ["\\Inbox"], ["seen"])

        provider.expects(:archive).with(100)
        sync_handler.process
      end

      it "does not archive email if not archived in discourse, it unarchives it instead" do
        @incoming_email.update(imap_sync: true)
        provider.stubs(:store).with(100, "FLAGS", anything, anything)
        provider.stubs(:store).with(100, "LABELS", ["\\Inbox"], ["\\Inbox", "seen"])

        provider.expects(:archive).with(100).never
        provider.expects(:unarchive).with(100)
        sync_handler.process
      end
    end
  end

  describe "invalidated previous sync" do
    let(:email_subject) { "Testing email post" }

    let(:first_from) { "john@free.fr" }
    let(:first_message_id) { SecureRandom.hex }
    let(:first_body) { "This is the first message of this exchange." }

    let(:second_from) { "sam@free.fr" }
    let(:second_message_id) { SecureRandom.hex }
    let(:second_body) { "<p>This is an <b>answer</b> to this message.</p>" }

    # TODO: Improve the invalidating flow for mailbox change. This is a destructive
    # action so it should not be done often.
    xit "is updated" do
      provider = MockedImapProvider.any_instance

      provider.stubs(:open_mailbox).returns(uid_validity: 1)
      provider.stubs(:uids).with.returns([100, 200])
      provider
        .stubs(:emails)
        .with([100, 200], %w[UID FLAGS LABELS RFC822], anything)
        .returns(
          [
            {
              "UID" => 100,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Seen],
              "RFC822" =>
                EmailFabricator(
                  message_id: first_message_id,
                  from: first_from,
                  to: group.email_username,
                  cc: second_from,
                  subject: email_subject,
                  body: first_body,
                ),
            },
            {
              "UID" => 200,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Recent],
              "RFC822" =>
                EmailFabricator(
                  message_id: second_message_id,
                  in_reply_to: first_message_id,
                  from: second_from,
                  to: group.email_username,
                  subject: "Re: #{email_subject}",
                  body: second_body,
                ),
            },
          ],
        )

      expect { sync_handler.process }.to change { Topic.count }.by(1).and change {
              Post.where(post_type: Post.types[:regular]).count
            }.by(2).and change { IncomingEmail.count }.by(2)

      imap_data = Topic.last.incoming_email.pluck(:imap_uid_validity, :imap_uid, :imap_group_id)
      expect(imap_data).to contain_exactly([1, 100, group.id], [1, 200, group.id])

      provider.stubs(:open_mailbox).returns(uid_validity: 2)
      provider.stubs(:uids).with.returns([111, 222])
      provider
        .stubs(:emails)
        .with([111, 222], %w[UID FLAGS LABELS RFC822], anything)
        .returns(
          [
            {
              "UID" => 111,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Seen],
              "RFC822" =>
                EmailFabricator(
                  message_id: first_message_id,
                  from: first_from,
                  to: group.email_username,
                  cc: second_from,
                  subject: email_subject,
                  body: first_body,
                ),
            },
            {
              "UID" => 222,
              "LABELS" => %w[\\Inbox],
              "FLAGS" => %i[Recent],
              "RFC822" =>
                EmailFabricator(
                  message_id: second_message_id,
                  in_reply_to: first_message_id,
                  from: second_from,
                  to: group.email_username,
                  subject: "Re: #{email_subject}",
                  body: second_body,
                ),
            },
          ],
        )

      expect { sync_handler.process }.to not_change { Topic.count }.and not_change {
              Post.where(post_type: Post.types[:regular]).count
            }.and not_change { IncomingEmail.count }

      imap_data = Topic.last.incoming_email.pluck(:imap_uid_validity, :imap_uid, :imap_group_id)
      expect(imap_data).to contain_exactly([2, 111, group.id], [2, 222, group.id])
    end
  end
end