# frozen_string_literal: true

#
# Copyright (C) 2016 - 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 UserListV2 do
  before do
    @account = Account.default
    @account.settings = { open_registration: true }
    @account.save!
  end

  it "complains about invalid input" do
    ul = UserListV2.new "i\x01nstructure", search_type: "unique_id"
    expect(ul.errors).to eq [{ address: "i\x01nstructure", details: :unparseable }]
  end

  it "responds responsibly to incorrect search type" do
    expect { UserListV2.new "instructure", search_type: "tarnation_declaration" }
      .to raise_error(UserListV2::ParameterError)
  end

  it "does not fail with unicode names" do
    ul = UserListV2.new '"senor molé" <blah@instructure.com>', search_type: "unique_id"
    expect(ul.missing_results.map { |x| [x[:user_name], x[:address]] }).to eq [["senor molé", "blah@instructure.com"]]
  end

  it "finds by SMS number" do
    user_with_pseudonym(name: "JT", active_all: 1)
    communication_channel(@user, { username: "8015555555@txt.att.net", path_type: "sms", active_cc: true })
    ul = UserListV2.new("(801) 555-5555", search_type: "cc_path")
    expect(ul.resolved_results.first[:address]).to eq "(801) 555-5555"
    expect(ul.resolved_results.first[:user_id]).to eq @user.id
    expect(ul.resolved_results.first[:user_token]).to eq @user.token

    ul = UserListV2.new("8015555555", search_type: "cc_path")
    expect(ul.resolved_results.first[:address]).to eq "8015555555"
    expect(ul.resolved_results.first[:user_id]).to eq @user.id
    expect(ul.resolved_results.first[:user_token]).to eq @user.token
  end

  it "finds duplicates by SMS number" do
    user_with_pseudonym(name: "JT", active_all: 1)
    @user1 = @user
    communication_channel(@user, { username: "8015555555@txt.att.net", path_type: "sms", active_cc: true })

    user_with_pseudonym(name: "JT2", active_all: 1)
    communication_channel(@user, { username: "8015555555@txt.fakeplace.net", path_type: "sms", active_cc: true })

    ul = UserListV2.new("(801) 555-5555", search_type: "cc_path")
    expect(ul.resolved_results).to be_empty
    expect(ul.duplicate_results.first.pluck(:user_id)).to match_array([@user1.id, @user.id])
    expect(ul.duplicate_results.first.pluck(:user_token)).to match_array([@user1.token, @user.token])
  end

  it "ignores unconfirmed emails" do
    # maaaybe we want to preserve the old behavior with this... but whatevr  ¯\_(ツ)_/¯
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true)
    communication_channel(@user, { username: "jt+2@instructure.com", active_cc: true })
    @user1 = @user
    user_with_pseudonym(name: "JT 1", username: "jt+1@instructure.com", active_all: true)
    communication_channel(@user, { username: "jt+2@instructure.com" })
    ul = UserListV2.new("jt+2@instructure.com", search_type: "cc_path")
    expect(ul.resolved_results.length).to eq 1
    expect(ul.resolved_results.first[:user_id]).to eq @user1.id
    expect(ul.duplicate_results).to be_empty
  end

  it "includes in duplicates if there is 1 active CC and 1 unconfirmed" do
    Account.default.enable_feature!(:allow_unconfirmed_users_in_user_list)
    # maaaybe we want to preserve the old behavior with this... but whatevr  ¯\_(ツ)_/¯
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true)
    communication_channel(@user, { username: "jt+2@instructure.com", active_cc: true })
    @user1 = @user
    user_with_pseudonym(name: "JT 1", username: "jt+1@instructure.com", active_all: true)
    communication_channel(@user, { username: "jt+2@instructure.com" })
    ul = UserListV2.new("jt+2@instructure.com", search_type: "cc_path")
    expect(ul.resolved_results).to be_empty
    expect(ul.duplicate_results.count).to eq 1
    expect(ul.duplicate_results.first.pluck(:user_id)).to match_array([@user1.id, @user.id])
    expect(ul.duplicate_results.first.pluck(:user_token)).to match_array([@user1.token, @user.token])
  end

  it "does not find users from untrusted accounts" do
    account = Account.create!
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account:)
    ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
    expect(ul.resolved_results).to be_empty
    expect(ul.missing_results.first[:address]).to eq "jt@instructure.com"
  end

  it "doesn't find site admins if you're not a site admin" do
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: Account.site_admin)
    allow(Account.default).to receive(:trusted_account_ids).and_return([Account.site_admin.id])
    jt = @user
    user_with_pseudonym
    other = @user

    ul = UserListV2.new("jt@instructure.com", current_user: other, search_type: "unique_id")
    expect(ul.resolved_results).to be_empty
    expect(ul.missing_results.first[:address]).to eq "jt@instructure.com"

    # when it's the user _from_ site admin doing it, it can be found
    allow(Account.default).to receive(:trusted_account_ids).and_return([Account.site_admin.id])
    ul = UserListV2.new("jt@instructure.com", current_user: jt, search_type: "unique_id")
    expect(ul.missing_results).to be_empty
    expect(ul.resolved_results.first[:address]).to eq "jt@instructure.com"
  end

  it "finds users from trusted accounts" do
    account = Account.create!(name: "naem")
    allow(Account.default).to receive(:trusted_account_ids).and_return([account.id])
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account:)
    ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
    expect(ul.resolved_results).to eq [{ address: "jt@instructure.com", user_id: @user.id, user_token: @user.token, user_name: "JT", account_id: account.id, account_name: account.name }]
  end

  it "shows duplicates for two results from the current account and the trusted account" do
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true)
    @user1 = @user
    account = Account.create!
    allow(Account.default).to receive(:trusted_account_ids).and_return([account.id])
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account:)
    ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")

    expect(ul.resolved_results).to be_empty
    expect(ul.duplicate_results.count).to eq 1
    expect(ul.duplicate_results.first.pluck(:user_id)).to match_array([@user1.id, @user.id])
    expect(ul.duplicate_results.first.pluck(:user_token)).to match_array([@user1.token, @user.token])
  end

  context "when searching by sis id" do
    it "raises an error without can_read_sis" do
      expect do
        UserListV2.new("SISID", root_account: @account, search_type: "sis_user_id", can_read_sis: false)
      end.to raise_error("cannot read sis ids")
    end

    it "shows duplicates for two results from the current account and the trusted account" do
      account1 = Account.create!
      account2 = Account.create!
      allow(account1).to receive_messages(trusted_account_ids: [account2.id], trust_exists?: true)

      user_with_managed_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: account1, sis_user_id: "SISID")
      @user1 = @user
      user_with_managed_pseudonym(name: "JT", username: "jt2@instructure.com", active_all: true, account: account2, sis_user_id: "SISID")
      ul = UserListV2.new("SISID", root_account: account1, search_type: "sis_user_id", can_read_sis: true)

      expect(ul.resolved_results).to be_empty
      expect(ul.duplicate_results.count).to eq 1
      dup = ul.duplicate_results.first
      expect(dup.pluck(:user_id)).to match_array([@user1.id, @user.id])
      expect(dup.pluck(:user_token)).to match_array([@user1.token, @user.token])

      # should include additional idenfitying info on duplicates
      expect(dup.pluck(:email)).to match_array(["jt@instructure.com", "jt2@instructure.com"])
      expect(dup.pluck(:login_id)).to match_array(["jt@instructure.com", "jt2@instructure.com"])
      expect(dup.pluck(:sis_user_id)).to match_array(["SISID", "SISID"])
    end
  end

  it "shows duplicates if there is a conflict of unique_ids from not-this-account" do
    account1 = Account.create!
    account2 = Account.create!
    allow(Account.default).to receive(:trusted_account_ids).and_return([account1.id, account2.id])

    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: account1)
    allow_any_instantiation_of(@pseudonym).to receive(:works_for_account?).and_return(true)
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: account2)
    allow_any_instantiation_of(@pseudonym).to receive(:works_for_account?).and_return(true)
    ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
    expect(ul.resolved_results).to be_empty
    expect(ul.duplicate_results.count).to eq 1
    dups = ul.duplicate_results.first
    expect(dups.pluck(:account_id)).to match_array([account1.id, account2.id])
    expect(dups.pluck(:login_id)).to match_array(["jt@instructure.com", "jt@instructure.com"])
    dups.each do |h|
      expect(h).to_not have_key(:sis_user_id) # only includes if can_read_sis is true
    end
  end

  it "finds a user with multiple not-this-account pseudonyms" do
    account1 = Account.create!
    account2 = Account.create!
    allow(Account.default).to receive(:trusted_account_ids).and_return([account1.id, account2.id])
    user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: account1)
    @user.pseudonyms.create!(unique_id: "jt@instructure.com", account: account2)
    ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
    expect(ul.duplicate_results).to be_empty
    expect(ul.resolved_results.count).to eq 1
    expect(ul.resolved_results.first[:user_id]).to eq @user.id
    expect(ul.resolved_results.first[:user_token]).to eq @user.token
  end

  it "does not find a user from a different account by SMS" do
    account = Account.create!
    user_with_pseudonym(name: "JT", active_all: 1, account:)
    communication_channel(@user, { username: "8015555555@txt.att.net", path_type: "sms", active_cc: true })
    ul = UserListV2.new("(801) 555-5555", search_type: "cc_path")
    expect(ul.resolved_results).to eq []
    expect(ul.missing_results.first[:address]).to eq "(801) 555-5555"
  end

  context "sharding" do
    specs_require_sharding

    it "finds a user from a trusted account in a different shard" do
      @shard1.activate do
        @account = Account.create!(name: "accountnaem")
        user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: @account)
      end
      allow(Account.default).to receive(:trusted_account_ids).and_return([@account.id])
      ul1 = UserListV2.new("jt@instructure.com", search_type: "sis_user_id", can_read_sis: true)
      expect(ul1.missing_results.pluck(:address)).to eq ["jt@instructure.com"]

      ul2 = UserListV2.new("jt@instructure.com", search_type: "unique_id")
      expect(ul2.resolved_results).to eq [{ address: "jt@instructure.com", user_id: @user.id, user_token: @user.token, account_id: @account.id, user_name: "JT", account_name: @account.name }]
    end

    it "does not get confused when dealing with cross-shard duplicate results that actually point to the same user" do
      user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true)
      @shard1.activate do
        @account = Account.create!(name: "accountnaem")
        ps = @account.pseudonyms.build(user: @user, unique_id: "username", password: "password", password_confirmation: "password")
        ps.save_without_session_maintenance
        @user.communication_channels.first.update!(pseudonym: ps)
      end

      allow(Account.default).to receive(:trusted_account_ids).and_return([@account.id])

      ul = UserListV2.new("jt@instructure.com", search_type: "cc_path")
      expect(ul.resolved_results.count).to eq 1
      r = ul.resolved_results.first
      expect(r[:user_id]).to eq @user.id
      expect(r[:user_token]).to eq @user.token
      expect(r[:account_id]).to eq Account.default.id
    end

    it "finds a user who has pseudonyms in multiple trusted accounts but not the one being searched" do
      user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true)

      @shard1.activate do
        @account1 = Account.create!(name: "account1", consortium_parent_account: Account.default)
        ps = @account1.pseudonyms.build(user: @user, unique_id: "jt@instructure.com")
        ps.save_without_session_maintenance
      end

      @shard2.activate do
        @account2 = Account.create!(name: "account2", consortium_parent_account: Account.default)
        @account2.trust_links.create!(managing_account: @account1)
        @account2.trust_links.create!(managing_account: Account.default)
      end

      ul = UserListV2.new("jt@instructure.com", search_type: "unique_id", root_account: @account2)
      expect(ul.resolved_results.pluck(:user_id)).to eq [@user.id]
    end

    it "finds a user whose home shard is not the target shard" do
      @shard1.activate do
        @account = Account.create!(name: "non-local")
        user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: @account)
        @pseudonym.destroy
      end
      Account.default.pseudonyms.create!(user: @user, unique_id: "bob")

      # strictly speaking we don't want this to be necessary,
      # but I'm not ready to rely on globallookups exclusively
      # for finding appropriate shards
      allow(Account.default).to receive(:trusted_account_ids).and_return([@account.id])

      ul = UserListV2.new("jt@instructure.com", search_type: "cc_path")
      expect(ul.resolved_results.count).to eq 1
      r = ul.resolved_results.first
      expect(r[:user_id]).to eq @user.id
      expect(r[:user_token]).to eq @user.token
      expect(r[:account_id]).to eq Account.default.id
    end

    context "global lookups" do
      before do
        @shard1.activate do
          @account1 = Account.create!
          @user1 = user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: @account1)
        end
        @shard2.activate do
          @account2 = Account.create!
          @user2 = user_with_pseudonym(name: "JT", username: "jt@instructure.com", active_all: true, account: @account2)
        end

        allow(Account.default).to receive(:trusted_account_ids).and_return([Account.site_admin.id, @account1.id, @account2.id])
        allow(GlobalLookups).to receive(:enabled?).and_return(true)
      end

      it "looks on every shard if there aren't that many shards to look on" do
        # identifiable as `undefined method `associated_shards_for_column' for class `#<Class:0x00007f87ade80828>'`
        skip_if_prepended_class_method_stubs_broken
        Setting.set("global_lookups_shard_threshold", "3") # i.e. if we'd have to look on more than 3 shards, we should use global lookups

        expect(Pseudonym).not_to receive(:associated_shards_for_column)
        ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
        expect(ul.resolved_results).to be_empty
        expect(ul.duplicate_results.first.pluck(:user_id)).to match_array([@user1.id, @user2.id])
        expect(ul.duplicate_results.first.pluck(:user_token)).to match_array([@user1.token, @user2.token])
      end

      it "uses the global lookups to restrict searched shard if there are enough shards to look on" do
        skip_if_prepended_class_method_stubs_broken
        Setting.set("global_lookups_shard_threshold", "1") # i.e. if we'd have to look on more than 1 shards, we should use global lookups

        expect(Pseudonym).to receive(:associated_shards_for_column).once.with(:unique_id, "jt@instructure.com").and_return([@shard1]) # don't look on shard2
        ul = UserListV2.new("jt@instructure.com", search_type: "unique_id")
        expect(ul.duplicate_results).to be_empty
        expect(ul.resolved_results.first[:user_id]).to eq @user1.id
        expect(ul.resolved_results.first[:user_token]).to eq @user1.token
      end
    end
  end
end
