object TestWizard
  components: length:Time and class:ClassName and constraints:ConstraintSet*;
	description:
  (* This is the data gathered from the user to be used in the
   * generation of a new test.
   *);
end TestWizard;

object QuestionBlock
	components: constraints:ConstraintSet and questions:TestQuestion*;
	description:
  (* A test consists of one or more blocks of questions. (When using the Simple
   * test generation interface, only one block is created.) During the creation
   * phase, a QuestionBlock does not yet contain questions, only the
   * information needed to select questions; once a test is generated, the
   * questions are populated, but the specifications are retained in case the
   * questions need to be replaced.
   *);
end QuestionBlock;

object ConstraintSet
	components: number:(NumQuestions) and time:(TimeConstraint) and
    lastUsed:(LastUsedConstraint) and
		type:(QuestionType) and length:(Length) and week:(Week);

	description:
  (* A model of all the constraints that are in effect when questions are
   * selected.
   *);
end ConstraintSet;

object TimeConstraint
	components: direction:UpperOrLower and limit:Minutes;
	description:
  (* When a test is generated, questions may be limited by an
   * upper or a lower bound on their completion time.
   *);
end TimeConstraint;

object LastUsedConstraint
	components: (direction:UpperOrLower and limit:DateLastUsed);
	description:
  (* When a test is generated, recently used questions may be filtered out, or
   * questions that have not been recently used may be filtered out.
   *);
end LastUsedConstraint;


object UpperOrLower = "Upper" or "Lower"
	description:
  (* Describes whether a limit is an upper bound or a lower bound.
   *);
end UpperOrLower;

object EditableTest
	components: length:Time and blocks:QuestionBlock* and className:ClassName
						  and numQuestions:integer;
	description:
  (* A test contains information about its length, what class it is for, and
   * what questions it consists of.
   *);
end EditableTest;

object Test
	components: length:Time and questions:TestQuestion* and className:ClassName;
	description:
  (* A test contains information about its length, what class it is for, and
   * what questions it consists of.
   *);
end Test;

object QuestionType = integer
	description:
  (* Enumeration of question types.
   *);
end QuestionType;

object TestQuestion extends Question
	components: Point and questionNumber:QuestionNumber and constraints:ConstraintSet;
	description:
	(* A question in a test knows its place in the test, the number of points it
	 * is worth, and the constraints that generated it.
	 *);
end TestQuestion;

object Point = integer
	description: (* The number of points that a question is worth on a test. *);
