require 'spec_helper'

describe UsersController do

  before do
    UsersController.any_instance.stubs(:honeypot_value).returns(nil)
    UsersController.any_instance.stubs(:challenge_value).returns(nil)
  end

  describe '.show' do
    let!(:user) { log_in }

    it 'returns success' do
      xhr :get, :show, username: user.username
      response.should be_success
    end

    it "returns not found when the username doesn't exist" do
      xhr :get, :show, username: 'madeuppity'
      response.should_not be_success
    end

    it "raises an error on invalid access" do
      Guardian.any_instance.expects(:can_see?).with(user).returns(false)
      xhr :get, :show, username: user.username
      response.should be_forbidden
    end
  end

  describe '.user_preferences_redirect' do
    it 'requires the user to be logged in' do
      lambda { get :user_preferences_redirect }.should raise_error(Discourse::NotLoggedIn)
    end

    it "redirects to their profile when logged in" do
      user = log_in
      get :user_preferences_redirect
      response.should redirect_to("/users/#{user.username_lower}/preferences")
    end
  end

  describe '.authorize_email' do
    context 'invalid token' do
      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(nil)
        get :authorize_email, token: 'asdfasdf'
      end

      it 'return success' do
        response.should be_success
      end

      it 'sets a flash error' do
        flash[:error].should be_present
      end
    end

    context 'valid token' do
      let(:user) { Fabricate(:user) }

      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(user)
        get :authorize_email, token: 'asdfasdf'
      end

      it 'returns success' do
        response.should be_success
      end

      it "doesn't set an error" do
        flash[:error].should be_blank
      end

      it 'logs in as the user' do
        session[:current_user_id].should be_present
      end
    end
  end

  describe '.activate_account' do
    context 'invalid token' do
      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(nil)
        get :activate_account, token: 'asdfasdf'
      end

      it 'return success' do
        response.should be_success
      end

      it 'sets a flash error' do
        flash[:error].should be_present
      end
    end

    context 'valid token' do
      let(:user) { Fabricate(:user) }

      context 'welcome message' do
        before do
          EmailToken.expects(:confirm).with('asdfasdf').returns(user)
        end

        it 'enqueues a welcome message if the user object indicates so' do
          user.send_welcome_message = true
          user.expects(:enqueue_welcome_message).with('welcome_user')
          get :activate_account, token: 'asdfasdf'
        end

        it "doesn't enqueue the welcome message if the object returns false" do
          user.send_welcome_message = false
          user.expects(:enqueue_welcome_message).with('welcome_user').never
          get :activate_account, token: 'asdfasdf'
        end

      end

      context 'reponse' do
        before do
          Guardian.any_instance.expects(:can_access_forum?).returns(true)
          EmailToken.expects(:confirm).with('asdfasdf').returns(user)
          get :activate_account, token: 'asdfasdf'
        end

        it 'returns success' do
          response.should be_success
        end

        it "doesn't set an error" do
          flash[:error].should be_blank
        end

        it 'logs in as the user' do
          session[:current_user_id].should be_present
        end

        it "doesn't set @needs_approval" do
          assigns[:needs_approval].should be_blank
        end

      end

      context 'user is not approved' do
        before do
          Guardian.any_instance.expects(:can_access_forum?).returns(false)
          EmailToken.expects(:confirm).with('asdfasdf').returns(user)
          get :activate_account, token: 'asdfasdf'
        end

        it 'returns success' do
          response.should be_success
        end

        it 'sets @needs_approval' do
          assigns[:needs_approval].should be_present
        end

        it "doesn't set an error" do
          flash[:error].should be_blank
        end

        it "doesn't log the user in" do
          session[:current_user_id].should be_blank
        end
      end

    end
  end

  describe '.change_email' do
    let(:new_email) { 'bubblegum@adventuretime.ooo' }

    it "requires you to be logged in" do
      lambda { xhr :put, :change_email, username: 'asdf', email: new_email }.should raise_error(Discourse::NotLoggedIn)
    end

    context 'when logged in' do
      let!(:user) { log_in }

      it 'raises an error without an email parameter' do
        lambda { xhr :put, :change_email, username: user.username }.should raise_error(ActionController::ParameterMissing)
      end

      it "raises an error if you can't edit the user" do
        Guardian.any_instance.expects(:can_edit?).with(user).returns(false)
        xhr :put, :change_email, username: user.username, email: new_email
        response.should be_forbidden
      end

      context 'when the new email address is taken' do
        let!(:other_user) { Fabricate(:coding_horror) }
        it 'raises an error' do
          lambda { xhr :put, :change_email, username: user.username, email: other_user.email }.should raise_error(Discourse::InvalidParameters)
        end

        it 'raises an error if there is whitespace too' do
          lambda { xhr :put, :change_email, username: user.username, email: other_user.email + ' ' }.should raise_error(Discourse::InvalidParameters)
        end
      end

      context 'success' do

        it 'has an email token' do
          lambda { xhr :put, :change_email, username: user.username, email: new_email }.should change(EmailToken, :count)
        end

        it 'enqueues an email authorization' do
          Jobs.expects(:enqueue).with(:user_email, has_entries(type: :authorize_email, user_id: user.id, to_address: new_email))
          xhr :put, :change_email, username: user.username, email: new_email
        end
      end
    end

  end

  describe '.password_reset' do
    let(:user) { Fabricate(:user) }

    context "you can view it even if login is required" do
      before do
        SiteSetting.stubs(:login_required).returns(true)
        get :password_reset, token: 'asdfasdf'
      end

      it "returns success" do
        response.should be_success
      end
    end

    context 'invalid token' do
      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(nil)
        get :password_reset, token: 'asdfasdf'
      end

      it 'return success' do
        response.should be_success
      end

      it 'sets a flash error' do
        flash[:error].should be_present
      end

      it "doesn't log in the user" do
        session[:current_user_id].should be_blank
      end
    end

    context 'valid token' do
      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(user)
        get :password_reset, token: 'asdfasdf'
      end

      it 'returns success' do
        response.should be_success
      end

      it "doesn't set an error" do
        flash[:error].should be_blank
      end
    end

    context 'submit change' do
      before do
        EmailToken.expects(:confirm).with('asdfasdf').returns(user)
      end

      it "logs in the user" do
        put :password_reset, token: 'asdfasdf', password: 'newpassword'
        session[:current_user_id].should be_present
      end

      it "doesn't log in the user when not approved" do
        SiteSetting.expects(:must_approve_users?).returns(true)
        put :password_reset, token: 'asdfasdf', password: 'newpassword'
        session[:current_user_id].should be_blank
      end
    end


  end


  describe '.create' do
    before do
      @user = Fabricate.build(:user)
      @user.password = "strongpassword"
      DiscourseHub.stubs(:register_nickname).returns([true, nil])
    end

    def post_user
      xhr :post, :create,
        name: @user.name,
        username: @user.username,
        password: "strongpassword",
        email: @user.email
    end

    context 'when creating a non active user (unconfirmed email)' do
      it 'enqueues a signup email' do
        Jobs.expects(:enqueue).with(:user_email, has_entries(type: :signup))
        post_user
      end

      it 'does not enqueue a welcome email' do
        User.any_instance.expects(:enqueue_welcome_message).with('welcome_user').never
        post_user
      end

      it 'indicates the user is not active in the response' do
        post_user
        expect(JSON.parse(response.body)['active']).to be_false
      end

      context "and 'must approve users' site setting is enabled" do
        before { SiteSetting.expects(:must_approve_users).returns(true) }

        it 'does not enqueue an email' do
          Jobs.expects(:enqueue).never
          post_user
        end

        it 'does not login the user' do
          post_user
          expect(session[:current_user_id]).to be_blank
        end

        it 'indicates the user is not active in the response' do
          post_user
          expect(JSON.parse(response.body)['active']).to be_false
        end

        it "shows the 'waiting approval' message" do
          post_user
          expect(JSON.parse(response.body)['message']).to eq(
            I18n.t 'login.wait_approval'
          )
        end
      end
    end

    context 'when creating an active user (confirmed email)' do
      before { User.any_instance.stubs(:active?).returns(true) }

      it 'enqueues a welcome email' do
        User.any_instance.expects(:enqueue_welcome_message).with('welcome_user')
        post_user
      end

      it "shows the 'active' message" do
        User.any_instance.expects(:enqueue_welcome_message)
        post_user
        expect(JSON.parse(response.body)['message']).to eq(
          I18n.t 'login.active'
        )
      end

      it "should be logged in" do
        User.any_instance.expects(:enqueue_welcome_message)
        post_user
        session[:current_user_id].should be_present
      end

      it 'indicates the user is active in the response' do
        User.any_instance.expects(:enqueue_welcome_message)
        post_user
        expect(JSON.parse(response.body)['active']).to be_true
      end

      context 'authentication records for' do

        before do
          SiteSetting.expects(:must_approve_users).returns(true)
        end

        it 'should create twitter user info if none exists' do
          twitter_auth = { twitter_user_id: 42, twitter_screen_name: "bruce" }
          session[:authentication] = twitter_auth
          TwitterUserInfo.expects(:find_by_twitter_user_id).returns(nil)
          TwitterUserInfo.expects(:create)

          post_user
        end

        it 'should create facebook user info if none exists' do
          fb_auth = { facebook: { facebook_user_id: 42} }
          session[:authentication] = fb_auth
          FacebookUserInfo.expects(:find_by_facebook_user_id).returns(nil)
          FacebookUserInfo.expects(:create!)

          post_user
        end

        it 'should create github user info if none exists' do
          gh_auth = { github_user_id: 2, github_screen_name: "bruce" }
          session[:authentication] = gh_auth
          GithubUserInfo.expects(:find_by_github_user_id).returns(nil)
          GithubUserInfo.expects(:create)

          post_user
        end
      end
    end

    context 'after success' do
      before { post_user }

      it 'should succeed' do
        should respond_with(:success)
      end

      it 'has the proper JSON' do
        json = JSON::parse(response.body)
        json["success"].should be_true
      end

      it 'should not result in an active account' do
        User.where(username: @user.username).first.active.should be_false
      end
    end

    shared_examples 'honeypot fails' do
      it 'should not create a new user' do
        expect {
          xhr :post, :create, create_params
        }.to_not change { User.count }
      end

      it 'should not send an email' do
        User.any_instance.expects(:enqueue_welcome_message).never
        xhr :post, :create, create_params
      end

      it 'should say it was successful' do
        xhr :post, :create, create_params
        json = JSON::parse(response.body)
        json["success"].should be_true
      end
    end

    context 'when honeypot value is wrong' do
      before do
        UsersController.any_instance.stubs(:honeypot_value).returns('abc')
      end
      let(:create_params) { {name: @user.name, username: @user.username, password: "strongpassword", email: @user.email, password_confirmation: 'wrong'} }
      include_examples 'honeypot fails'
    end

    context 'when challenge answer is wrong' do
      before do
        UsersController.any_instance.stubs(:challenge_value).returns('abc')
      end
      let(:create_params) { {name: @user.name, username: @user.username, password: "strongpassword", email: @user.email, challenge: 'abc'} }
      include_examples 'honeypot fails'
    end

    context "when 'invite only' setting is enabled" do
      before { SiteSetting.expects(:invite_only?).returns(true) }

      let(:create_params) {{
        name: @user.name,
        username: @user.username,
        password: 'strongpassword',
        email: @user.email
      }}

      include_examples 'honeypot fails'
    end

    shared_examples 'failed signup' do
      it 'should not create a new User' do
        expect { xhr :post, :create, create_params }.to_not change { User.count }
      end

      it 'should report failed' do
        xhr :post, :create, create_params
        json = JSON::parse(response.body)
        json["success"].should_not be_true
      end
    end

    context 'when password is blank' do
      let(:create_params) { {name: @user.name, username: @user.username, password: "", email: @user.email} }
      include_examples 'failed signup'
    end

    context 'when password param is missing' do
      let(:create_params) { {name: @user.name, username: @user.username, email: @user.email} }
      include_examples 'failed signup'
    end

    context 'when an Exception is raised' do

      [ ActiveRecord::StatementInvalid,
        DiscourseHub::NicknameUnavailable,
        RestClient::Forbidden ].each do |exception|
        before { User.any_instance.stubs(:save).raises(exception) }

        let(:create_params) {
          { name: @user.name, username: @user.username,
            password: "strongpassword", email: @user.email}
        }

        include_examples 'failed signup'
      end
    end

  end

  context '.username' do
    it 'raises an error when not logged in' do
      lambda { xhr :put, :username, username: 'somename' }.should raise_error(Discourse::NotLoggedIn)
    end

    context 'while logged in' do
      let!(:user) { log_in }
      let(:new_username) { "#{user.username}1234" }

      it 'raises an error without a new_username param' do
        lambda { xhr :put, :username, username: user.username }.should raise_error(ActionController::ParameterMissing)
      end

      it 'raises an error when you don\'t have permission to change the user' do
        Guardian.any_instance.expects(:can_edit?).with(user).returns(false)
        xhr :put, :username, username: user.username, new_username: new_username
        response.should be_forbidden
      end

      it 'raises an error when change_username fails' do
        User.any_instance.expects(:change_username).with(new_username).returns(false)
        lambda { xhr :put, :username, username: user.username, new_username: new_username }.should raise_error(Discourse::InvalidParameters)
      end

      it 'should succeed when the change_username returns true' do
        User.any_instance.expects(:change_username).with(new_username).returns(true)
        xhr :put, :username, username: user.username, new_username: new_username
        response.should be_success
      end

    end
  end

  context '.check_username' do
    before do
      DiscourseHub.stubs(:nickname_available?).returns([true, nil])
    end

    it 'raises an error without a username parameter' do
      lambda { xhr :get, :check_username }.should raise_error(ActionController::ParameterMissing)
    end

    shared_examples 'when username is unavailable locally' do
      it 'should return success' do
        response.should be_success
      end

      it 'should return available as false in the JSON' do
        ::JSON.parse(response.body)['available'].should be_false
      end

      it 'should return a suggested username' do
        ::JSON.parse(response.body)['suggestion'].should be_present
      end
    end

    shared_examples 'when username is available everywhere' do
      it 'should return success' do
        response.should be_success
      end

      it 'should return available in the JSON' do
        ::JSON.parse(response.body)['available'].should be_true
      end
    end

    context 'when call_discourse_hub is disabled' do
      before do
        SiteSetting.stubs(:call_discourse_hub?).returns(false)
        DiscourseHub.expects(:nickname_available?).never
        DiscourseHub.expects(:nickname_match?).never
      end

      context 'available everywhere' do
        before do
          xhr :get, :check_username, username: 'BruceWayne'
        end
        include_examples 'when username is available everywhere'
      end

      context 'available locally but not globally' do
        before do
          xhr :get, :check_username, username: 'BruceWayne'
        end
        include_examples 'when username is available everywhere'
      end

      context 'unavailable locally but available globally' do
        let!(:user) { Fabricate(:user) }
        before do
          xhr :get, :check_username, username: user.username
        end
        include_examples 'when username is unavailable locally'
      end

      context 'unavailable everywhere' do
        let!(:user) { Fabricate(:user) }
        before do
          xhr :get, :check_username, username: user.username
        end
        include_examples 'when username is unavailable locally'
      end

      shared_examples 'checking an invalid username' do
        it 'should return success' do
          response.should be_success
        end

        it 'should not return an available key' do
          ::JSON.parse(response.body)['available'].should be_nil
        end

        it 'should return an error message' do
          ::JSON.parse(response.body)['errors'].should_not be_empty
        end
      end

      context 'has invalid characters' do
        before do
          xhr :get, :check_username, username: 'bad username'
        end
        include_examples 'checking an invalid username'

        it 'should return the invalid characters message' do
          ::JSON.parse(response.body)['errors'].should include(I18n.t(:'user.username.characters'))
        end
      end

      context 'is too long' do
        before do
          xhr :get, :check_username, username: 'abcdefghijklmnop'
        end
        include_examples 'checking an invalid username'

        it 'should return the "too short" message' do
          ::JSON.parse(response.body)['errors'].should include(I18n.t(:'user.username.long', max: User.username_length.end))
        end
      end
    end

    context 'when call_discourse_hub is enabled' do
      before do
        SiteSetting.stubs(:call_discourse_hub?).returns(true)
      end

      context 'available locally and globally' do
        before do
          DiscourseHub.stubs(:nickname_available?).returns([true, nil])
          DiscourseHub.stubs(:nickname_match?).returns([false, true, nil])  # match = false, available = true, suggestion = nil
        end

        shared_examples 'check_username when nickname is available everywhere' do
          it 'should return success' do
            response.should be_success
          end

          it 'should return available in the JSON' do
            ::JSON.parse(response.body)['available'].should be_true
          end

          it 'should return global_match false in the JSON' do
            ::JSON.parse(response.body)['global_match'].should be_false
          end
        end

        context 'and email is not given' do
          before do
            xhr :get, :check_username, username: 'BruceWayne'
          end
          include_examples 'check_username when nickname is available everywhere'
        end

        context 'and email is given' do
          before do
            xhr :get, :check_username, username: 'BruceWayne', email: 'brucie@gmail.com'
          end
          include_examples 'check_username when nickname is available everywhere'
        end
      end

      shared_examples 'when email is needed to check nickname match' do
        it 'should return success' do
          response.should be_success
        end

        it 'should return available as false in the JSON' do
          ::JSON.parse(response.body)['available'].should be_false
        end

        it 'should not return a suggested username' do
          ::JSON.parse(response.body)['suggestion'].should_not be_present
        end
      end

      context 'available locally but not globally' do
        before do
          DiscourseHub.stubs(:nickname_available?).returns([false, 'suggestion'])
        end

        context 'email param is not given' do
          before do
            xhr :get, :check_username, username: 'BruceWayne'
          end
          include_examples 'when email is needed to check nickname match'
        end

        context 'email param is an empty string' do
          before do
            xhr :get, :check_username, username: 'BruceWayne', email: ''
          end
          include_examples 'when email is needed to check nickname match'
        end

        context 'email matches global nickname' do
          before do
            DiscourseHub.stubs(:nickname_match?).returns([true, false, nil])
            xhr :get, :check_username, username: 'BruceWayne', email: 'brucie@example.com'
          end
          include_examples 'when username is available everywhere'

          it 'should indicate a global match' do
            ::JSON.parse(response.body)['global_match'].should be_true
          end
        end

        context 'email does not match global nickname' do
          before do
            DiscourseHub.stubs(:nickname_match?).returns([false, false, 'suggestion'])
            xhr :get, :check_username, username: 'BruceWayne', email: 'brucie@example.com'
          end
          include_examples 'when username is unavailable locally'

          it 'should not indicate a global match' do
            ::JSON.parse(response.body)['global_match'].should be_false
          end
        end
      end

      context 'unavailable locally and globally' do
        let!(:user) { Fabricate(:user) }

        before do
          DiscourseHub.stubs(:nickname_available?).returns([false, 'suggestion'])
          xhr :get, :check_username, username: user.username
        end

        include_examples 'when username is unavailable locally'
      end

      context 'unavailable locally and available globally' do
        let!(:user) { Fabricate(:user) }

        before do
          DiscourseHub.stubs(:nickname_available?).returns([true, nil])
          xhr :get, :check_username, username: user.username
        end

        include_examples 'when username is unavailable locally'
      end
    end

    context 'when discourse_org_access_key is wrong' do
      before do
        SiteSetting.stubs(:call_discourse_hub?).returns(true)
        DiscourseHub.stubs(:nickname_available?).raises(RestClient::Forbidden)
        DiscourseHub.stubs(:nickname_match?).raises(RestClient::Forbidden)
      end

      it 'should return an error message' do
        xhr :get, :check_username, username: 'horsie'
        json = JSON.parse(response.body)
        json['errors'].should_not be_nil
        json['errors'][0].should_not be_nil
      end
    end

    describe 'different case of existing username' do
      context "it's my username" do
        let!(:user) { Fabricate(:user, username: 'hansolo') }
        before do
          log_in_user(user)
          xhr :get, :check_username, username: 'HanSolo'
        end
        include_examples 'when username is available everywhere'
      end

      context "it's someone else's username" do
        let!(:user) { Fabricate(:user, username: 'hansolo') }
        before do
          log_in
          xhr :get, :check_username, username: 'HanSolo'
        end
        include_examples 'when username is unavailable locally'
      end

      context "an admin changing it for someone else" do
        let!(:user) { Fabricate(:user, username: 'hansolo') }
        before do
          log_in_user(Fabricate(:admin))
          xhr :get, :check_username, username: 'HanSolo', for_user_id: user.id
        end
        include_examples 'when username is available everywhere'
      end
    end
  end

  describe '.invited' do

    let(:user) { Fabricate(:user) }

    it 'returns success' do
      xhr :get, :invited, username: user.username
      response.should be_success
    end

  end

  describe '.update' do

    context 'not logged in' do
      it 'raises an error when not logged in' do
        expect do
          xhr :put, :update, username: 'somename'
        end.to raise_error(Discourse::NotLoggedIn)
      end
    end

    context 'logged in' do
      let!(:user) { log_in }

      context 'without a token' do
        it 'should ensure you can update the user' do
          Guardian.any_instance.expects(:can_edit?).with(user).returns(false)
          put :update, username: user.username
          response.should be_forbidden
        end

        context 'as a user who can edit the user' do

          before do
            put :update, username: user.username, bio_raw: 'brand new bio'
            user.reload
          end

          it 'updates the user' do
            user.bio_raw.should == 'brand new bio'
          end

          it 'returns json success' do
            response.should be_success
          end
        end
      end
    end
  end

  describe "search_users" do

    let(:topic) { Fabricate :topic }
    let(:user)  { Fabricate :user, username: "joecabot", name: "Lawrence Tierney" }

    before do
      Fabricate :post, user: user, topic: topic
    end

    it "searches when provided the term only" do
      xhr :post, :search_users, term: user.name.split(" ").last
      response.should be_success
      json = JSON.parse(response.body)
      json["users"].map { |u| u["username"] }.should include(user.username)
    end

    it "searches when provided the topic only" do
      xhr :post, :search_users, topic_id: topic.id
      response.should be_success
      json = JSON.parse(response.body)
      json["users"].map { |u| u["username"] }.should include(user.username)
    end

    it "searches when provided the term and topic" do
      xhr :post, :search_users, term: user.name.split(" ").last, topic_id: topic.id
      response.should be_success
      json = JSON.parse(response.body)
      json["users"].map { |u| u["username"] }.should include(user.username)
    end

  end

  describe 'send_activation_email' do
    context 'for an existing user' do
      let(:user) { Fabricate(:user) }

      before do
        UsersController.any_instance.stubs(:fetch_user_from_params).returns(user)
      end

      context 'with a valid email_token' do
        it 'should send the activation email' do
          Jobs.expects(:enqueue).with(:user_email, has_entries(type: :signup))
          xhr :post, :send_activation_email, username: user.username
        end
      end

      context 'without an existing email_token' do
        before do
          user.email_tokens.each {|t| t.destroy}
          user.reload
        end

        it 'should generate a new token' do
          expect {
            xhr :post, :send_activation_email, username: user.username
          }.to change{ user.email_tokens(true).count }.by(1)
        end

        it 'should send an email' do
          Jobs.expects(:enqueue).with(:user_email, has_entries(type: :signup))
          xhr :post, :send_activation_email, username: user.username
        end
      end
    end

    context 'when username does not exist' do
      it 'should not send an email' do
        Jobs.expects(:enqueue).never
        xhr :post, :send_activation_email, username: 'nopenopenopenope'
      end
    end
  end

end
