# frozen_string_literal: true

require 'rails_helper'
require Rails.root.join 'spec/models/concerns/liquidable_shared.rb'

RSpec.describe Message do
  before do
    # rubocop:disable RSpec/AnyInstance
    allow_any_instance_of(described_class).to receive(:reindex_for_search).and_return(true)
    # rubocop:enable RSpec/AnyInstance
  end

  context 'with validations' do
    it { is_expected.to validate_presence_of(:inbox_id) }
    it { is_expected.to validate_presence_of(:conversation_id) }
    it { is_expected.to validate_presence_of(:account_id) }
  end

  describe 'length validations' do
    let!(:message) { create(:message) }

    context 'when it validates name length' do
      it 'valid when within limit' do
        message.content = 'a' * 120_000
        expect(message.valid?).to be true
      end

      it 'invalid when crossed the limit' do
        message.content = 'a' * 150_001
        message.processed_message_content = 'a' * 150_001
        message.valid?

        expect(message.errors[:processed_message_content]).to include('is too long (maximum is 150000 characters)')
        expect(message.errors[:content]).to include('is too long (maximum is 150000 characters)')
      end

      it 'adds error in case of message flooding' do
        with_modified_env 'CONVERSATION_MESSAGE_PER_MINUTE_LIMIT': '2' do
          conversation = message.conversation
          create(:message, conversation: conversation)
          conv_new_message = build(:message, conversation: message.conversation)

          expect(conv_new_message.valid?).to be false
          expect(conv_new_message.errors[:base]).to eq(['Too many messages'])
        end
      end
    end
  end

  describe 'concerns' do
    it_behaves_like 'liqudable'
  end

  describe 'message_filter_helpers' do
    context 'when webhook_sendable?' do
      [
        { type: :incoming, expected: true },
        { type: :outgoing, expected: true },
        { type: :template, expected: true },
        { type: :activity, expected: false }
      ].each do |scenario|
        it "returns #{scenario[:expected]} for #{scenario[:type]} message" do
          message = create(:message, message_type: scenario[:type])
          expect(message.webhook_sendable?).to eq(scenario[:expected])
        end
      end
    end
  end

  describe '#push_event_data' do
    subject(:push_event_data) { message.push_event_data }

    let(:message) { create(:message, echo_id: 'random-echo_id') }

    let(:expected_data) do
      {

        account_id: message.account_id,
        additional_attributes: message.additional_attributes,
        content_attributes: message.content_attributes,
        content_type: message.content_type,
        content: message.content,
        conversation_id: message.conversation.display_id,
        created_at: message.created_at.to_i,
        external_source_ids: message.external_source_ids,
        id: message.id,
        inbox_id: message.inbox_id,
        message_type: message.message_type_before_type_cast,
        private: message.private,
        processed_message_content: message.processed_message_content,
        sender_id: message.sender_id,
        sender_type: message.sender_type,
        source_id: message.source_id,
        status: message.status,
        updated_at: message.updated_at,
        conversation: {
          assignee_id: message.conversation.assignee_id,
          contact_inbox: {
            source_id: message.conversation.contact_inbox.source_id
          },
          last_activity_at: message.conversation.last_activity_at.to_i,
          unread_count: message.conversation.unread_incoming_messages.count
        },
        sentiment: {},
        sender: message.sender.push_event_data,
        echo_id: 'random-echo_id'
      }
    end

    it 'returns push event payload' do
      expect(push_event_data).to eq(expected_data)
    end
  end

  describe 'message create event' do
    let!(:conversation) { create(:conversation) }

    before do
      conversation.reload
    end

    it 'updates the conversation first reply created at if it is the first outgoing message' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      outgoing_message = create(:message, message_type: :outgoing, conversation: conversation)

      expect(conversation.first_reply_created_at).to eq outgoing_message.created_at
      expect(conversation.waiting_since).to be_nil
    end

    it 'does not update the conversation first reply created at if the message is incoming' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :incoming, conversation: conversation)

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at
    end

    it 'does not update the conversation first reply created at if the message is template' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :template, conversation: conversation)

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at
    end

    it 'does not update the conversation first reply created at if the message is activity' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :activity, conversation: conversation)

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at
    end

    it 'does not update the conversation first reply created at if the message is a private message' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :outgoing, conversation: conversation, private: true)

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      next_message = create(:message, message_type: :outgoing, conversation: conversation)
      expect(conversation.first_reply_created_at).to eq next_message.created_at
      expect(conversation.waiting_since).to be_nil
    end

    it 'does not update first reply if the message is sent as campaign' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :outgoing, conversation: conversation, additional_attributes: { campaign_id: 1 })

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at
    end

    it 'does not update first reply if the message is sent by automation' do
      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at

      create(:message, message_type: :outgoing, conversation: conversation, content_attributes: { automation_rule_id: 1 })

      expect(conversation.first_reply_created_at).to be_nil
      expect(conversation.waiting_since).to eq conversation.created_at
    end
  end

  describe '#reopen_conversation' do
    let(:conversation) { create(:conversation) }
    let(:message) { build(:message, message_type: :incoming, conversation: conversation) }

    it 'reopens resolved conversation when the message is from a contact' do
      conversation.resolved!
      message.save!
      expect(message.conversation.open?).to be true
    end

    it 'reopens snoozed conversation when the message is from a contact' do
      conversation.snoozed!
      message.save!
      expect(message.conversation.open?).to be true
    end

    it 'will not reopen if the conversation is muted' do
      conversation.resolved!
      conversation.mute!
      message.save!
      expect(message.conversation.open?).to be false
    end

    it 'will mark the conversation as pending if the agent bot is active' do
      agent_bot = create(:agent_bot)
      inbox = conversation.inbox
      inbox.agent_bot = agent_bot
      inbox.save!
      conversation.resolved!
      message.save!
      expect(conversation.open?).to be false
      expect(conversation.pending?).to be true
    end
  end

  describe '#waiting since' do
    let(:conversation) { create(:conversation) }
    let(:agent) { create(:user, account: conversation.account) }
    let(:message) { build(:message, conversation: conversation) }

    it 'resets the waiting_since if an agent sent a reply' do
      message.message_type = :outgoing
      message.sender = agent
      message.save!

      expect(conversation.waiting_since).to be_nil
    end

    it 'sets the waiting_since if there is an incoming message' do
      conversation.update(waiting_since: nil)
      message.message_type = :incoming
      message.save!

      expect(conversation.waiting_since).not_to be_nil
    end

    it 'does not overwrite the previous value if there are newer messages' do
      old_waiting_since = conversation.waiting_since
      message.message_type = :incoming
      message.save!
      conversation.reload

      expect(conversation.waiting_since).to eq old_waiting_since
    end
  end

  context 'with webhook_data' do
    it 'contains the message attachment when attachment is present' do
      message = create(:message)
      attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
      attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
      attachment.save!
      expect(message.webhook_data.key?(:attachments)).to be true
    end

    it 'does not contain the message attachment when attachment is not present' do
      message = create(:message)
      expect(message.webhook_data.key?(:attachments)).to be false
    end

    it 'uses outgoing_content for webhook content' do
      message = create(:message, content: 'Test content')
      expect(message).to receive(:outgoing_content).and_return('Outgoing test content')

      webhook_data = message.webhook_data
      expect(webhook_data[:content]).to eq('Outgoing test content')
    end

    it 'includes CSAT survey link in webhook content for input_csat messages' do
      inbox = create(:inbox, channel: create(:channel_api))
      conversation = create(:conversation, inbox: inbox)
      message = create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience')

      expect(message.outgoing_content).to include('survey/responses/')
      expect(message.webhook_data[:content]).to include('survey/responses/')
    end
  end

  context 'when message is created' do
    let(:message) { build(:message, account: create(:account)) }

    it 'updates conversation last_activity_at when created' do
      message.save!
      expect(message.created_at).to eq message.conversation.last_activity_at
    end

    it 'updates contact last_activity_at when created' do
      expect { message.save! }.to(change { message.sender.last_activity_at })
    end

    it 'triggers ::MessageTemplates::HookExecutionService' do
      hook_execution_service = double
      allow(MessageTemplates::HookExecutionService).to receive(:new).and_return(hook_execution_service)
      allow(hook_execution_service).to receive(:perform).and_return(true)

      message.save!

      expect(MessageTemplates::HookExecutionService).to have_received(:new).with(message: message)
      expect(hook_execution_service).to have_received(:perform)
    end

    context 'with conversation continuity' do
      let(:inbox_with_continuity) do
        create(:inbox, account: message.account,
                       channel: build(:channel_widget, account: message.account, continuity_via_email: true))
      end

      it 'schedules email notification for outgoing messages in website channel' do
        message.inbox = inbox_with_continuity
        message.conversation.update!(inbox: inbox_with_continuity)
        message.conversation.contact.update!(email: 'test@example.com')
        message.message_type = 'outgoing'

        # Perform jobs inline to test full integration
        perform_enqueued_jobs do
          message.save!
        end

        # Verify the email worker is eventually scheduled through the service
        jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
        expect(jobs_for_conversation_count).to eq(1)
      end

      it 'does not schedule email for website channel if continuity is disabled' do
        inbox_without_continuity = create(:inbox, account: message.account,
                                                  channel: build(:channel_widget, account: message.account, continuity_via_email: false))
        message.inbox = inbox_without_continuity
        message.conversation.update!(inbox: inbox_without_continuity)
        message.conversation.contact.update!(email: 'test@example.com')
        message.message_type = 'outgoing'

        initial_job_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }

        perform_enqueued_jobs do
          message.save!
        end

        # No new jobs should be scheduled for this conversation
        jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
        expect(jobs_for_conversation_count).to eq(initial_job_count)
      end

      it 'does not schedule email for private notes' do
        message.inbox = inbox_with_continuity
        message.conversation.update!(inbox: inbox_with_continuity)
        message.conversation.contact.update!(email: 'test@example.com')
        message.private = true
        message.message_type = 'outgoing'

        initial_job_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }

        perform_enqueued_jobs do
          message.save!
        end

        # No new jobs should be scheduled for this conversation
        jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
        expect(jobs_for_conversation_count).to eq(initial_job_count)
      end

      it 'calls SendReplyJob for all channels' do
        allow(SendReplyJob).to receive(:perform_later).and_return(true)
        message.message_type = 'outgoing'
        message.save!
        expect(SendReplyJob).to have_received(:perform_later).with(message.id)
      end
    end
  end

  context 'when content_type is blank' do
    let(:message) { build(:message, content_type: nil, account: create(:account)) }

    it 'sets content_type as text' do
      message.save!
      expect(message.content_type).to eq 'text'
    end
  end

  context 'when processed_message_content is blank' do
    let(:message) { build(:message, content_type: :text, account: create(:account), content: 'Processed message content') }

    it 'sets content_type as text' do
      message.save!
      expect(message.processed_message_content).to eq message.content
    end
  end

  context 'when attachments size maximum' do
    let(:message) { build(:message, content_type: nil, account: create(:account)) }

    it 'add errors to message for attachment size is more than allowed limit' do
      16.times.each do
        attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
        attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
      end

      expect(message.errors.messages).to eq({ attachments: ['exceeded maximum allowed'] })
    end
  end

  context 'when email notifiable message' do
    let(:message) { build(:message, content_type: nil, account: create(:account)) }

    it 'return false if private message' do
      message.private = true
      message.message_type = 'outgoing'
      expect(message.email_notifiable_message?).to be false
    end

    it 'return false if incoming message' do
      message.private = false
      message.message_type = 'incoming'
      expect(message.email_notifiable_message?).to be false
    end

    it 'return false if activity message' do
      message.private = false
      message.message_type = 'activity'
      expect(message.email_notifiable_message?).to be false
    end

    it 'return false if message type is template and content type is not input_csat or text' do
      message.private = false
      message.message_type = 'template'
      message.content_type = 'incoming_email'
      expect(message.email_notifiable_message?).to be false
    end

    it 'return true if not private and not incoming and message content type is input_csat or text' do
      message.private = false
      message.message_type = 'template'
      message.content_type = 'text'
      expect(message.email_notifiable_message?).to be true
    end
  end

  context 'when facebook channel with unavailable story link' do
    let(:instagram_message) { create(:message, :instagram_story_mention) }

    before do
      # stubbing the request to facebook api during the message creation
      stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
        story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
        from: { username: 'Sender-id-1', id: 'Sender-id-1' },
        id: 'instagram-message-id-1234'
      }.to_json, headers: {})
    end

    it 'keeps the attachment for deleted stories' do
      expect(instagram_message.attachments.count).to eq 1
      stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 404)
      instagram_message.push_event_data
      expect(instagram_message.reload.attachments.count).to eq 1
    end

    it 'keeps the attachment for expired stories' do
      expect(instagram_message.attachments.count).to eq 1
      # for expired stories, the link will be empty
      stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
        story: { mention: { link: '', id: '17920786367196703' } }
      }.to_json, headers: {})
      instagram_message.push_event_data
      expect(instagram_message.reload.attachments.count).to eq 1
    end
  end

  describe '#ensure_in_reply_to' do
    let(:conversation) { create(:conversation) }
    let(:message) { create(:message, conversation: conversation, source_id: 12_345) }

    context 'when in_reply_to is present' do
      let(:content_attributes) { { in_reply_to: message.id } }
      let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }

      it 'sets in_reply_to_external_id based on the source_id of the referenced message' do
        new_message.send(:ensure_in_reply_to)
        expect(new_message.content_attributes[:in_reply_to_external_id]).to eq(message.source_id)
      end
    end

    context 'when in_reply_to is not present' do
      let(:content_attributes) { { in_reply_to_external_id: message.source_id } }
      let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }

      it 'sets in_reply_to based on the source_id of the referenced message' do
        new_message.send(:ensure_in_reply_to)
        expect(new_message.content_attributes[:in_reply_to]).to eq(message.id)
      end
    end

    context 'when the referenced message is not found' do
      let(:content_attributes) { { in_reply_to: message.id + 1 } }
      let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }

      it 'does not set in_reply_to_external_id' do
        new_message.send(:ensure_in_reply_to)
        expect(new_message.content_attributes[:in_reply_to_external_id]).to be_nil
      end
    end

    context 'when the source message is not found' do
      let(:content_attributes) { { in_reply_to_external_id: 'source-id-that-does-not-exist' } }
      let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }

      it 'does not set in_reply_to' do
        new_message.send(:ensure_in_reply_to)
        expect(new_message.content_attributes[:in_reply_to]).to be_nil
      end
    end
  end

  describe '#content' do
    let(:conversation) { create(:conversation) }

    context 'when message is not input_csat' do
      let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }

      it 'returns original content' do
        expect(message.content).to eq('Regular message')
      end
    end

    context 'when message is input_csat' do
      let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') }

      context 'when inbox is web widget' do
        before do
          allow(message.inbox).to receive(:web_widget?).and_return(true)
        end

        it 'returns original content without survey URL' do
          expect(message.content).to eq('Rate your experience')
        end
      end

      context 'when inbox is not web widget' do
        before do
          allow(message.inbox).to receive(:web_widget?).and_return(false)
        end

        it 'returns only the stored content (clean for dashboard)' do
          expect(message.content).to eq('Rate your experience')
        end

        it 'returns only the base content without URL when survey_url stored separately' do
          message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' }
          expect(message.content).to eq('Rate your experience')
        end
      end
    end
  end

  describe '#outgoing_content' do
    let(:conversation) { create(:conversation) }
    let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }

    it 'delegates to MessageContentPresenter' do
      presenter = instance_double(MessageContentPresenter)
      allow(MessageContentPresenter).to receive(:new).with(message).and_return(presenter)
      allow(presenter).to receive(:outgoing_content).and_return('Presented content')

      expect(message.outgoing_content).to eq('Presented content')
      expect(MessageContentPresenter).to have_received(:new).with(message)
      expect(presenter).to have_received(:outgoing_content)
    end
  end

  describe '#auto_reply_email?' do
    context 'when message is not an incoming email and inbox is not email' do
      let(:conversation) { create(:conversation) }
      let(:message) { create(:message, conversation: conversation, message_type: :outgoing) }

      it 'returns false' do
        expect(message.auto_reply_email?).to be false
      end
    end

    context 'when message is an incoming email' do
      let(:email_channel) { create(:channel_email) }
      let(:email_inbox) { create(:inbox, channel: email_channel) }
      let(:conversation) { create(:conversation, inbox: email_inbox) }

      it 'returns false when auto_reply is not set to true' do
        message = create(
          :message,
          conversation: conversation,
          message_type: :incoming,
          content_type: 'incoming_email',
          content_attributes: {}
        )
        expect(message.auto_reply_email?).to be false
      end

      it 'returns true when auto_reply is set to true' do
        message = create(
          :message,
          conversation: conversation,
          message_type: :incoming,
          content_type: 'incoming_email',
          content_attributes: { email: { auto_reply: true } }
        )
        expect(message.auto_reply_email?).to be true
      end
    end

    context 'when inbox is email' do
      let(:email_channel) { create(:channel_email) }
      let(:email_inbox) { create(:inbox, channel: email_channel) }
      let(:conversation) { create(:conversation, inbox: email_inbox) }

      it 'returns false when auto_reply is not set to true' do
        message = create(
          :message,
          conversation: conversation,
          message_type: :outgoing,
          content_attributes: {}
        )
        expect(message.auto_reply_email?).to be false
      end

      it 'returns true when auto_reply is set to true' do
        message = create(
          :message,
          conversation: conversation,
          message_type: :outgoing,
          content_attributes: { email: { auto_reply: true } }
        )
        expect(message.auto_reply_email?).to be true
      end
    end
  end

  describe '#should_index?' do
    let(:account) { create(:account) }
    let(:conversation) { create(:conversation, account: account) }
    let(:message) { create(:message, conversation: conversation, account: account) }

    before do
      allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(true)
      account.enable_features('advanced_search_indexing')
    end

    context 'when advanced search is not allowed globally' do
      before do
        allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(false)
      end

      it 'returns false' do
        expect(message.should_index?).to be false
      end
    end

    context 'when advanced search feature is not enabled for account on chatwoot cloud' do
      before do
        allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
        account.disable_features('advanced_search_indexing')
      end

      it 'returns false' do
        expect(message.should_index?).to be false
      end
    end

    context 'when advanced search feature is not enabled for account on self-hosted' do
      before do
        allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
        account.disable_features('advanced_search_indexing')
      end

      it 'returns true' do
        expect(message.should_index?).to be true
      end
    end

    context 'when message type is not incoming or outgoing' do
      before do
        message.message_type = 'activity'
      end

      it 'returns false' do
        expect(message.should_index?).to be false
      end
    end

    context 'when all conditions are met' do
      it 'returns true for incoming message' do
        message.message_type = 'incoming'
        expect(message.should_index?).to be true
      end

      it 'returns true for outgoing message' do
        message.message_type = 'outgoing'
        expect(message.should_index?).to be true
      end
    end
  end

  describe '#reindex_for_search callback' do
    let(:account) { create(:account) }
    let(:conversation) { create(:conversation, account: account) }

    before do
      allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(true)
      account.enable_features('advanced_search_indexing')
    end

    context 'when message should be indexed' do
      it 'calls reindex_for_search for incoming message on create' do
        message = build(:message, conversation: conversation, account: account, message_type: :incoming)
        expect(message).to receive(:reindex_for_search)
        message.save!
      end

      it 'calls reindex_for_search for outgoing message on update' do
        # rubocop:disable RSpec/AnyInstance
        allow_any_instance_of(described_class).to receive(:reindex_for_search).and_return(true)
        # rubocop:enable RSpec/AnyInstance
        message = create(:message, conversation: conversation, account: account, message_type: :outgoing)
        expect(message).to receive(:reindex_for_search).and_return(true)
        message.update!(content: 'Updated content')
      end
    end

    context 'when message should not be indexed' do
      it 'does not call reindex_for_search for activity message' do
        message = build(:message, conversation: conversation, account: account, message_type: :activity)
        expect(message).not_to receive(:reindex_for_search)
        message.save!
      end

      it 'does not call reindex_for_search for unpaid account on cloud' do
        allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
        account.disable_features('advanced_search_indexing')
        message = build(:message, conversation: conversation, account: account, message_type: :incoming)
        expect(message).not_to receive(:reindex_for_search)
        message.save!
      end

      it 'does not call reindex_for_search when advanced search is not allowed' do
        allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(false)
        message = build(:message, conversation: conversation, account: account, message_type: :incoming)
        expect(message).not_to receive(:reindex_for_search)
        message.save!
      end
    end
  end
end