end;
object QuestionNumber = integer
	description: (* The question's place in a test. *);
end;
object NumQuestions = integer
	description: (* The number of questions on a test. *);
end;
object Time
	components: Hours and Minutes;
	description: (* The length of a test. *);
end Time;
object Hours = integer
	description: (* The hours place of a time. *);
end;
object Minutes = integer
	description: (* The minutes place of a time. *);
end;

function GetQuestionEditable(test:EditableTest, index:integer)->question:TestQuestion =
	(* Retrieves the question at the given index in the test. *)
	  question.questionNumber = index
	  and 
		(exists (qb in test.blocks) question in qb.questions);

function GetQuestion(test:Test, index:integer)->question:TestQuestion =
	(* Retrieves the question at the given index in the test. *)
	question.questionNumber = index
	and
	((question in test.questions));

operation MakeEditableTest(wizard:TestWizard)->t:EditableTest
	pre: (* The TestWizard object is initialized. *);
	post:
  (* The EditableTest object contains a number of QuestionBlocks equal to the number of
   * ConstraintSets in the TestWizard. Each QuestionBlock has a reference to
   * one of the ConstraintSets, and no to QuestionBlocks refer to the same
   * ConstraintSet.
   *)
	wizard.class = t.className and wizard.length = t.length
	and
	forall (fs in wizard.constraints) exists (qb in t.blocks) qb.constraints = fs
	and
	not (exists (qb in t.blocks) exists (qb' in t.blocks)
		 qb.constraints = qb'.constraints)
	and
  (* Each QuestionBlock has a number of TestQuestions equal to the number
   * specified by its ConstrainSet; these questions must meet the criteria
   * given by the ConstraintSet, and no question may be present in the test
   * more than once. A QuestionSet may contain null questions if there are
   * insufficient questions in the database.
   *)
	forall (qb in t.blocks) (
		qb.constraints.number = #(qb.questions)
		and
		forall (q in qb.questions) (
			(Passes(qb.constraints, q) and q.cl = t.className)
			or q = nil
		)
	);
	description:
  (* MakeTest extracts the data from the TestWizard to generate a EditableTest. The
   * QuestionBlocks are populated with questions and added to the EditableTest.
   *);
end MakeTestEditable;

operation MakeTest(et:EditableTest)->t:Test
	pre: ;
	post:
	et.className = t.className and et.length = t.length
	and
	forall (q in t.questions) exists (qb in et.blocks) q in qb.questions
	and
	forall (qb in et.blocks) forall (q in qb.questions) q in t.questions
	;
	description:
	(* Converts an editable test object into one suitable for administering to
	 * students.
	 *);
end;

function Passes(filters:ConstraintSet, question:TestQuestion)
		 -> result:boolean =
	(
	 (((filters.time)).direction = "Upper" and question.l <= filters.time.limit)
	 or (filters.time.direction = "Lower" and question.l >= filters.time.limit)
	 or filters.time = nil
	)
  and
  (filters.lastUsed = nil
	 or (filters.lastUsed.direction = "Upper" and question.dlu <= filters.lastUsed.limit)
	 or (filters.lastUsed.direction = "Lower" and question.dlu >= filters.lastUsed.limit)
	)
  and
  (filters.length = nil
	 or (filters.length = question.l)
	)
  and
  (filters.week = nil
	 or filters.week = question.w
	);

function Equivalent(q1:TestQuestion, q2:TestQuestion) -> result:boolean =
	  (* The two questions share the same class name, length, keywords, week,
		 * question string, author, last used date, difficulty, answer, and database
		 * number.
		 *)
	q1.cl = q2.cl and q1.l = q2.l and q1.k = q2.k and q1.w = q2.w
		 and q1.qs = q2.qs and q1.au = q2.au and q1.dlu = q2.dlu and q1.d = q2.d
		 and q1.an = q2.an and q1.dbn = q2.dbn;

operation AddQuestionEditable(test:EditableTest, question:TestQuestion, index:integer)->test':EditableTest
  (* The integer >= 1 and <= the number of questions in the test + 1. *)
	pre: index >= 1 and index <= test.numQuestions + 1
			 and question.questionNumber = index;
	post:
  (* The EditableTest contains the TestQuestion, at the index given by the integer.
   *)
	(exists (qb in test.blocks) question in qb.questions)
	and
  (* If a question already exists at that index, every question with an index
   * equal to or greater than the argument has its index incremented by one. *)
  (forall (q:TestQuestion | exists (qb in test.blocks) q in qb.questions)
		if (q.questionNumber < index) then
			(exists (qb' in test'.blocks) q in qb'.questions)
		else
      (exists (qb' in test'.blocks) (exists (q' in qb'.questions)
				 q'.questionNumber = q.questionNumber + 1)))
	;
	description: (* Adds a question to an EditableTest. *);
end AddQuestionEditable;

operation RemoveQuestionEditable(test:EditableTest, index:integer)->test':EditableTest
	pre: (* The EditableTest contains a TestQuestion with the given index. *)
	index >= 1 and index <= test.numQuestions
	;
	post:
	  (* The EditableTest no longer contains the TestQuestion that had the passed index.
		 *)
		if (exists (qb in test.blocks) (exists (q in qb.questions)
				 q.questionNumber = index)) then
			not (exists (qb' in test'.blocks) (exists (q' in qb'.questions)
				 q' = GetQuestionEditable(test, index)))
		and
		(* All TestQuestions with an index greater than the argument have their
		 * index decremented by one.
		 *)
		(forall (q:TestQuestion | exists (qb in test.blocks) q in qb.questions)
			if (q.questionNumber < index) then
				(exists (qb' in test'.blocks) q in qb'.questions)
			else
				(exists (qb' in test'.blocks) (exists (q' in qb'.questions)
					q'.questionNumber = q.questionNumber - 1)));
	description: (* Removes a question from an editable test. *);
end RemoveQuestionEditable;

operation MoveQuestionUpEditable(test:EditableTest, index:integer)->test':EditableTest
	pre: (* The integer argument is > 1 and <= the number of questions in the test. *)
		index > 1 and index <= test.numQuestions;
	post:
  (* The question and the previous question have their indeces swapped.
	 *)
	Equivalent(GetQuestionEditable(test, index), GetQuestionEditable(test', index - 1))
	and
	Equivalent(GetQuestionEditable(test, index - 1), GetQuestionEditable(test', index));
	description:
	(* Swaps the question at the given index with the question at the previous
	 * index.
	 *);
end MoveQuestionUpEditable;

operation MoveQuestionDownEditable(test:EditableTest, index:integer)->test':EditableTest
	pre:
  (* The integer argument is >= 1 and < the number of questions in the test.
   *)
		index >= 1 and index < test.numQuestions;
	post: (* The question and the next question have their indeces switched. *)
	Equivalent(GetQuestionEditable(test, index), GetQuestionEditable(test', index + 1))
	and
	Equivalent(GetQuestionEditable(test, index + 1), GetQuestionEditable(test', index));
	description:
	(* Swaps the question at the given index with the question at the next index. *);
end MoveQuestionDownEditable;

operation ReplaceQuestionEditable(test:EditableTest, index:integer)->test':EditableTest
	pre:
  (* The integer argument is >= 1 and <= the number of questions in the
	 * test.
	 *)
	index >= 1 and index <= test.numQuestions
	;
	post:
	  (* The question at the given index has been replaced with a different
		 * question that meets the criteria given by the ConstraintSet of its
		 * QuestionGroup.
		 *)
		not (exists (qb in test'.blocks) GetQuestionEditable(test, index) in qb.questions)
		and
		forall (qb in test'.blocks)
		  if (GetQuestionEditable(test', index) in qb.questions) then
				GetQuestionEditable(test', index) = nil or
			  (Passes(qb.constraints, GetQuestionEditable(test', index)) and GetQuestionEditable(test',index).cl = test.className)
			else
				true
	;
	description:
  (* The TestQuestion's QuestionBlock is found, a new question is generated
   * according to the ConstraintSet of the QuestionBlock, and this new
   * TestQuestion replaces the existing one.
   *);
end ReplaceQuestionEditable;

operation AddConstraintSet(w:TestWizard, constraints:ConstraintSet)->w':TestWizard
	pre: ;
	post: (* The TestWizard contains the QuestionBlock. *)
		constraints in w'.constraints
		and
		forall (constraints':ConstraintSet | constraints' != constraints)
			if (constraints' in w.constraints) then
				constraints' in w'.constraints
			else
				not (constraints' in w'.constraints);
	description:
  (* Adds a new block of questions to the test wizard.
   *);
end AddConstraintSet;

operation RemoveConstraintSet(w:TestWizard, cset:ConstraintSet)->w':TestWizard
	pre: (* The TestWizard contains the QuestionBlock. *)
		cset in w.constraints;
	post: (* The TestWizard no longer contains the QuestionBlock. *)
		not (cset in w'.constraints)
		and
		forall (cset':ConstraintSet | cset' != cset)
			if (cset' in w.constraints) then
				cset' in w'.constraints
			else
				not (cset' in w'.constraints);
	description:
  (* Removes a block of questions from the test wizard.
   *);
end RemoveConstraintSet;