# Source: form-host/form-filler.js

    const _ = require('underscore');
    const $ = require('jquery');

    const getRecordForCompletedForm = require('./enketo');
    const saveContact = require('./save-contact');

    class FormFiller {
      constructor(formName, form, formXml, options) {
        this.form = form;
        this.formName = formName;
        this.formXml = formXml;
        this.options = _.defaults(options, {
          verbose: true,
        });
        this.log = (...args) => this.options.verbose && console.log('FormFiller', ...args);
      }

      /**
       * An object describing the result of filling a form.
       * @typedef {Object} FillResult
       * @property {FillError[]} errors A list of errors
       * @property {string} section The page number on which the errors occurred
       * @property {Object} report The report object which resulted from submitting the filled report. Undefined if an error blocks form submission.
       * @property {Object[]} additionalDocs An array of database documents which are created in addition to the report.
       */

      /**
       * An object describing an error which has occurred while filling a form.
       * @typedef {Object} FillError
       * @property {string} type A classification of the error [ 'validation', 'general', 'page' ]
       * @property {string} msg Description of the error
       */

      async fillAppForm(multiPageAnswer) {
        const { isComplete, errors } = await fillForm(this, multiPageAnswer);
        const resultingDocs = isComplete ? getRecordForCompletedForm(this.form, this.formXml, this.formName, window.now) : [];
        const [report, ...additionalDocs] = resultingDocs;

        return {
          errors,
          section: 'general',
          report,
          additionalDocs,
        };
      }

      async fillContactForm(contactType, multiPageAnswer) {
        const { isComplete, errors } = await fillForm(this, multiPageAnswer);
        const contacts = isComplete ? await saveContact(this.form, contactType, window.now) : [];

        return {
          errors,
          section: 'general',
          contacts
        };
      }

      // Modified from enketo-core/src/js/Form.js validateContent
      getVisibleValidationErrors() {
        const self = this;
        const $container = self.form.view.$;
        const validations = $container.find('.question').addBack('.question').map(() => {
          const $elem = $(this).find('input:not(.ignore):not(:disabled), select:not(.ignore):not(:disabled), textarea:not(.ignore):not(:disabled)');
          if ($elem.length === 0) {
            return Promise.resolve();
          }
          return self.form.validateInput( $elem.eq( 0 ) );
        }).toArray();

        return Promise.all( validations )
          .then(() => {
            const validationErrors = $container
              .find('.invalid-required:not(.disabled), .invalid-constraint:not(.disabled), .invalid-relevant:not(.disabled)')
              .children('span.active:not(.question-label)')
              .filter(function() {
                return $(this).css('display') === 'block';
              });

            return Array.from(validationErrors)
              .map(span => ({
                type: 'validation',
                question: span.parentElement.innerText,
                msg: span.innerText,
              }));
          })
          .catch(err => [{
            type: 'failure to validate',
            msg: err,
          }]);
      }
    }

    const fillForm = async (self, multiPageAnswer) => {
      self.log(`Filling form in ${multiPageAnswer.length} pages.`);
      const results = [];
      for (const pageIndex in multiPageAnswer) {
        const pageAnswer = multiPageAnswer[pageIndex];
        makeNoteFieldsNotRequired();
        const result = await fillPage(self, pageAnswer);
        results.push(result);

        if (result.errors.length > 0) {
          return {
            errors: result.errors,
            section: `page-${pageIndex}`,
            answers: pageAnswer,
          };
        }
      }

      self.form.validateAll();
      const errors = await self.getVisibleValidationErrors();
      const isComplete = self.form.pages.getCurrentIndex() === self.form.pages.$activePages.length - 1;
      const incompleteError = isComplete ? [] : [{ type: 'general', msg: 'Form is incomplete' }];

      return {
        isComplete,
        errors: [...incompleteError, ...errors],
      };
    };

    const fillPage = async (self, pageAnswer) => {
      self.log(`Answering ${pageAnswer.length} questions.`);

      const answeredQuestions = new Set();
      for (let i = 0; i < pageAnswer.length; i++) {
        const answer = pageAnswer[i];
        const $questions = getVisibleQuestions(self);
        if ($questions.length <= i) {
          return {
            errors: [{
              type: 'page',
              answers: pageAnswer,
              section: `answer-${i}`,
              msg: `Attempted to fill ${pageAnswer.length} questions, but only ${$questions.length} are visible.`,
            }],
          };
        }

        const nextUnansweredQuestion = Array.from($questions).find(question => !answeredQuestions.has(question));
        answeredQuestions.add(nextUnansweredQuestion);
        fillQuestion(nextUnansweredQuestion, answer);
      }

      const allPagesSuccessful = hasPages(window.form) ? await window.form.pages.next() : true;
      const validationErrors = await self.getVisibleValidationErrors();
      const advanceFailure = allPagesSuccessful || validationErrors.length ? [] : [{
        type: 'general',
        msg: 'Failed to advance to next page',
      }];

      return {
        errors: [...advanceFailure, ...validationErrors],
      };
    };

    const fillQuestion = (question, answer) => {
      if (!answer) {
        return;
      }

      const $question = $(question);
      const allInputs = $question.find('input:not([type="hidden"]),textarea,button');
      const firstInput = Array.from(allInputs)[0];

      if (!firstInput) {
        throw 'No input field found within question';
      }

      if (firstInput.localName === 'textarea') {
        return allInputs.val(answer).trigger('change');
      }

      switch (firstInput.type) {
      case 'button':
        // select_one appearance:minimal
        if (firstInput.className.includes('dropdown-toggle')) {
          $question.find(`input[value="${answer}"]:not([checked="checked"])`).click();
        }

        // repeate section
        else {

          if (!Number.isInteger(answer)) {
            throw `Failed to answer question which is a "+" for repeat section. This question expects an answer which is an integer - representing how many times to click the +. "${answer}"`;
          }

          for (let i = 0; i < answer; ++i) {
            allInputs.click();
          }
        }
        break;
      case 'radio':
        $question.find(`input[value="${answer}"]`).click();
        break;
      case 'date':
      case 'text':
      case 'tel':
      case 'time':
      case 'number':
        allInputs.val(answer).trigger('change');
        break;
      case 'checkbox': {
        /*
        There are two accepted formats for multi-select checkboxes
        Option 1 - A set of comma-delimited boolean strings representing the state of the boxes. eg. "true,false,true" checks the first and third box
        Option 2 - A set of comma-delimited values to be checked. eg. "heart_condition,none" checks the two boxes with corresponding values
        */
        const answerArray = Array.isArray(answer) ? answer.map(answer => answer.toString()) : answer.split(',');
        const isNonBooleanString = str => !str || !['true', 'false'].includes(str.toLowerCase());
        const answerContainsSpecificValues = answerArray.some(isNonBooleanString);

        // [value != ""] is necessary because blank lines in `choices` table of xlsx can cause empty unrendered input
        const options = $question.find('input[value!=""]');

        if (!answerContainsSpecificValues) {
          answerArray.forEach((val, index) => {
            const propValue = val === true || val.toLowerCase() === 'true' ? 'checked' : '';
            $(options[index]).prop('checked', propValue).trigger('change');
          });
        } else {
          options.prop('checked', '');
          answerArray.forEach(val => $question.find(`input[value="${val}"]`).prop('checked', 'checked').trigger('change'));
        }
        break;
      }
      default:
        throw `Unhandled input type ${firstInput.type}`;
      }
    };

    const getVisibleQuestions = form => {
      const currentPage = hasPages(form.form) ? form.form.pages.getCurrent() : form.form.pages.form.view.$;

      if (!currentPage) {
        throw Error('Form has no active pages');
      }

      if (currentPage.hasClass('question')) {
        return currentPage;
      }

      const findQuestionsInSection = section => {
        const inquisitiveChildren = Array.from($(section)
          .children(`
            section:not(.disabled,.or-appearance-hidden),
            fieldset:not(.disabled,.note,.or-appearance-hidden,.or-appearance-label,#or-calculated-items),
            label:not(.disabled,.note,.or-appearance-hidden),
            div.or-repeat-info:not(.disabled,.or-appearance-hidden):not([data-repeat-count])
          `));

        const result = [];
        for (const child of inquisitiveChildren) {
          const questions = child.localName === 'section' ? findQuestionsInSection(child) : [child];
          result.push(...questions);
        }

        return result;
      };

      return findQuestionsInSection(currentPage);
    };

    /*
    Not sure why the input element for the 'note' labels is a required field or how this
    doesn't trigger warnings in webapp. As a workaround, just update notes so that they are not
    required
    */
    function makeNoteFieldsNotRequired() {
      window.$$('label.note > input').attr('data-required', '');
    }

    const hasPages = form => form.pages.getCurrent().length > 0;

    module.exports = FormFiller;