# frozen_string_literal: true

#
# Copyright (C) 2014 - 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/>.
#

# rubcop:disable RSpec/MultipleMemoizedHelpers

describe Lti::LtiOutboundAdapter do
  let(:url) { "/launch/url" }
  let(:account) { Account.new }
  let(:return_url) { "/return/url" }
  let(:user) { User.new }
  let(:resource_type) { :lti_launch_type }
  let(:tool_url) { "http://www.tool.com/launch/url?firstname=rory" }

  let(:tool) do
    ContextExternalTool.new.tap do |tool|
      allow(tool).to receive(:id).and_return("tool_id")
      tool.url = tool_url
    end
  end

  let(:user) { User.create! }

  let(:context) do
    Course.new.tap do |course|
      allow(course).to receive(:id).and_return("course_id")
      course.root_account = account
      course.account = account
    end
  end

  let(:assignment) do
    Assignment.new.tap do |assignment|
      allow(assignment).to receive(:id).and_return("assignment_id")
    end
  end

  let(:subject) { adapter }
  let(:adapter) { Lti::LtiOutboundAdapter.new(tool, user, context) }
  let(:lti_consumer_instance) { LtiOutbound::LTIConsumerInstance.new }
  let(:lti_context) do
    LtiOutbound::LTIContext.new.tap do |lti_context|
      lti_context.consumer_instance = lti_consumer_instance
    end
  end
  let(:lti_user) { LtiOutbound::LTIUser.new }
  let(:lti_tool) { LtiOutbound::LTITool.new }
  let(:lti_assignment) { LtiOutbound::LTIAssignment.new }
  let(:controller) do
    request_mock = double("request")
    allow(request_mock).to receive_messages(host: "/my/url", scheme: "https")
    m = double("controller")
    allow(m).to receive_messages(request: request_mock, logged_in_user: @user || user)
    m
  end
  let(:variable_expander) do
    Lti::VariableExpander.new(account, context, controller, current_user: user)
  end

  before do
    allow_any_instance_of(Lti::LtiContextCreator).to receive(:convert).and_return(lti_context)
    allow_any_instance_of(Lti::LtiUserCreator).to receive(:convert).and_return(lti_user)
    allow_any_instance_of(Lti::LtiToolCreator).to receive(:convert).and_return(lti_tool)
    allow_any_instance_of(Lti::LtiAssignmentCreator).to receive(:convert).and_return(lti_assignment)
  end

  describe "#prepare_tool_launch" do
    it "passes the return_url through" do
      expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:return_url]).to eq return_url }

      adapter.prepare_tool_launch(return_url, variable_expander)
    end

    it "generates the outgoing_email_address" do
      allow(HostUrl).to receive(:outgoing_email_address).and_return("email@email.com")
      expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:outgoing_email_address]).to eq "email@email.com" }

      adapter.prepare_tool_launch(return_url, variable_expander)
    end

    context "launch url" do
      it "gets the launch url from the tool" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:url]).to eq tool.url }

        adapter.prepare_tool_launch(return_url, variable_expander)
      end

      it "gets the launch url from the tool settings when resource_type is specified" do
        expect(tool).to receive(:extension_setting).with(resource_type, :url).and_return("/resource/launch/url")
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:url]).to eq "/resource/launch/url" }

        adapter.prepare_tool_launch(return_url, variable_expander, resource_type:)
      end

      it "passes the launch url through when provided" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:url]).to eq url }

        adapter.prepare_tool_launch(return_url, variable_expander, launch_url: url)
      end
    end

    it "accepts selected html" do
      expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:selected_html]).to eq "<div>something</div>" }

      adapter.prepare_tool_launch(return_url, variable_expander, selected_html: "<div>something</div>")
    end

    context "link code" do
      it "generates the link_code when excluded" do
        generated_link_code = "abc123"
        tool = ContextExternalTool.new
        allow(tool).to receive(:opaque_identifier_for).and_return(generated_link_code)
        adapter = Lti::LtiOutboundAdapter.new(tool, user, context)

        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:link_code]).to eq generated_link_code }

        adapter.prepare_tool_launch(return_url, variable_expander)
      end

      it "passes the link_code through when provided" do
        link_code = "link_code"
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:link_code]).to eq link_code }

        adapter.prepare_tool_launch(return_url, variable_expander, link_code:)
      end
    end

    context "resource_type" do
      it "passes the resource_type through when provided" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:resource_type]).to eq :lti_launch_type }

        adapter.prepare_tool_launch(return_url, variable_expander, resource_type:)
      end
    end

    context "lti outbound object creation" do
      it "creates an lti_context" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:context]).to eq lti_context }

        adapter.prepare_tool_launch(return_url, variable_expander)
      end

      it "creates an lti_user" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:user]).to eq lti_user }

        adapter.prepare_tool_launch(return_url, variable_expander)
      end

      it "creates an lti_tool" do
        expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:tool]).to eq lti_tool }

        adapter.prepare_tool_launch(return_url, variable_expander)
      end
    end
  end

  context "link_params" do
    let(:link_params) { { ext: { lti_assignment_id: "1234" } } }

    it "passes through the secure_parameters when provided" do
      expect(LtiOutbound::ToolLaunch).to receive(:new) { |options| expect(options[:link_params]).to eq link_params }
      adapter.prepare_tool_launch(return_url, variable_expander, { link_params: })
    end
  end

  describe "#launch_url" do
    it "returns the launch url from the prepared tool launch" do
      tool_launch = double("tool launch", url: "/launch/url?with_param")
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
      adapter.prepare_tool_launch(return_url, variable_expander)

      expect(adapter.launch_url).to eq "/launch/url?with_param"
    end

    context "with post_only set to true" do
      it "removes the params from the url" do
        tool_launch = double("tool launch", url: "/launch/url?with_param")
        allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
        adapter.prepare_tool_launch(return_url, variable_expander)

        expect(adapter.launch_url(post_only: true)).to eq "/launch/url"
      end
    end

    it "raises a not prepared error if the tool launch has not been prepared" do
      expect { adapter.launch_url }.to raise_error(RuntimeError, "Called launch_url before calling prepare_tool_launch")
    end
  end

  describe "#default_launch_url" do
    subject { adapter.send :default_launch_url, placement }

    let(:placement) { nil }

    it "returns tool url" do
      expect(subject).to eq tool.url
    end

    context "with placement provided" do
      let(:placement) { :course_navigation }
      let(:placement_url) { "https://example.com/course_navigation" }

      before do
        tool.course_navigation = { url: placement_url }
      end

      it "returns tool url for that placement" do
        expect(subject).to eq placement_url
      end
    end

    context "with environment-specific overrides" do
      let(:override_url) { "http://www.example-beta.com/basic_lti" }
      let(:url) { "http://www.example.com/basic_lti" }

      before do
        allow(ApplicationController).to receive_messages(test_cluster?: true, test_cluster_name: "beta")

        tool.settings[:environments] = {
          launch_url: override_url
        }
      end

      it "uses override for launch_url and includes query parameters" do
        expect(subject).to eq override_url + "?firstname=rory"
      end
    end
  end

  describe "#generate_post_payload" do
    it "calls generate on the tool launch" do
      tool_launch = double("tool launch")
      expect(tool_launch).to receive_messages(generate: {})
      allow(tool_launch).to receive_messages(url: "http://example.com/launch")
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
      adapter.prepare_tool_launch(return_url, variable_expander)
      adapter.generate_post_payload
    end

    it "errors if the url is ridiculous" do
      tool_launch = double("tool launch")
      expect(tool_launch).to receive_messages(generate: {})
      allow(tool_launch).to receive_messages(url: "wat;no-way")
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
      adapter.prepare_tool_launch(return_url, variable_expander)
      expect { adapter.generate_post_payload }.to raise_error(Lti::Errors::InvalidLaunchUrlError)
    end

    it "does not copy query params to the post body if oauth_compliant tool setting is enabled" do
      allow(account).to receive(:all_account_users_for).with(user).and_return([])
      tool.settings = { oauth_compliant: true }
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload
      expect(payload["firstname"]).to be_nil
    end

    it "does not copy query params to the post body if post_only is set and oauth_compliant tool setting is enabled" do
      allow(account).to receive(:all_account_users_for).with(user).and_return([])
      tool.settings = { oauth_compliant: true, post_only: true }
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload
      expect(payload["firstname"]).to be_nil
    end

    it "does copy query params to the post body if oauth_compliant tool setting not set and :disable_post_only is disabled on root account" do
      allow(account).to receive(:all_account_users_for).with(user).and_return([])
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload
      expect(payload["firstname"]).to eq "rory"
    end

    it "does not copy query params to the post body if :disable_post_only is set on root_Account" do
      allow(account).to receive(:all_account_users_for).with(user).and_return([])
      allow(account).to receive(:feature_enabled?).with(:disable_lti_post_only).and_return(true)
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload
      expect(payload["firstname"]).to be_nil
    end

    it "includes the 'ext_lti_assignment_id' if the optional assignment parameter is present" do
      assignment.update(lti_context_id: SecureRandom.uuid)
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload(assignment:)
      expect(payload["ext_lti_assignment_id"]).to eq assignment.lti_context_id
    end

    it "does not include the 'ext_lti_assignment_id' if the optional assignment parameter is absent" do
      adapter.prepare_tool_launch(return_url, variable_expander)
      payload = adapter.generate_post_payload(assignment:)
      expect(payload.keys).not_to include "ext_lti_assignment_id"
    end

    it "passes parent_frame_context to the return url if supplied" do
      adapter.prepare_tool_launch("http://example.com/tool_launch?foo=bar", variable_expander, {
                                    launch_url: "http://www.tool.com/launch/url?firstname=rory",
                                    resource_type: "resource_selection",
                                    parent_frame_context: "123456"
                                  })
      payload = adapter.generate_post_payload(assignment:)
      expect(payload["ext_content_return_url"]).to include "&parent_frame_context=123456"
    end

    it "raises a not prepared error if the tool launch has not been prepared" do
      expect { adapter.generate_post_payload }.to raise_error(RuntimeError, "Called generate_post_payload before calling prepare_tool_launch")
    end
  end

  describe "#generate_post_payload_for_assignment" do
    let(:outcome_service_url) { "/outcome/service" }
    let(:legacy_outcome_service_url) { "/legacy/service" }
    let(:lti_turnitin_outcomes_placement_url) { "turnitin/outcomes/placement" }
    let(:tool_launch) { double("tool launch", generate: {}, url: "http://example.com/launch") }

    before do
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
      allow(BasicLTI::Sourcedid).to receive(:encryption_secret) { "encryption-secret-5T14NjaTbcYjc4" }
      allow(BasicLTI::Sourcedid).to receive(:signing_secret) { "signing-secret-vp04BNqApwdwUYPUI" }
    end

    it "includes the 'ext_lti_assignment_id' parameter" do
      assignment.update(lti_context_id: SecureRandom.uuid)
      adapter.prepare_tool_launch(return_url, variable_expander)
      expect(tool_launch).to receive(:for_assignment!).with(lti_assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
      payload = adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
      expect(payload["ext_lti_assignment_id"]).to eq assignment.lti_context_id
    end

    it "creates an lti_assignment" do
      adapter.prepare_tool_launch(return_url, variable_expander)

      expect(tool_launch).to receive(:for_assignment!).with(lti_assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)

      adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
    end

    it "raises a not prepared error if the tool launch has not been prepared" do
      expect do
        adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
      end.to raise_error(RuntimeError, "Called generate_post_payload_for_assignment before calling prepare_tool_launch")
    end
  end

  describe "#generate_post_payload_for_homework_submission" do
    it "creates an lti_assignment" do
      tool_launch = double("tool launch", generate: {}, url: "http://example.com/launch")
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
      adapter.prepare_tool_launch(return_url, variable_expander)

      expect(tool_launch).to receive(:for_homework_submission!).with(lti_assignment)

      adapter.generate_post_payload_for_homework_submission(assignment)
    end

    it "raises a not prepared error if the tool launch has not been prepared" do
      expect do
        adapter.generate_post_payload_for_homework_submission(assignment)
      end.to raise_error(RuntimeError, "Called generate_post_payload_for_homework_submission before calling prepare_tool_launch")
    end
  end

  describe "#generate_post_payload_for_student_context_card" do
    let(:student_id) { user.global_id }
    let(:student) { user }

    it "raises a not prepared error if the tool launch has not been prepared" do
      expect do
        adapter.generate_post_payload_for_student_context_card(student: @student)
      end.to raise_error(RuntimeError, "Called generate_post_payload_for_student_context_card before calling prepare_tool_launch")
    end

    it "add student id to the overrides" do
      tool_launch = double("tool launch", generate: {}, url: "http://example.com/launch")
      allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)

      adapter.prepare_tool_launch(return_url, variable_expander)
      adapter.generate_post_payload_for_student_context_card(student:)
      expect(tool_launch).to have_received(:generate).with(hash_including(lti_student_id: student_id))
    end
  end

  describe ".consumer_instance_class" do
    around do |example|
      orig_class = Lti::LtiOutboundAdapter.consumer_instance_class
      example.run
      Lti::LtiOutboundAdapter.consumer_instance_class = orig_class
    end

    it "returns the custom instance class if defined" do
      some_class = Class.new
      Lti::LtiOutboundAdapter.consumer_instance_class = some_class

      expect(Lti::LtiOutboundAdapter.consumer_instance_class).to eq some_class
    end

    it "returns the LtiOutbound::LTIConsumerInstance if none defined" do
      Lti::LtiOutboundAdapter.consumer_instance_class = nil
      expect(Lti::LtiOutboundAdapter.consumer_instance_class).to eq LtiOutbound::LTIConsumerInstance
    end
  end

  describe "#encode_source_id" do
    let(:user) do
      student_in_course
      @student
    end
    let(:assignment) { assignment_model(course: @course) }
    let(:course) { assignment.course }
    let(:tool) { external_tool_model(context: course) }
    let(:adapter) { Lti::LtiOutboundAdapter.new(tool, user, course) }
    let(:enrollment) { StudentEnrollment.create!(user:, course:, workflow_state: "active") }

    before do
      allow(BasicLTI::Sourcedid).to receive(:encryption_secret) { "encryption-secret-5T14NjaTbcYjc4" }
      allow(BasicLTI::Sourcedid).to receive(:signing_secret) { "signing-secret-vp04BNqApwdwUYPUI" }
      assignment.update!(
        external_tool_tag: ContentTag.create!(
          context: assignment,
          content: tool,
          title: "test",
          url: tool.url
        )
      )
      allow_any_instance_of(Account).to receive(:feature_enabled?).and_call_original
    end

    it "builds the expected encrypted JWT with the correct course data" do
      allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
      sourced_id = adapter.encode_source_id(assignment)
      parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
      expect(parsed_sourced_id.course).to eq course
    end

    it "builds the expected encrypted JWT with the correct assignment data" do
      allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
      sourced_id = adapter.encode_source_id(assignment)
      parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
      expect(parsed_sourced_id.assignment).to eq assignment
    end

    it "builds the expected encrypted JWT with the correct user data" do
      allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
      sourced_id = adapter.encode_source_id(assignment)
      parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
      expect(parsed_sourced_id.user).to eq user
    end

    it 'uses the new sourcedids if the "encrypted_sourcedids" FF is enabled' do
      allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
      sourced_id = adapter.encode_source_id(assignment)
      expect(sourced_id).not_to match(BasicLTI::Sourcedid::SOURCE_ID_REGEX)
    end

    it 'uses legacy sourcedids if the "encrypted_sourcedids" FF is disabled' do
      allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(false)
      sourced_id = adapter.encode_source_id(assignment)
      expect(sourced_id).to match(BasicLTI::Sourcedid::SOURCE_ID_REGEX)
    end
  end
end

# rubcop:enable RSpec/MultipleMemoizedHelpers
