# frozen_string_literal: true

RSpec.describe SecondFactor::AuthManager do
  fab!(:user)
  let(:guardian) { Guardian.new(user) }
  fab!(:user_totp) { Fabricate(:user_second_factor_totp, user: user) }

  def create_request(request_method: "GET", path: "/")
    ActionDispatch::TestRequest.create({ "REQUEST_METHOD" => request_method, "PATH_INFO" => path })
  end

  def create_manager(action)
    SecondFactor::AuthManager.new(guardian, action, target_user: user)
  end

  def create_action(request = nil)
    request ||= create_request
    TestSecondFactorAction.new(guardian, request, target_user: user)
  end

  def stage_challenge(successful:)
    request = create_request(request_method: "POST", path: "/abc/xyz")
    action = create_action(request)
    action.expects(:no_second_factors_enabled!).never
    action
      .expects(:second_factor_auth_required!)
      .with({ random_param: "hello" })
      .returns({ callback_params: { call_me_back: 4314 } })
      .once
    manager = create_manager(action)
    secure_session = {}
    expect { manager.run!(request, { random_param: "hello" }, secure_session) }.to raise_error(
      SecondFactor::AuthManager::SecondFactorRequired,
    ) do |ex|
      expect(ex.nonce).to be_present
    end

    challenge =
      JSON.parse(secure_session["current_second_factor_auth_challenge"]).deep_symbolize_keys

    if successful
      challenge[:successful] = true
      secure_session["current_second_factor_auth_challenge"] = challenge.to_json
    end
    [challenge[:nonce], secure_session]
  end

  describe "#allow_backup_codes!" do
    it "adds the backup codes method to the allowed methods set" do
      manager = create_manager(create_action)
      expect(manager.allowed_methods).not_to include(UserSecondFactor.methods[:backup_codes])
      manager.allow_backup_codes!
      expect(manager.allowed_methods).to include(UserSecondFactor.methods[:backup_codes])
    end
  end

  describe "#run!" do
    context "when the user does not have a suitable 2FA method" do
      before { user_totp.destroy! }

      it "calls the no_second_factors_enabled! method of the action" do
        action = create_action
        action.expects(:no_second_factors_enabled!).with({ hello_world: 331 }).once
        action.expects(:second_factor_auth_required!).never
        action.expects(:second_factor_auth_completed!).never
        manager = create_manager(action)
        manager.run!(create_request, { hello_world: 331 }, {})
      end
    end

    it "initiates the 2FA process and stages a challenge in secure session when there is no nonce in params" do
      request = create_request(request_method: "POST", path: "/abc/xyz")
      action = create_action(request)
      action.expects(:no_second_factors_enabled!).never
      action
        .expects(:second_factor_auth_required!)
        .with({ expect_me: 131 })
        .returns(
          callback_params: {
            call_me_back: 4314,
          },
          redirect_url: "/gg",
          description: "hello world!",
        )
        .once
      action.expects(:second_factor_auth_completed!).never
      manager = create_manager(action)
      secure_session = {}
      expect { manager.run!(request, { expect_me: 131 }, secure_session) }.to raise_error(
        SecondFactor::AuthManager::SecondFactorRequired,
      )
      json = secure_session["current_second_factor_auth_challenge"]
      challenge = JSON.parse(json).deep_symbolize_keys
      expect(challenge[:nonce]).to be_present
      expect(challenge[:callback_method]).to eq("POST")
      expect(challenge[:callback_path]).to eq("/abc/xyz")
      expect(challenge[:redirect_url]).to eq("/gg")
      expect(challenge[:allowed_methods]).to eq(manager.allowed_methods.to_a)
      expect(challenge[:callback_params]).to eq({ call_me_back: 4314 })
      expect(challenge[:description]).to eq("hello world!")
    end

    it "prefers callback_method and callback_path from the output of the action's second_factor_auth_required! method if they're present" do
      request = create_request(request_method: "POST", path: "/abc/xyz")
      action = create_action(request)
      action
        .expects(:second_factor_auth_required!)
        .with({})
        .returns(
          callback_params: {
            call_me_back: 4314,
          },
          callback_method: "PUT",
          callback_path: "/test/443",
        )
        .once
      manager = create_manager(action)
      secure_session = {}
      expect { manager.run!(request, {}, secure_session) }.to raise_error(
        SecondFactor::AuthManager::SecondFactorRequired,
      )
      json = secure_session["current_second_factor_auth_challenge"]
      challenge = JSON.parse(json).deep_symbolize_keys
      expect(challenge[:callback_method]).to eq("PUT")
      expect(challenge[:callback_path]).to eq("/test/443")
    end

    it "calls the second_factor_auth_completed! method of the action if the challenge is successful and not expired" do
      nonce, secure_session = stage_challenge(successful: true)

      request = create_request(request_method: "POST", path: "/abc/xyz")
      action = create_action(request)

      action.expects(:no_second_factors_enabled!).never
      action.expects(:second_factor_auth_required!).never
      action.expects(:second_factor_auth_completed!).with({ call_me_back: 4314 }).once
      manager = create_manager(action)
      manager.run!(request, { second_factor_nonce: nonce }, secure_session)
    end

    it "does not call the second_factor_auth_completed! method of the action if the challenge is not marked successful" do
      nonce, secure_session = stage_challenge(successful: false)

      request = create_request(request_method: "POST", path: "/abc/xyz")
      action = create_action(request)
      action.expects(:no_second_factors_enabled!).never
      action.expects(:second_factor_auth_required!).never
      action.expects(:second_factor_auth_completed!).never
      manager = create_manager(action)
      expect {
        manager.run!(request, { second_factor_nonce: nonce }, secure_session)
      }.to raise_error(SecondFactor::BadChallenge) do |ex|
        expect(ex.error_translation_key).to eq("second_factor_auth.challenge_not_completed")
      end
    end

    it "does not call the second_factor_auth_completed! method of the action if the challenge is expired" do
      nonce, secure_session = stage_challenge(successful: true)

      request = create_request(request_method: "POST", path: "/abc/xyz")
      action = create_action(request)
      action.expects(:no_second_factors_enabled!).never
      action.expects(:second_factor_auth_required!).never
      action.expects(:second_factor_auth_completed!).never
      manager = create_manager(action)

      freeze_time (SecondFactor::AuthManager::MAX_CHALLENGE_AGE + 1.minute).from_now
      expect {
        manager.run!(request, { second_factor_nonce: nonce }, secure_session)
      }.to raise_error(SecondFactor::BadChallenge) do |ex|
        expect(ex.error_translation_key).to eq("second_factor_auth.challenge_expired")
      end
    end

    it "calls second_factor_auth_skipped! if skip_second_factor_auth? return true" do
      action = create_action
      params = { a: 1 }
      action.expects(:skip_second_factor_auth?).with(params).returns(true).once
      action.expects(:second_factor_auth_skipped!).with(params).once
      action.expects(:no_second_factors_enabled!).never
      action.expects(:second_factor_auth_required!).never
      action.expects(:second_factor_auth_completed!).never
      manager = create_manager(action)
      manager.run!(action.request, params, {})
    end

    it "doesn't call second_factor_auth_skipped! if skip_second_factor_auth? return false" do
      action = create_action
      params = { a: 1 }
      action.expects(:skip_second_factor_auth?).with(params).returns(false).once
      action.expects(:second_factor_auth_skipped!).never
      action.expects(:no_second_factors_enabled!).never
      action.expects(:second_factor_auth_required!).with(params).returns({}).once
      action.expects(:second_factor_auth_completed!).never
      manager = create_manager(action)
      expect { manager.run!(action.request, params, {}) }.to raise_error(
        SecondFactor::AuthManager::SecondFactorRequired,
      ) do |ex|
        expect(ex.nonce).to be_present
      end
    end

    context "with returned results object" do
      it "has the correct status and contains the return value of the action hook that's called" do
        action = create_action
        action.expects(:skip_second_factor_auth?).with({}).returns(true).once
        action.expects(:second_factor_auth_skipped!).with({}).returns("yeah whatever").once
        manager = create_manager(action)
        results = manager.run!(action.request, {}, {})
        expect(results.data).to eq("yeah whatever")
        expect(results.second_factor_auth_skipped?).to eq(true)

        nonce, secure_session = stage_challenge(successful: true)
        request = create_request(request_method: "POST", path: "/abc/xyz")
        action = create_action(request)
        action
          .expects(:second_factor_auth_completed!)
          .with({ call_me_back: 4314 })
          .returns({ eviltrout: "goodbye :(" })
          .once
        manager = create_manager(action)
        results = manager.run!(request, { second_factor_nonce: nonce }, secure_session)
        expect(results.data).to eq({ eviltrout: "goodbye :(" })
        expect(results.second_factor_auth_completed?).to eq(true)

        user_totp.destroy!
        action = create_action
        action.expects(:no_second_factors_enabled!).with({}).returns("NOTHING WORKS").once
        manager = create_manager(action)
        results = manager.run!(action.request, {}, {})
        expect(results.data).to eq("NOTHING WORKS")
        expect(results.no_second_factors_enabled?).to eq(true)
      end
    end
  end
end