# frozen_string_literal: true

require 'rails_helper'

class Controller < AnonymousController
  include Lti
end

RSpec.describe Lti do
  let(:controller) { Controller.new }
  let(:session) { double }

  describe '#build_tool_provider' do
    it 'instantiates a tool provider' do
      expect(IMS::LTI::ToolProvider).to receive(:new)
      controller.send(:build_tool_provider, consumer: build(:consumer), parameters: {})
    end
  end

  describe '#external_user_name' do
    let(:first_name) { 'Jane' }
    let(:last_name) { 'Doe' }
    let(:full_name) { 'John Doe' }
    let(:provider) { double }
    let(:provider_full) { instance_double(IMS::LTI::ToolProvider, lis_person_name_full: full_name) }

    context 'when a full name is provided' do
      it 'returns the full name' do
        allow(provider_full).to receive(:lis_person_name_full).and_return(full_name)
        expect(controller.send(:external_user_name, provider_full)).to eq(full_name)
      end
    end

    context 'when only partial information is provided' do
      it 'returns the first available name' do
        expect(provider).to receive(:lis_person_name_full)
        allow(provider).to receive(:lis_person_name_given).and_return(first_name)
        expect(provider).not_to receive(:lis_person_name_family)
        expect(controller.send(:external_user_name, provider)).to eq(first_name)
      end
    end
  end

  describe '#refuse_lti_launch' do
    it 'returns to the tool consumer' do
      message = I18n.t('sessions.oauth.invalid_consumer')
      expect(controller).to receive(:return_to_consumer).with(lti_errormsg: I18n.t('sessions.oauth.failure', error: message))
      controller.send(:refuse_lti_launch, message:)
    end
  end

  describe '#return_to_consumer' do
    context 'with a return URL' do
      let(:consumer_return_url) { 'https://example.org' }
      let(:provider) { instance_double(IMS::LTI::ToolProvider, launch_presentation_return_url: consumer_return_url) }

      before { controller.instance_variable_set(:@provider, provider) }

      it 'redirects to the tool consumer' do
        expect(controller).to receive(:redirect_to).with(consumer_return_url, allow_other_host: true, status: :see_other)
        controller.send(:return_to_consumer)
      end

      it 'passes messages to the consumer' do
        message = I18n.t('sessions.oauth.failure', error: 'dummy error')
        expect(controller).to receive(:redirect_to).with("#{consumer_return_url}?lti_errorlog=#{CGI.escape(message)}", allow_other_host: true, status: :see_other)
        controller.send(:return_to_consumer, lti_errorlog: message)
      end

      context 'when the return URL already contains a query parameter' do
        let(:consumer_return_url) { 'https://example.org?foo=bar' }

        it 'correctly appends query parameters' do
          message = I18n.t('sessions.oauth.failure', error: 'dummy error')
          expect(controller).to receive(:redirect_to).with("#{consumer_return_url}&lti_errorlog=#{CGI.escape(message)}&lti_msg=#{CGI.escape(message)}", allow_other_host: true, status: :see_other)
          controller.send(:return_to_consumer, lti_errorlog: message, lti_msg: message)
        end
      end

      context 'when the return URL is empty' do
        let(:consumer_return_url) { '' }

        it 'redirects to the root URL' do
          expect(controller).to receive(:redirect_to).with(:root, status: :see_other)
          controller.send(:return_to_consumer)
        end

        it 'displays alerts' do
          message = I18n.t('sessions.oauth.failure', error: 'dummy error')
          controller.send(:return_to_consumer, lti_errormsg: message)
          expect(controller.instance_variable_get(:@flash)[:danger]).to eq(obtain_message(message))
        end
      end

      context 'when the return URL is relative' do
        let(:consumer_return_url) { '/path' }

        it 'redirects to the root URL' do
          expect(controller).to receive(:redirect_to).with(:root, status: :see_other)
          controller.send(:return_to_consumer)
        end

        it 'displays alerts' do
          message = I18n.t('sessions.oauth.failure', error: 'dummy error')
          controller.send(:return_to_consumer, lti_errormsg: message)
          expect(controller.instance_variable_get(:@flash)[:danger]).to eq(obtain_message(message))
        end
      end
    end

    context 'without a return URL' do
      before do
        allow(controller).to receive(:params).and_return({})
      end

      it 'redirects to the root URL' do
        expect(controller).to receive(:redirect_to).with(:root, status: :see_other)
        controller.send(:return_to_consumer)
      end

      it 'displays alerts' do
        message = I18n.t('sessions.oauth.failure', error: 'dummy error')
        controller.send(:return_to_consumer, lti_errormsg: message)
        expect(controller.instance_variable_get(:@flash)[:danger]).to eq(obtain_message(message))
      end

      it 'displays notices' do
        message = I18n.t('sessions.destroy_through_lti.success_without_outcome')
        controller.send(:return_to_consumer, lti_msg: message)
        expect(controller.instance_variable_get(:@flash)[:info]).to eq(obtain_message(message))
      end
    end
  end

  describe '#send_scores' do
    let(:consumer) { create(:consumer) }
    let(:score) { 0.5 }
    let(:submission) { create(:submission) }

    before do
      create(:lti_parameter, external_user: submission.contributor, exercise: submission.exercise)
    end

    context 'with an invalid score' do
      it 'raises an exception' do
        allow(submission).to receive(:normalized_score).and_return Lti::MAXIMUM_SCORE * 2
        expect { controller.send(:send_scores, submission) }.to raise_error(Lti::Error)
      end
    end

    context 'with an valid score' do
      context 'with a tool consumer' do
        context 'when grading is not supported' do
          it 'returns a corresponding status' do
            allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false)
            allow(submission).to receive(:normalized_score).and_return score
            submit_info = controller.send(:send_scores, submission)
            expect(submit_info[:users][:all]).to eq(submit_info[:users][:unsupported])
          end
        end

        context 'when grading is supported' do
          let(:response) { double }
          let(:send_scores) { controller.send(:send_scores, submission) }
          let(:score_sent) { score }

          before do
            allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(true)
            allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score_sent).and_return(response)
            allow(response).to receive(:response_code).at_least(:once).and_return(200)
            allow(response).to receive(:post_response).and_return(response)
            allow(response).to receive(:body).at_least(:once).and_return('')
            allow(response).to receive(:code_major).at_least(:once).and_return('success')
            allow(submission).to receive(:normalized_score).and_return score
          end

          it 'sends the score' do
            expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score_sent)
            send_scores
          end

          it 'returns code, message, deadline and status' do
            expect(send_scores[:users][:all]).to eq(send_scores[:users][:success])
            expect(send_scores[:deadline]).to eq(:none)
            expect(send_scores[:detailed_results].first[:code]).to eq(response.response_code)
            expect(send_scores[:detailed_results].first[:message]).to eq(response.body)
            expect(send_scores[:detailed_results].first[:status]).to eq(response.code_major)
          end

          context 'when submission is before deadline' do
            before do
              allow(submission).to receive(:before_deadline?).and_return true
            end

            it 'returns deadline' do
              expect(send_scores[:deadline]).to eq(:before_deadline)
            end
          end

          context 'when submission is within grace period' do
            let(:score_sent) { score * 0.8 }

            before do
              allow(submission).to receive_messages(before_deadline?: false, within_grace_period?: true)
            end

            it 'returns deadline and reduced score' do
              expect(send_scores[:deadline]).to eq(:within_grace_period)
              expect(send_scores[:score][:sent]).to eq(score * 0.8)
            end

            it 'sends the reduced score' do
              expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score_sent)
              send_scores
            end
          end

          context 'when submission is after late deadline' do
            let(:score_sent) { 0 }

            before do
              allow(submission).to receive_messages(before_deadline?: false,
                within_grace_period?: false,
                after_late_deadline?: true)
            end

            it 'returns deadline and reduced score' do
              expect(send_scores[:deadline]).to eq(:after_late_deadline)
              expect(send_scores[:score][:sent]).to eq(0)
            end

            it 'sends the reduced score' do
              expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score_sent)
              send_scores
            end
          end
        end

        context 'when transmission fails' do
          let(:send_scores) { controller.send(:send_scores, submission) }
          let(:score_sent) { 0 }

          before do
            allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(true)
            allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score_sent).and_raise(IMS::LTI::XMLParseError)
          end

          it 'does not raise an exception' do
            expect { send_scores }.not_to raise_error
          end

          it 'returns an error status' do
            expect(send_scores[:users][:all]).to eq(send_scores[:users][:error])
            expect(send_scores[:detailed_results].first[:status]).to eq('error')
          end
        end
      end

      context 'without a tool consumer' do
        it 'returns a corresponding status' do
          submission.contributor.consumer = nil

          allow(submission).to receive(:normalized_score).and_return score
          submit_info = controller.send(:send_scores, submission)
          expect(submit_info[:users][:all]).to eq(submit_info[:users][:unsupported])
        end
      end
    end
  end

  describe '#store_lti_session_data' do
    let(:parameters) { ActionController::Parameters.new({}) }

    it 'creates an LtiParameter Object' do
      expect do
        controller.instance_variable_set(:@user, create(:external_user))
        controller.instance_variable_set(:@exercise, create(:fibonacci))
        controller.send(:store_lti_session_data, parameters)
      end.to change(LtiParameter, :count).by(1)
    end
  end

  describe '#store_nonce' do
    it 'adds a nonce to the nonce store' do
      nonce = SecureRandom.hex
      expect(NonceStore).to receive(:add).with(nonce)
      controller.send(:store_nonce, nonce)
    end
  end
end
