# frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

describe DelayedMessage do
  it "creates a new instance given valid attributes" do
    delayed_message_model
  end

  context "named scopes" do
    before :once do
      DelayedMessage.delete_all
    end

    it "has scope for :daily" do
      delayed_message_model(frequency: "daily")
      expect(DelayedMessage.for(:daily)).to eq [@delayed_message]
    end

    it "scopes for :weekly" do
      delayed_message_model(frequency: "weekly")
      expect(DelayedMessage.for(:weekly)).to eq [@delayed_message]
    end

    it "scopes for notification" do
      notification_model
      delayed_message_model
      expect(DelayedMessage.for(@notification)).to eq [@delayed_message]
    end

    it "scopes for notification_policy" do
      notification_policy_model
      delayed_message_model(notification_policy_id: @notification_policy.id)
      expect(@notification_policy).to be_is_a(NotificationPolicy)
      expect(DelayedMessage.for(@notification_policy)).to eq [@delayed_message]
    end

    it "scopes for communication_channel" do
      communication_channel_model
      delayed_message_model(communication_channel_id: @communication_channel.id)
      expect(@communication_channel).to be_is_a(CommunicationChannel)
      expect(DelayedMessage.for(@communication_channel)).to eq [@delayed_message]
    end

    it "scopes for context" do
      delayed_message_model
      @delayed_message.context = assignment_model
      @delayed_message.save!
      expect(DelayedMessage.for(@assignment)).to eq [@delayed_message]
    end

    it "has a scope to filter by the state" do
      notification_model name: "New Stuff"
      delayed_message_model(workflow_state: "pending")
      delayed_message_model(workflow_state: "cancelled")
      delayed_message_model(workflow_state: "sent")
      expect(DelayedMessage.in_state(:pending).all? { |d| d.state == :pending }).to be_truthy
      expect(DelayedMessage.in_state(:pending).size).to be(1)
      expect(DelayedMessage.in_state(:cancelled).all? { |d| d.state == :cancelled }).to be_truthy
      expect(DelayedMessage.in_state(:cancelled).size).to be(1)
      expect(DelayedMessage.in_state(:sent).all? { |d| d.state == :sent }).to be_truthy
      expect(DelayedMessage.in_state(:sent).size).to be(1)
    end
  end

  context "workflow" do
    before :once do
      delayed_message_model
    end

    it "starts the workflow with pending" do
      expect(@delayed_message.state).to be(:pending)
    end

    it "shoulds be able to go to cancelled from pending" do
      @delayed_message.cancel
      expect(@delayed_message.state).to be(:cancelled)
    end

    it "is able to be sent from pending" do
      @delayed_message.begin_send
      expect(@delayed_message.state).to be(:sent)
    end
  end

  it "uses the user's main account domain for links" do
    Canvas::MessageHelper.create_notification(name: "Summaries", category: "Summaries")
    account = Account.create!(name: "new acct")
    user = user_with_pseudonym(account:)
    expect(user.pseudonym.account).to eq account
    expect(SisPseudonym).to receive(:for).with(user, Account.default, type: :implicit, require_sis: false).and_return(user.pseudonym)
    expect(HostUrl).to receive(:context_host).with(account, any_args).at_least(1).and_return("dm.dummy.test.host")
    allow(HostUrl).to receive(:default_host).and_return("test.host")
    user.communication_channel.confirm!
    dm = DelayedMessage.create!(summary: "This is a notification", context: Account.default, communication_channel: user.communication_channel, notification: notification_model)
    DelayedMessage.summarize([dm])
    message = Message.last
    expect(message.body.to_s).not_to match(%r{http://test.host/})
    expect(message.body.to_s).to match(%r{http://dm.dummy.test.host/})
  end

  it "returns nil if the delayed messages are using a retired communication channel" do
    Canvas::MessageHelper.create_notification(name: "Summaries", category: "Summaries")
    account = Account.create!(name: "new acct")
    user = user_with_pseudonym(account:)
    user.communication_channel.retire!
    dm = DelayedMessage.create!(summary: "This is a notification", context: Account.default, communication_channel: user.communication_channel, notification: notification_model)
    expect(DelayedMessage.summarize([dm])).to be_nil
  end

  it "uses the root account's locale if the user locale isn't set" do
    Canvas::MessageHelper.create_notification(name: "Summaries", category: "Summaries")
    account = Account.create!(default_locale: "es")
    delayed_message_model(root_account_id: account.id).save!
    expect(I18n).to receive(:with_locale).with("es").once
    DelayedMessage.summarize([@delayed_message])
  end

  it "uses the user's locale for the summary message" do
    Canvas::MessageHelper.create_notification(name: "Summaries", category: "Summaries")
    @user = User.create!(locale: "es")
    delayed_message_model.save!
    expect(I18n).to receive(:with_locale).with("es").once
    DelayedMessage.summarize([@delayed_message])
  end

  context "sharding" do
    specs_require_sharding

    it "creates messages on the user's shard" do
      Canvas::MessageHelper.create_notification(name: "Summaries", category: "Summaries")

      @shard1.activate do
        account = Account.create!(name: "new acct")
        user = user_with_pseudonym(account:)
        expect(user.pseudonym.account).to eq account
        expect(HostUrl).to receive(:context_host).with(user.pseudonym.account, any_args).at_least(1).and_return("dm.dummy.test.host")
        allow(HostUrl).to receive(:default_host).and_return("test.host")
        @cc = user.communication_channel
        @cc.confirm!
        @dm = DelayedMessage.create!(summary: "This is a notification", context: account, communication_channel: @cc, notification: notification_model)
      end
      @shard2.activate do
        DelayedMessage.summarize([@dm])
        expect(@cc.messages.last).not_to be_nil
        expect(@cc.messages.last.shard).to eq @shard1
      end
    end
  end

  describe "set_send_at" do
    before :once do
      # shouldn't be used, but to make sure it's not equal to any of the other
      # time zones in play
      Time.zone = "UTC"
      @true_now = Time.zone.now

      # time zones of interest
      @mountain = ActiveSupport::TimeZone.us_zones.find { |zone| zone.name == "Mountain Time (US & Canada)" }
      @central = ActiveSupport::TimeZone.us_zones.find { |zone| zone.name == "Central Time (US & Canada)" }
      @eastern = ActiveSupport::TimeZone.us_zones.find { |zone| zone.name == "Eastern Time (US & Canada)" }

      # set up user in central time (different than the specific time zones
      # referenced in set_send_at)
      @account = Account.create!(name: "new acct")
      @user = user_with_pseudonym(account: @account)
      @user.time_zone = @central.name
      @user.pseudonym.update_attribute(:account, @account)
      @user.save
    end

    before do
      # build the delayed message
      @dm = DelayedMessage.new(context: @account, communication_channel: @user.communication_channel)
    end

    it "does nothing if the CC isn't set yet" do
      @dm.communication_channel = nil
      @dm.send(:set_send_at)
      expect(@dm.send_at).to be_nil
    end

    it "does nothing if send_at is already set" do
      send_at = @true_now - 5.days
      @dm.send_at = send_at
      @dm.send(:set_send_at)
      expect(@dm.send_at).to eq send_at
    end

    it "sets to 6pm in the user's time zone for non-weekly messages" do
      Timecop.freeze(@central.now.change(hour: 12)) do
        @dm.frequency = "daily"
        @dm.send(:set_send_at)
        expect(@dm.send_at).to eq @central.now.change(hour: 18)
      end
    end

    it "sets to 6pm in the Mountain time zone for non-weekly messages when the user hasn't set a time zone" do
      @user.time_zone = nil
      @user.save

      Timecop.freeze(@mountain.now.change(hour: 12)) do
        @dm.frequency = "daily"
        @dm.send(:set_send_at)
        expect(@dm.send_at).to eq @mountain.now.change(hour: 18)
      end
    end

    it "sets to 6pm the next day for non-weekly messages created after 6pm" do
      Timecop.freeze(@central.now.change(hour: 20)) do
        @dm.frequency = "daily"
        @dm.send(:set_send_at)
        expect(@dm.send_at).to eq @central.now.tomorrow.change(hour: 18)
      end
    end

    it "sets to next saturday (Eastern-time) for weekly messages" do
      monday = @eastern.now.monday
      saturday = monday + 5.days
      sunday = saturday + 1.day

      Timecop.freeze(monday) do
        @dm.frequency = "weekly"
        @dm.send(:set_send_at)
        expect(@dm.send_at.in_time_zone(@eastern).midnight).to eq saturday
      end

      Timecop.freeze(sunday) do
        @dm.send_at = nil
        @dm.send(:set_send_at)
        expect(@dm.send_at.in_time_zone(@eastern).midnight).to eq saturday + 1.week
      end
    end

    it "sets to next saturday (Eastern-time) for weekly messages scheduled later saturday" do
      monday = @eastern.now.monday
      saturday = monday + 5.days

      Timecop.freeze(monday) do
        @dm.frequency = "weekly"
        @dm.send(:set_send_at)
      end

      Timecop.freeze(@dm.send_at + 30.minutes) do
        @dm.send_at = nil
        @dm.send(:set_send_at)
        expect(@dm.send_at.in_time_zone(@eastern).midnight).to eq saturday + 1.week
      end
    end

    it "uses the same time of day across weeks for weekly messages for the same user" do
      # anchor to January 1st to avoid DST; we're consigned to slightly weird
      # behavior around DST, but don't want it failing tests
      monday = @eastern.now.change(month: 1, day: 1).monday
      first = nil

      Timecop.freeze(monday) do
        @dm.frequency = "weekly"
        @dm.send(:set_send_at)
        first = @dm.send_at
      end

      Timecop.freeze(monday + 1.week) do
        @dm.send_at = nil
        @dm.send(:set_send_at)
        expect(@dm.send_at.in_time_zone(@eastern)).to eq first + 1.week
      end
    end

    it "spreads weekly messages for users in different accounts over the windows" do
      monday = @eastern.now.monday
      saturday = monday + 5.days

      Timecop.freeze(monday) do
        @dm.frequency = "weekly"

        expected_windows = []
        actual_windows = []

        DelayedMessage::WEEKLY_ACCOUNT_BUCKETS.times do |i|
          @dm.communication_channel.user.pseudonym.account_id = i
          @dm.send_at = nil
          @dm.send(:set_send_at)
          actual_windows << ((@dm.send_at - saturday).to_i / (60 * DelayedMessage::MINUTES_PER_WEEKLY_ACCOUNT_BUCKET))
          expected_windows << i
        end

        expect(actual_windows.sort).to eq expected_windows
      end
    end

    it "spreads weekly messages for different users in the same account over the same window" do
      monday = @eastern.now.monday
      saturday = monday + 5.days

      Timecop.freeze(monday) do
        @dm.frequency = "weekly"

        expected_diffs = []
        actual_diffs = []
        windows = []

        DelayedMessage::MINUTES_PER_WEEKLY_ACCOUNT_BUCKET.times do |i|
          @dm.communication_channel.user.id = i
          @dm.send_at = nil
          @dm.send(:set_send_at)
          window = (@dm.send_at - saturday).to_i / (60 * DelayedMessage::MINUTES_PER_WEEKLY_ACCOUNT_BUCKET)
          windows << window
          actual_diffs << (@dm.send_at - saturday - (DelayedMessage::MINUTES_PER_WEEKLY_ACCOUNT_BUCKET * window).minutes.to_i)
          expected_diffs << i.minutes
        end

        expect(actual_diffs.sort).to eq expected_diffs
        expect(windows.uniq.size).to eq 1
      end
    end
  end
end
