# 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/>.
#

describe QuizzesHelper do
  include ApplicationHelper
  include QuizzesHelper
  include ERB::Util

  describe "#needs_unpublished_warning" do
    before :once do
      course_with_teacher
    end

    it "is false if quiz not manageable" do
      quiz = Quizzes::Quiz.new(context: @course)

      allow(self).to receive(:can_publish).and_return(false)
      expect(needs_unpublished_warning?(quiz)).to be_falsey
    end

    it "is false if quiz is available with no unpublished changes" do
      quiz = Quizzes::Quiz.new(context: @course)
      quiz.workflow_state = "available"
      quiz.last_edited_at = 10.minutes.ago
      quiz.published_at   = Time.zone.now

      allow(self).to receive(:can_publish).and_return(true)
      expect(needs_unpublished_warning?(quiz)).to be_falsey
    end

    it "is true if quiz is not available" do
      quiz = Quizzes::Quiz.new(context: @course)
      quiz.workflow_state = "created"

      allow(self).to receive(:can_publish).and_return(true)
      expect(needs_unpublished_warning?(quiz)).to be_truthy
    end

    it "is true if quiz has unpublished changes" do
      quiz = Quizzes::Quiz.new(context: @course)
      quiz.workflow_state = "available"
      quiz.last_edited_at = Time.zone.now
      quiz.published_at   = 10.minutes.ago

      allow(self).to receive(:can_publish).and_return(true)
      expect(needs_unpublished_warning?(quiz)).to be_truthy
    end
  end

  describe "#attachment_id_for" do
    it "returns the attachment id if attachment exists" do
      question = { id: 1 }
      @attachments = { 1 => { id: "11" } }
      @stored_params = { "question_1" => ["1"] }
      expect(attachment_id_for(question)).to eq "11"
    end

    it "returns empty string when no attachments stored" do
      question = { id: 1 }
      @stored_params = {}
      @attachments = {}
      expect(attachment_id_for(question)).to be_nil
    end
  end

  context "render_number" do
    it "renders numbers" do
      expect(render_number(1)).to eq "1"
      expect(render_number(100)).to eq "100"
      expect(render_number(1.123)).to eq "1.123"
      expect(render_number(1000.45)).to eq "1,000.45"
      expect(render_number(1000.45966)).to eq "1,000.45966"
      expect(render_number("100")).to eq "100"
      expect(render_number("1.43")).to eq "1.43"
    end

    it "removes trailing zeros" do
      expect(render_number(1.20000000)).to eq "1.2"
      expect(render_number(0.10340000)).to eq "0.1034"
    end

    it "removes trailing zeros and decimal point" do
      expect(render_number(0.00000000)).to eq "0"
      expect(render_number(1.00000000)).to eq "1"
      expect(render_number(100.0)).to eq "100"
    end

    it "renders percentages" do
      expect(render_number("1234.456%")).to eq "1,234.456%"
    end
  end

  context "render_score" do
    it "renders nil scores" do
      expect(render_score(nil)).to eq "_"
    end

    it "renders with default precision" do
      expect(render_score(1000.45966)).to eq "1,000.46"
      expect(render_score("12.3456")).to eq "12.35"
    end

    it "supports higher precision" do
      expect(render_score(1234.4567, 3)).to eq "1,234.457"
      expect(render_score(0.12345000, 4)).to eq "0.1235"
    end
  end

  context "render_quiz_type" do
    it "renders a humanized quiz type string" do
      expect(render_quiz_type("practice_quiz")).to eq "Practice Quiz"
      expect(render_quiz_type("assignment")).to eq "Graded Quiz"
      expect(render_quiz_type("graded_survey")).to eq "Graded Survey"
      expect(render_quiz_type("survey")).to eq "Ungraded Survey"
    end

    it "returns nil for an unrecognized quiz_type" do
      expect(render_quiz_type(nil)).to be_nil
      expect(render_quiz_type("made_up_quiz_type")).to be_nil
    end
  end

  context "render_score_to_keep" do
    it "renders which score to keep when passed in a scoring_policy option" do
      expect(render_score_to_keep("keep_highest")).to eq "Highest"
      expect(render_score_to_keep("keep_latest")).to eq "Latest"
    end

    it "returns nil for an unrecognized scoring_policy" do
      expect(render_score_to_keep(nil)).to be_nil
      expect(render_score_to_keep("made_up_scoring_policy")).to be_nil
    end
  end

  context "render_show_responses" do
    it "answers 'Let Students See Quiz Responses?' when passed a hide_results option" do
      expect(render_show_responses("always")).to eq "No"
      expect(render_show_responses("until_after_last_attempt")).to eq "After Last Attempt"
      expect(render_show_responses(nil)).to eq "Always"
    end

    it "returns nil for an unrecognized hide_results value" do
      expect(render_show_responses("made_up_hide_results")).to be_nil
    end
  end

  context "score_out_of_points_possible" do
    it "shows single digit scores" do
      expect(score_out_of_points_possible(1, 5)).to eq "1 out of 5"
      expect(score_out_of_points_possible(0, 9)).to eq "0 out of 9"
    end

    it "shows 2-decimal precision if necessary" do
      expect(score_out_of_points_possible(0.66666666666, 1)).to eq "0.67 out of 1"
      expect(score_out_of_points_possible(5.23333333333, 10.0)).to eq "5.23 out of 10"
    end

    it "is wrapped by a span when a CSS class, id, or style is given" do
      expect(score_out_of_points_possible(1.5, 3, class: "score_value"))
        .to eq('<span class="score_value">1.5</span> out of 3')
      expect(score_out_of_points_possible(1.5, 3, id: "score"))
        .to eq('<span id="score">1.5</span> out of 3')
      expect(score_out_of_points_possible(1.5, 3, style: "width:100%"))
        .to eq('<span style="width:100%">1.5</span> out of 3')
    end
  end

  context "fill_in_multiple_blanks_question" do
    before do
      @question_text = %(<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value={{question_1}}' />)
      @answer_list = []
      @answers = []

      # double #user_content
      def user_content(stuff)
        stuff
      end
    end

    it "extracts the answers by blank" do
      @answer_list = [{ blank_id: "color", answer: "red" }]

      html = fill_in_multiple_blanks_question(
        question: { question_text: @question_text },
        answer_list: @answer_list,
        answers: @answers
      )

      expect(html).to eq %(<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value=red' readonly="readonly" aria-label='Fill in the blank, read surrounding text' />)
    end

    it "sanitizes user input" do
      malicious_answer_list = [{
        blank_id: "color",
        answer: "><script>alert()</script><img"
      }]

      html = fill_in_multiple_blanks_question(
        question: { question_text: @question_text },
        answer_list: malicious_answer_list,
        answers: @answers
      )

      expect(html).to eq %|<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value=&gt;&lt;script&gt;alert()&lt;/script&gt;&lt;img' readonly="readonly" aria-label='Fill in the blank, read surrounding text' />|
      expect(html).to be_html_safe
    end

    it "adds an appropriate label" do
      html = fill_in_multiple_blanks_question(
        question: { question_text: @question_text },
        answer_list: @answer_list,
        answers: @answers
      )

      expect(html).to match(/aria-label/)
      expect(html).to match(/Fill in the blank/)
    end

    it "handles equation img tags in the question text" do
      broken_question_text = "\"<p>Rubisco is a <input class='question_input' type='text' autocomplete='off' style='width: 120px;' name=\\\"question_8_26534e6c8737f63335d5d98ca4136d09\\\" value='{{question_8_26534e6c8737f63335d5d98ca4136d09}}' > responsible for the first enzymatic step of carbon <input class='question_input' type='text' autocomplete='off' style='width: 120px;' name='question_8_f8e302199c03689d87c52e942b56e1f4' value='{{question_8_f8e302199c03689d87c52e942b56e1f4}}' >. <br><br>equation here: <img class=\\\"equation_image\\\" title=\\\"\\sum\\frac{k}{l}\\\" src=\\\"/equation_images/%255Csum%255Cfrac%257Bk%257D%257Bl%257D\\\" alt=\\\"\\sum\\frac{k}{l}\\\"></p>\""
      @answer_list = [
        { blank_id: "kindof", answer: "protein" },
        { blank_id: "role", answer: "fixing" }
      ]
      html = fill_in_multiple_blanks_question(
        question: { question_text: broken_question_text },
        answer_list: @answer_list,
        answers: @answers
      )
      expect(html).to match(/"readonly"/)
      expect(html).to match(/value='fixing'/)
      expect(html).to match(/value='protein'/)
    end

    it "sanitizes the answer blocks in the noisy question data" do
      broken_question_text = "<p><span>\"Roses are <input\n class='question_input'\n type='text'\n autocomplete='off'\n style='width: 120px;'\n name='question_244_ec9a1c7e5a9f3a6278e9055d8dec00f0'\n value='{{question_244_ec9a1c7e5a9f3a6278e9055d8dec00f0}}' />\n, violets are <input\n class='question_input'\n type='text'\n autocomplete='off'\n style='width: 120px;'\n name='question_244_01731fa53c4cf2f32e893d5c3dbae9c1'\n value='{{question_244_01731fa53c4cf2f32e893d5c3dbae9c1}}' />\n\")</span></p>"
      html = fill_in_multiple_blanks_question(
        question: { question_text: ActiveSupport::SafeBuffer.new(broken_question_text) },
        answer_list: [
          { blank_id: "color1", answer: "red" },
          { blank_id: "color2", answer: "black" }
        ],
        answers: @answers
      )
      expect(html).not_to match "{{"
    end
  end

  context "multiple_dropdowns_question" do
    before do
      # double #user_content
      def user_content(stuff)
        stuff
      end
    end

    it "selects the user's answer" do
      html = multiple_dropdowns_question({
                                           question: {
                                             question_text: 'some <select class="question_input" name="question_4"><option value="val">val</option></select>'
                                           },
                                           answer_list: ["val"],
                                           editable: true
                                         })
      expect(html).to eq 'some <select class="question_input" name="question_4" aria-label="Multiple dropdowns, read surrounding text"><option value="val" selected="selected">val</option></select>'
      expect(html).to be_html_safe
    end

    it "does not blow up if the user's answer isn't there" do
      html = multiple_dropdowns_question({
                                           question: {
                                             question_text: 'some <select class="question_input" name="question_4"><option value="other_val">val</option></select>'
                                           },
                                           answer_list: ["val"],
                                           editable: true
                                         })
      expect(html).to eq 'some <select class="question_input" name="question_4" aria-label="Multiple dropdowns, read surrounding text"><option value="other_val">val</option></select>'
      expect(html).to be_html_safe
    end

    it "disables select boxes that are not editable" do
      html_string = multiple_dropdowns_question({
                                                  question: {
                                                    question_text: 'some <select class="question_input" name="question_4"><option value="val">val</option></select>'
                                                  },
                                                  answer_list: ["val"],
                                                  editable: false
                                                })
      html = Nokogiri::HTML.fragment(html_string)
      span_html = html.css("span").first
      expect(span_html).not_to be_nil
      expect(html_string).to be_html_safe
    end
  end

  describe "#quiz_edit_text" do
    it "returns correct string for survey" do
      quiz = double(survey?: true)
      expect(quiz_edit_text(quiz)).to eq "Edit Survey"
    end

    it "returns correct string for quiz" do
      quiz = double(survey?: false)
      expect(quiz_edit_text(quiz)).to eq "Edit Quiz"
    end
  end

  describe "#quiz_delete_text" do
    it "returns correct string for survey" do
      quiz = double(survey?: true)
      expect(quiz_delete_text(quiz)).to eq "Delete Survey"
    end

    it "returns correct string for quiz" do
      quiz = double(survey?: false)
      expect(quiz_delete_text(quiz)).to eq "Delete Quiz"
    end
  end

  describe "#score_affected_by_regrade" do
    it "returns true if kept score differs from score before regrade" do
      submission = double(score_before_regrade: 5, kept_score: 10, score: 5)
      expect(score_affected_by_regrade?(submission)).to be_truthy
    end

    it "returns false if kept score equals score before regrade" do
      submission = double(score_before_regrade: 5, kept_score: 5, score: 0)
      expect(score_affected_by_regrade?(submission)).to be_falsey
    end
  end

  describe "#answer_title" do
    it "builds title if answer is selected" do
      title = answer_title("foo", true, false, false)
      expect(title).to eq "title=\"foo. You selected this answer.\""
    end

    it "builds title if answer is correct" do
      title = answer_title("foo", false, true, true)
      expect(title).to eq "title=\"foo. This was the correct answer.\""
    end

    it "returns nil if not selected or correct" do
      title = answer_title("foo", false, false, false)
      expect(title).to be_nil
    end
  end

  describe "#render_show_correct_answers" do
    context "show_correct_answers is false" do
      it "shows No" do
        quiz = double({ show_correct_answers: false })
        expect(render_show_correct_answers(quiz)).to eq "No"
      end
    end

    context "show_correct_answers is true, but nothing else is set" do
      it "shows Immediately" do
        quiz = double({
                        show_correct_answers: true,
                        show_correct_answers_at: nil,
                        hide_correct_answers_at: nil,
                        show_correct_answers_last_attempt: false
                      })
        expect(render_show_correct_answers(quiz)).to eq "Immediately"
      end
    end

    context "show_correct_answers_last_attempt is true" do
      it "shows After Last Attempt" do
        quiz = double({
                        show_correct_answers: true,
                        show_correct_answers_at: nil,
                        hide_correct_answers_at: nil,
                        show_correct_answers_last_attempt: true
                      })
        expect(render_show_correct_answers(quiz)).to eq "After Last Attempt"
      end
    end

    context "show_correct_answers_at is set" do
      it "shows date of" do
        time = 1.day.from_now
        quiz = double({
                        show_correct_answers: true,
                        show_correct_answers_at: time,
                        hide_correct_answers_at: nil
                      })
        expect(render_show_correct_answers(quiz)).to eq "After #{datetime_string(time)}"
      end
    end

    context "hide_correct_answers_at is set" do
      it "shows date of" do
        time = 1.day.from_now
        quiz = double({
                        show_correct_answers: true,
                        show_correct_answers_at: nil,
                        hide_correct_answers_at: time,
                      })
        expect(render_show_correct_answers(quiz)).to eq "Until #{datetime_string(time)}"
      end
    end

    context "show_correct_answers_at and hide_correct_answers_at are set" do
      it "shows date of" do
        time = 1.day.from_now
        time2 = 1.week.from_now

        quiz = double({
                        show_correct_answers: true,
                        show_correct_answers_at: time,
                        hide_correct_answers_at: time2,
                      })
        expect(render_show_correct_answers(quiz)).to eq "From #{datetime_string(time)} to #{datetime_string(time2)}"
      end
    end
  end

  describe "#render_correct_answer_protection" do
    it 'provides a useful message when "last attempt"' do
      quiz = double({
                      show_correct_answers_last_attempt: true,
                    })
      quiz_submission = double(last_attempt_completed?: false)

      message = render_correct_answer_protection(quiz, quiz_submission)
      expect(message).to match(/last attempt/)
    end

    context("when correct answers are hidden") do
      let(:quiz) do
        double({
                 show_correct_answers_last_attempt: nil,
                 show_correct_answers: false,
                 show_correct_answers_at: nil,
                 hide_correct_answers_at: nil
               })
      end
      let(:quiz_submission) { double(last_attempt_completed?: false) }

      it "provides a useful message" do
        expect(render_correct_answer_protection(quiz, quiz_submission)).to match(/are hidden/)
      end
    end

    it 'provides nothing when "yes"' do
      quiz = double({
                      show_correct_answers_last_attempt: nil,
                      show_correct_answers: true,
                      show_correct_answers_at: nil,
                      hide_correct_answers_at: nil
                    })
      quiz_submission = double(last_attempt_completed?: false)

      message = render_correct_answer_protection(quiz, quiz_submission)
      expect(message).to be_nil
    end

    it 'provides a useful message, and an availability date, when "show at" is set' do
      quiz = double({
                      show_correct_answers_last_attempt: nil,
                      show_correct_answers: true,
                      show_correct_answers_at: 1.day.from_now,
                      hide_correct_answers_at: nil
                    })
      quiz_submission = double(last_attempt_completed?: false)

      message = render_correct_answer_protection(quiz, quiz_submission)
      expect(message).to match(/will be available/)
    end

    it 'provides a useful message, and a date, when "hide at" is set' do
      quiz = double({
                      show_correct_answers_last_attempt: nil,
                      show_correct_answers: true,
                      show_correct_answers_at: nil,
                      hide_correct_answers_at: 1.day.from_now
                    })
      quiz_submission = double(last_attempt_completed?: false)

      message = render_correct_answer_protection(quiz, quiz_submission)
      expect(message).to match(/are available until/)
    end
  end

  describe "#point_value_for_input" do
    let(:user_answer) { @user_answer }
    let(:question) { { points_possible: 5 } }
    let(:quiz) { @quiz }

    before do
      @quiz = double(quiz_type: "graded_survey")
      @user_answer = { correct: "undefined", points: 5 }
    end

    it "returns user_answer[:points] if correct is true/false" do
      [true, false].each do |bool|
        user_answer[:correct] = bool
        expect(point_value_for_input(user_answer, question)).to eq user_answer[:points]
      end
    end

    it "returns empty if quiz is practice quiz or assignment" do
      ["assignment", "practice_quiz"].each do |quiz_type|
        expect(@quiz).to receive(:quiz_type).and_return quiz_type
        expect(point_value_for_input(user_answer, question)).to eq ""
      end
    end

    it "returns points possible for the question if (un)graded survey" do
      ["survey", "graded_survey"].each do |quiz_type|
        expect(@quiz).to receive(:quiz_type).and_return quiz_type
        expect(point_value_for_input(user_answer, question)).to eq(
          question[:points_possible]
        )
      end
    end
  end

  describe "#comment_get" do
    it "returns _html field if present" do
      comment = comment_get({ foo_html: "<div>Foo</div>", foo: "Bar" }, "foo")
      expect(comment).to eq "<div>Foo</div>"
    end

    it "returns raw field if _html field not present" do
      comment = comment_get({ foo: "Bar" }, "foo")
      expect(comment).to eq "Bar"
    end

    it "adds MathML if appropriate" do
      comment = comment_get({
                              foo_html: '<img class="equation_image" data-equation-content="\coprod"></img>'
                            },
                            "foo")
      expect(comment).to match(/MathML/)
      expect(comment).to match(/∐/)
    end

    it "does not add MathML if new math handling feature is active" do
      def controller.use_new_math_equation_handling?
        true
      end
      comment = comment_get({
                              foo_html: '<img class="equation_image" data-equation-content="\coprod"></img>'
                            },
                            "foo")
      expect(comment).to eq('<img class="equation_image" data-equation-content="\\coprod">')
    end
  end

  describe "#link_to_take_quiz" do
    before do
      @quiz = instance_double("Quiz", cant_go_back?: false)
      allow(self).to receive_messages(preview_quiz_url: "/preview_quiz_url", take_quiz_url: "/take_quiz_url")
    end

    context 'when opts["class"] includes "btn"' do
      let(:link_body) { "Take the Quiz Again" }
      let(:opts) { { "class" => "btn btn-primary", "id" => "take_quiz_link" } }

      it 'adds role="button"' do
        result = link_to_take_quiz(link_body, opts)
        expect(result).to include('role="button"')
      end
    end
  end
end
