# frozen_string_literal: true

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

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')

describe Turnitin::Client do
  def turnitin_assignment
    @assignment = @course.assignments.new(:title => "some assignment")
    @assignment.workflow_state = "published"
    @assignment.turnitin_enabled = true
    @assignment.save!
  end

  def turnitin_submission
    @submission = @assignment.submit_homework(@user, :submission_type => 'online_upload', :attachments => [attachment_model(:context => @user, :content_type => 'text/plain')])
    @submission.reload
  end

  FakeHTTPResponse = Struct.new(:body)
  def stub_net_http_to_return(partial_body, return_code = 1)
    body = "<returndata>#{ partial_body }<rcode>#{return_code}</rcode></returndata>"
    fake_response = FakeHTTPResponse.new(body)
    expect_any_instance_of(Net::HTTP).to receive(:start).and_return(fake_response)
  end

  describe '#state_from_similarity_score' do
    it "returns 'none' if the similarity score is zero" do
      expect(Turnitin.state_from_similarity_score(0)).to eq 'none'
    end

    it "returns 'acceptable' if similarity score is less than 25" do
      expect(Turnitin.state_from_similarity_score(24)).to eq 'acceptable'
    end

    it "returns 'warning' if similarity score is between 25 and 50" do
      expect(Turnitin.state_from_similarity_score(49)).to eq 'warning'
    end

    it "returns 'problem' if similarity score is between 50 and 75" do
      expect(Turnitin.state_from_similarity_score(74)).to eq 'problem'
    end

    it "returns 'failure' if score is greater or equal to 75" do
      expect(Turnitin.state_from_similarity_score(75)).to eq 'failure'
    end
  end

  describe "initialize" do
    it "defaults to using api.turnitin.com" do
      expect(Turnitin::Client.new('test_account', 'sekret').host).to eq "api.turnitin.com"
    end

    it "allows the endpoint to be configurable" do
      expect(Turnitin::Client.new('test_account', 'sekret', 'www.blah.com').host).to eq "www.blah.com"
    end
  end

  describe 'class methods' do
    before(:each) do
      @default_settings = {
        :originality_report_visibility => 'immediate',
        :s_paper_check => '1',
        :internet_check => '1',
        :journal_check => '1',
        :exclude_biblio => '1',
        :exclude_quoted => '1',
        :exclude_type => '0',
        :exclude_value => '',
        :submit_papers_to => '1'
      }
    end

    it 'has correct default assignment settings' do
      expect(Turnitin::Client.default_assignment_turnitin_settings).to eq @default_settings
    end

    it 'normalizes assignment settings' do
      @default_settings[:originality_report_visibility] = 'never'
      @default_settings[:exclude_type] = '1'
      @default_settings[:exclude_value] = '50'
      normalized_settings = Turnitin::Client.normalize_assignment_turnitin_settings(@default_settings)
      expect(normalized_settings).to eq({
        :originality_report_visibility=>"never",
        :s_paper_check=>"1",
        :internet_check=>"1",
        :journal_check=>"1",
        :exclude_biblio=>"1",
        :exclude_quoted=>"1",
        :exclude_type=>"1",
        :exclude_value=>"50",
        :submit_papers_to=>"1",
        :s_view_report=>"0" })
    end

    it 'determines student visibility' do
      expect(Turnitin::Client.determine_student_visibility('after_grading')).to eq '1'
      expect(Turnitin::Client.determine_student_visibility('never')).to eq '0'
    end
  end

  describe "create assignment" do
    before(:each) do
      course_with_student(:active_all => true)
      turnitin_assignment
      @turnitin_api = Turnitin::Client.new('test_account', 'sekret')
      expect(@assignment.context).to receive(:turnitin_settings).at_least(1).and_return([:placeholder])
      expect(Turnitin::Client).to receive(:new).with(:placeholder).and_return(@turnitin_api)

      @sample_turnitin_settings = {
        :originality_report_visibility => 'after_grading',
        :s_paper_check => '0',
        :internet_check => '0',
        :journal_check => '0',
        :exclude_biblio => '0',
        :exclude_quoted => '0',
        :exclude_type => '1',
        :exclude_value => '5'
      }
      @assignment.update(:turnitin_settings => @sample_turnitin_settings)
    end

    it "marks assignment as created and current on success" do
      # doesn't matter what the assignmentid is, it's existance is simply used as a request success test
      stub_net_http_to_return('<assignmentid>12345</assignmentid>')
      status = @assignment.create_in_turnitin

      expect(status).to be_truthy
      expect(@assignment.reload.turnitin_settings).to eql @sample_turnitin_settings.merge({ :created => true, :current => true, :s_view_report => "1", :submit_papers_to => '0'})
    end

    it "stores error code and message on failure" do
      # doesn't matter what the assignmentid is, it's existance is simply used as a request success test
      stub_net_http_to_return '<rcode>123</rcode><rmessage>You cannot create this assignment right now</rmessage>'
      status = @assignment.create_in_turnitin

      expect(status).to be_falsey
      expect(@assignment.reload.turnitin_settings).to eql @sample_turnitin_settings.merge({
        :s_view_report => "1",
        :submit_papers_to => '0',
        :error => {
          :error_code => 123,
          :error_message => 'You cannot create this assignment right now',
          :public_error_message => 'There was an error submitting to the similarity detection service. Please try resubmitting the file before contacting support.'
        }
      })
    end

    it "does not make api call if assignment is marked current" do
      stub_net_http_to_return('<assignmentid>12345</assignmentid')
      @assignment.create_in_turnitin
      status = @assignment.create_in_turnitin

      expect(status).to be_truthy
      expect(@assignment.reload.turnitin_settings).to eql @sample_turnitin_settings.merge({ :created => true, :current => true, :s_view_report => "1", :submit_papers_to => '0'})
    end

    it "sets s_view_report to 0 if originality_report_visibility is 'never'" do
      @sample_turnitin_settings[:originality_report_visibility] = 'never'
      @assignment.update(:turnitin_settings => @sample_turnitin_settings)
      stub_net_http_to_return('<assignmentid>12345</assignmentid>')
      @assignment.create_in_turnitin

      expect(@assignment.reload.turnitin_settings).to eql @sample_turnitin_settings.merge({ :created => true, :current => true, :s_view_report => '0', :submit_papers_to => '0'})
    end
  end

  describe "submit paper" do
    before(:each) do
      course_with_student(:active_all => true)
      turnitin_assignment
      turnitin_submission
      @turnitin_api = Turnitin::Client.new('test_account', 'sekret')

      expect(@submission.context).to receive(:turnitin_settings).at_least(1).and_return([:placeholder])
      expect(@submission.assignment.context).to receive(:turnitin_settings).at_least(1).and_return([:placeholder])
      expect(Turnitin::Client).to receive(:new).at_least(1).with(:placeholder).and_return(@turnitin_api)
      expect(@turnitin_api).to receive(:enrollStudent).with(@course, @user).and_return(double(:success? => true))
      expect(@turnitin_api).to receive(:createOrUpdateAssignment).with(@assignment, @assignment.turnitin_settings).and_return({ :assignment_id => "1234" })
      expect_any_instantiation_of(@attachment).to receive(:open).and_return(:my_stub)
    end

    it "submits attached files to turnitin" do
      stub_net_http_to_return('<objectID>12345</objectID>')
      status = @submission.submit_to_turnitin

      expect(status).to be_truthy
      expect(@submission.turnitin_data[@attachment.asset_string][:object_id]).to eql "12345"
    end

    it "stores errors in the turnitin_data hash" do
      stub_net_http_to_return('<rmessage>I am a random turnitin error message.</rmessage>', 216)
      status = @submission.submit_to_turnitin

      expect(status).to be_falsey
      expect(@submission.turnitin_data[@attachment.asset_string][:object_id]).to be_nil
      expect(@submission.turnitin_data[@attachment.asset_string][:error_code]).to eql 216
      expect(@submission.turnitin_data[@attachment.asset_string][:error_message]).to eql "I am a random turnitin error message."
      expect(@submission.turnitin_data[@attachment.asset_string][:public_error_message]).to eql "The student limit for this account has been reached. Please contact your account administrator."
    end
  end

  describe "#prepare_params" do
    before(:each) do
      course_with_student(:active_all => true)
      turnitin_assignment
      turnitin_submission
      @turnitin_api = Turnitin::Client.new('test_account', 'sekret')
    end

    let(:turnitin_submit_args) do
      {
        :post => true,
        :utp => '1',
        :ptl => @attachment.display_name,
        :ptype => "2",
        :user => @student,
        :course => @course,
        :assignment => @assignment,
        :tem => "spec@null.instructure.example.com"
      }
    end

    context "when submitting a paper" do

      let(:teacher_email_arg) { turnitin_submit_args[:tem] }
      let(:paper_title_arg) { turnitin_submit_args[:ptl] }

      let(:processed_params) { @turnitin_api.prepare_params(:submit_paper, '2', turnitin_submit_args) }

      let(:processed_teacher_email) { processed_params[:tem] }
      let(:processed_paper_title) { processed_params[:ptl] }
      let(:processed_user_first_name) { processed_params[:ufn] }
      let(:processed_md5) { processed_params[:md5] }

      context "when escaping parameters" do

        it "escapes '%' signs" do
          @attachment.display_name = "Awkward%20Name.txt"
          expect(paper_title_arg).to include("%") # sanity check

          expect(processed_paper_title).to eql(CGI.escape(paper_title_arg))
        end

        it "escapes '@' signs" do
          expect(teacher_email_arg).to include("@") # sanity check
          expect(processed_teacher_email).to eql(CGI.escape(teacher_email_arg))
        end

        it "escapes spaces with '%20', not '+'" do
          @attachment.display_name = "My Submission With Spaces.txt"
          expect(processed_paper_title).to eql(@attachment.display_name.gsub(" ", "%20"))
        end

        # we can't test an actual md5 returned from turnitin without putting our
        # credentials in the test code (since the credentials are part of the string
        # from which the md5 is generated). So the next best thing is to check what
        # we're assuming turnitin does, which is to first unescape and then compute
        # md5.
        it "generates the md5 before escaping parameters" do
          md5_params = {}
          processed_params.each do |key, value|
            md5_params[key] = URI.unescape(value) unless key == :md5
          end

          expect(@turnitin_api.request_md5(md5_params)).to eql(processed_md5)
        end
      end
    end

    context "when creating a user" do

      let(:processed_params) { @turnitin_api.prepare_params(:create_user, '2', turnitin_submit_args) }

      let(:processed_user_first_name) { processed_params[:ufn] }
      let(:processed_user_last_name) { processed_params[:uln] }

      it "correctly uses the user's first and last names" do
        @student.name = "First Last"
        @student.sortable_name = "Last, First"

        expect(processed_user_first_name).to eq "First"
        expect(processed_user_last_name).to eq "Last"
      end

      it "creates a last name if none is given" do
        @student.name = "User"
        @student.sortable_name = "User"

        expect(processed_user_first_name).to eq "User"
        expect(processed_user_last_name).not_to be_empty
      end

    end

    it "ensures turnitin recieves unique assignment names even if the assignments have the same name" do
      process_title = lambda do |title|
        turnitin_assignment
        @assignment.title = title
        args = turnitin_submit_args.clone # we have to #clone this and call #prepare_params maually because rspec only evaluates 'let' blocks once per test
        args[:assignment] = @assignment

        params = @turnitin_api.prepare_params(:this_param_is_irrelevant_for_this_test, '2', args)

        processed_title = params[:assign]
        processed_title
      end

      processed_title_a = process_title.call("non_unique_title")
      process_title_b = process_title.call("non_unique_title")

      # sanity check
      expect(processed_title_a).to include("non_unique_title")
      expect(process_title_b).to include("non_unique_title")

      expect(processed_title_a).not_to eq process_title_b
    end
  end

  describe "#request_md5" do
    # From the turnitin api docs: the md5 for thess parameters should be
    # calculated by concatenatin aid + diagnostic + encrypt + fcmd + fid + gmtime
    # + uem + ufn + uln + utp + shared secret key:
    #
    # The concatenated string, before md5 is:
    # 1000011200310311john.doe@myschool.eduJohnDoejohn1232hothouse123
    it "follows the turnitin documentation way of generating the md5" do
      doc_sample_account_id = "100"
      doc_sample_shared_secret = "hothouse123"
      doc_sample_params = {
        :gmtime => "200310311",
        :fid => "1",
        :fcmd  => "1",
        :encrypt => "0",
        :aid => doc_sample_account_id,
        :diagnostic => "0",
        :uem => "john.doe@myschool.edu",
        :upw => "john123",
        :ufn => "John",
        :uln => "Doe",
        :utp => "2"
      }
      doc_sample_md5 = "12a4e7b0bfc5f55b4b1ef252b1b05919"

      @turnitin_api = Turnitin::Client.new(doc_sample_account_id, doc_sample_shared_secret)
      expect(@turnitin_api.request_md5(doc_sample_params)).to eql(doc_sample_md5)
    end
  end

  describe '#email' do
    it "uses turnitin_id for courses" do
      course_factory
      t = Turnitin::Client.new('blah', 'blah')
      expect(@course.turnitin_id).to be_nil
      expect(t.email(@course)).to eq "course_#{@course.global_id}@null.instructure.example.com"
      expect(@course.turnitin_id).to eql @course.global_id
    end
  end

  describe '#id' do
    it "uses turnitin_id when defined" do
      turnitin = Turnitin::Client.new('blah', 'blah')
      student_in_course active_all: true
      assignment = @course.assignments.create!

      expect(turnitin.id(@course)).to eql "course_#{@course.turnitin_id}"
      expect(turnitin.id(assignment)).to eql "assignment_#{assignment.turnitin_id}"
      expect(turnitin.id(@student)).to eql "user_#{@student.turnitin_id}"
    end
  end
end
