<?php
if (!defined('MOODLE_INTERNAL')) {
    die('Direct access to this script is forbidden.');
}
require_once($CFG->libdir.'/completionlib.php');

global $DB;
Mock::generate(get_class($DB), 'mock_database');

Mock::generatePartial('completion_info','completion_cutdown',
    array('delete_all_state','get_tracked_users','update_state',
        'internal_get_grade_state','is_enabled','get_data','internal_get_state','internal_set_data'));
Mock::generatePartial('completion_info','completion_cutdown2',
    array('is_enabled','get_data','internal_get_state','internal_set_data'));
Mock::generatePartial('completion_info','completion_cutdown3',
    array('internal_get_grade_state'));

class fake_recordset implements Iterator {
    var $closed;
    var $values,$index;

    function fake_recordset($values) {
        $this->values=$values;
        $this->index=0;
    }

    function current() {
        return $this->values[$this->index];
    }

    function key() {
        return $this->values[$this->index];
    }

    function next() {
        $this->index++;
    }

    function rewind() {
        $this->index=0;
    }

    function valid() {
        return count($this->values) > $this->index;
    }

    function close() {
        $closed=true;
    }

    function was_closed() {
        return $closed;
    }
}

/**
 * Expectation that checks an object for given values (normal equality test)
 * plus a 'timemodified' field that is current (last second or two).
 */
class TimeModifiedExpectation extends SimpleExpectation {
    private $otherfields;

    /**
     * @param array $otherfields Array key=>value of required object fields
     */
    function TimeModifiedExpectation($otherfields) {
        $this->otherfields=$otherfields;
    }

    function test($thing) {
        $thingfields=(array)$thing;
        foreach($this->otherfields as $key=>$value) {
            if(!array_key_exists($key,$thingfields)) {
                return false;
            }
            if($thingfields[$key]!=$value) {
                return false;
            }
        }

        $timedifference=time()-$thing->timemodified;
        return ($timedifference < 2 && $timedifference>=0);
    }

    function testMessage($thing) {
        return "Object does not match fields/time requirement";
    }
}

class completionlib_test extends UnitTestCaseUsingDatabase {

    public static $includecoverage = array('lib/completionlib.php');
    var $realdb,$realcfg,$realsession,$realuser;

    function setUp() {
        global $DB,$CFG,$SESSION,$USER;
        $this->realdb=$DB;
        $this->realcfg=$CFG;
        $this->realsession=$SESSION;
        $this->prevuser=$USER;
        $DB=new mock_database();
        $CFG=clone($this->realcfg);
        $CFG->prefix='test_';
        $CFG->enablecompletion=COMPLETION_ENABLED;
        $SESSION=new stdClass();
        $USER=(object)array('id'=>314159);
    }

    function tearDown() {
        global $DB,$CFG,$SESSION,$USER;
        $DB=$this->realdb;
        $CFG=$this->realcfg;
        $SESSION=$this->realsession;
        $USER=$this->prevuser;
    }

    function test_is_enabled() {
        global $CFG;

        // Config alone
        $CFG->enablecompletion=COMPLETION_DISABLED;
        $this->assertEqual(COMPLETION_DISABLED,completion_info::is_enabled_for_site());
        $CFG->enablecompletion=COMPLETION_ENABLED;
        $this->assertEqual(COMPLETION_ENABLED,completion_info::is_enabled_for_site());

        // Course
        //$course=new stdClass;
        $course=(object)array('id'=>13);
        $c=new completion_info($course);
        $course->enablecompletion=COMPLETION_DISABLED;
        $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());
        $course->enablecompletion=COMPLETION_ENABLED;
        $this->assertEqual(COMPLETION_ENABLED,$c->is_enabled());
        $CFG->enablecompletion=COMPLETION_DISABLED;
        $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());

        // Course and CM
        $cm=new stdClass;
        $cm->completion=COMPLETION_TRACKING_MANUAL;
        $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
        $CFG->enablecompletion=COMPLETION_ENABLED;
        $course->enablecompletion=COMPLETION_DISABLED;
        $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
        $course->enablecompletion=COMPLETION_ENABLED;
        $this->assertEqual(COMPLETION_TRACKING_MANUAL,$c->is_enabled($cm));
        $cm->completion=COMPLETION_TRACKING_NONE;
        $this->assertEqual(COMPLETION_TRACKING_NONE,$c->is_enabled($cm));
        $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
        $this->assertEqual(COMPLETION_TRACKING_AUTOMATIC,$c->is_enabled($cm));
    }

    function test_update_state() {
        $c=new completion_cutdown2();
        $c->__construct((object)array('id'=>42));
        $cm=(object)array('id'=>13,'course'=>42);

        // Not enabled, should do nothing
        $c->expectAt(0,'is_enabled',array($cm));
        $c->setReturnValueAt(0,'is_enabled',false);
        $c->update_state($cm);

        // Enabled, but current state is same as possible result, do nothing
        $current=(object)array('completionstate'=>COMPLETION_COMPLETE);
        $c->expectAt(1,'is_enabled',array($cm));
        $c->setReturnValueAt(1,'is_enabled',true);

        $c->expectAt(0,'get_data',array($cm,false,0));
        $c->setReturnValueAt(0,'get_data',$current);
        $c->update_state($cm,COMPLETION_COMPLETE);

        // Enabled, but current state is a specific one and new state is just
        // omplete, so do nothing
        $current->completionstate=COMPLETION_COMPLETE_PASS;
        $c->expectAt(2,'is_enabled',array($cm));
        $c->setReturnValueAt(2,'is_enabled',true);
        $c->expectAt(1,'get_data',array($cm,false,0));
        $c->setReturnValueAt(1,'get_data',$current);
        $c->update_state($cm,COMPLETION_COMPLETE);

        // Manual, change state (no change)
        $cm->completion=COMPLETION_TRACKING_MANUAL;
        $current->completionstate=COMPLETION_COMPLETE;
        $c->expectAt(3,'is_enabled',array($cm));
        $c->setReturnValueAt(3,'is_enabled',true);
        $c->expectAt(2,'get_data',array($cm,false,0));
        $c->setReturnValueAt(2,'get_data',$current);
        $c->update_state($cm,COMPLETION_COMPLETE);

        // Manual, change state (change)
        $c->expectAt(4,'is_enabled',array($cm));
        $c->setReturnValueAt(4,'is_enabled',true);
        $c->expectAt(3,'get_data',array($cm,false,0));
        $c->setReturnValueAt(3,'get_data',$current);
        $c->expectAt(0,'internal_set_data',array($cm,
            new TimeModifiedExpectation(array('completionstate'=>COMPLETION_INCOMPLETE))));
        $c->update_state($cm,COMPLETION_INCOMPLETE);

        // Auto, change state
        $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
        $c->expectAt(5,'is_enabled',array($cm));
        $c->setReturnValueAt(5,'is_enabled',true);
        $c->expectAt(4,'get_data',array($cm,false,0));
        $c->setReturnValueAt(4,'get_data',$current);
        $c->expectAt(0,'internal_get_state',array($cm,0,$current));
        $c->setReturnValueAt(0,'internal_get_state',COMPLETION_COMPLETE_PASS);
        $c->expectAt(1,'internal_set_data',array($cm,
            new TimeModifiedExpectation(array('completionstate'=>COMPLETION_COMPLETE_PASS))));
        $c->update_state($cm,COMPLETION_COMPLETE_PASS);

        $c->tally();
    }

    function test_internal_get_state() {
        global $DB;

        $c=new completion_cutdown3();
        $c->__construct((object)array('id'=>42));
        $cm=(object)array('id'=>13,'course'=>42,'completiongradeitemnumber'=>null);

        // If view is required, but they haven't viewed it yet
        $cm->completionview=COMPLETION_VIEW_REQUIRED;
        $current=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
        $this->assertEqual(COMPLETION_INCOMPLETE,$c->internal_get_state($cm,123,$current));

        // OK set view not required
        $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;

        // Test not getting module name
        $cm->modname='label';
        $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));

        // Test getting module name
        $cm->module=13;
        unset($cm->modname);
        $DB->expectOnce('get_field',array('modules','name',array('id'=>13)));
        $DB->setReturnValue('get_field','label');
        $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));

        // Note: This function is not fully tested (including kind of the main
        // part) because:
        // * the grade_item/grade_grade calls are static and can't be mocked
        // * the plugin_supports call is static and can't be mocked

        $DB->tally();
        $c->tally();
    }

    function test_set_module_viewed() {
        $c=new completion_cutdown();
        $c->__construct((object)array('id'=>42));
        $cm=(object)array('id'=>13,'course'=>42);

        // Not tracking completion, should do nothing
        $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;
        $c->set_module_viewed($cm);

        // Tracking completion but completion is disabled, should do nothing
        $cm->completionview=COMPLETION_VIEW_REQUIRED;
        $c->expectAt(0,'is_enabled',array($cm));
        $c->setReturnValueAt(0,'is_enabled',false);
        $c->set_module_viewed($cm);

        // Now it's enabled, we expect it to get data. If data already has
        // viewed, still do nothing
        $c->expectAt(1,'is_enabled',array($cm));
        $c->setReturnValueAt(1,'is_enabled',true);
        $c->expectAt(0,'get_data',array($cm,0));
        $hasviewed=(object)array('viewed'=>COMPLETION_VIEWED);
        $c->setReturnValueAt(0,'get_data',$hasviewed);
        $c->set_module_viewed($cm);

        // OK finally one that hasn't been viewed, now it should set it viewed
        // and update state
        $c->expectAt(2,'is_enabled',array($cm));
        $c->setReturnValueAt(2,'is_enabled',true);
        $notviewed=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
        $c->expectAt(1,'get_data',array($cm,1337));
        $c->setReturnValueAt(1,'get_data',$notviewed);
        $c->expectOnce('internal_set_data',array($cm,$hasviewed));
        $c->expectOnce('update_state',array($cm,COMPLETION_COMPLETE,1337));
        $c->set_module_viewed($cm,1337);

        $c->tally();
    }

    function test_count_user_data() {
        global $DB;
        $course=(object)array('id'=>13);
        $cm=(object)array('id'=>42);
        $DB->setReturnValue('get_field_sql',666);
        $DB->expectOnce('get_field_sql',array(new IgnoreWhitespaceExpectation("SELECT
    COUNT(1)
FROM
    {course_modules_completion}
WHERE
    coursemoduleid=? AND completionstate<>0"),array(42)));
        $c=new completion_info($course);
        $this->assertEqual(666,$c->count_user_data($cm));

        $DB->tally();
    }

    function test_delete_all_state() {
        global $DB,$SESSION;
        $course=(object)array('id'=>13);
        $cm=(object)array('id'=>42,'course'=>13);
        $c=new completion_info($course);
        // Check it works ok without data in session
        $DB->expectAt(0,'delete_records',
            array('course_modules_completion',array('coursemoduleid'=>42)));
        $c->delete_all_state($cm);

        // Build up a session to check it deletes the right bits from it
        // (and not other bits)
        $SESSION->completioncache=array();
        $SESSION->completioncache[13]=array();
        $SESSION->completioncache[13][42]='foo';
        $SESSION->completioncache[13][43]='foo';
        $SESSION->completioncache[14]=array();
        $SESSION->completioncache[14][42]='foo';
        $DB->expectAt(1,'delete_records',
            array('course_modules_completion',array('coursemoduleid'=>42)));
        $c->delete_all_state($cm);
        $this->assertEqual(array(13=>array(43=>'foo'),14=>array(42=>'foo')),
            $SESSION->completioncache);

        $DB->tally();
    }

    function test_reset_all_state() {
        global $DB;
        $c=new completion_cutdown();
        $c->__construct((object)array('id'=>42));

        $cm=(object)array('id'=>13,'course'=>42,
            'completion'=>COMPLETION_TRACKING_AUTOMATIC);

        $DB->setReturnValue('get_recordset',new fake_recordset(array(
            (object)array('id'=>1,'userid'=>100),
            (object)array('id'=>2,'userid'=>101),
        )));
        $DB->expectOnce('get_recordset',array('course_modules_completion',
            array('coursemoduleid'=>13),'','userid'));
        $c->expectOnce('delete_all_state',array($cm));
        $c->expectOnce('get_tracked_users',array());
        $c->setReturnValue('get_tracked_users',array(
            (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
            (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
            ));

        $c->expectAt(0,'update_state',array($cm,COMPLETION_UNKNOWN,100));
        $c->expectAt(1,'update_state',array($cm,COMPLETION_UNKNOWN,101));
        $c->expectAt(2,'update_state',array($cm,COMPLETION_UNKNOWN,201));

        $c->reset_all_state($cm);

        $DB->tally();
        $c->tally();
    }

    function test_get_data() {
        global $DB,$SESSION;

        $c=new completion_info((object)array('id'=>42));
        $cm=(object)array('id'=>13,'course'=>42);

        // 1. Not current user, record exists
        $sillyrecord=(object)array('frog'=>'kermit');
        $DB->expectAt(0,'get_record',array('course_modules_completion',
            array('coursemoduleid'=>13,'userid'=>123)));
        $DB->setReturnValueAt(0,'get_record',$sillyrecord);
        $result=$c->get_data($cm,false,123);
        $this->assertEqual($sillyrecord,$result);
        $this->assertTrue(empty($SESSION->completioncache));

        // 2. Not current user, default record, wholecourse (ignored)
        $DB->expectAt(1,'get_record',array('course_modules_completion',
            array('coursemoduleid'=>13,'userid'=>123)));
        $DB->setReturnValueAt(1,'get_record',false);
        $result=$c->get_data($cm,true,123);
        $this->assertEqual((object)array(
            'id'=>'0','coursemoduleid'=>13,'userid'=>123,'completionstate'=>0,
            'viewed'=>0,'timemodified'=>0),$result);
        $this->assertTrue(empty($SESSION->completioncache));

        // 3. Current user, single record, not from cache
        $DB->expectAt(2,'get_record',array('course_modules_completion',
            array('coursemoduleid'=>13,'userid'=>314159)));
        $DB->setReturnValueAt(2,'get_record',$sillyrecord);
        $result=$c->get_data($cm);
        $this->assertEqual($sillyrecord,$result);
        $this->assertEqual($sillyrecord,$SESSION->completioncache[42][13]);
        // When checking time(), allow for second overlaps
        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);

        // 4. Current user, 'whole course', but from cache
        $result=$c->get_data($cm,true);
        $this->assertEqual($sillyrecord,$result);

        // 5. Current user, single record, cache expired
        $SESSION->completioncache[42]['updated']=37; // Quite a long time ago
        $now=time();
        $SESSION->completioncache[17]['updated']=$now;
        $SESSION->completioncache[39]['updated']=72; // Also a long time ago
        $DB->expectAt(3,'get_record',array('course_modules_completion',
            array('coursemoduleid'=>13,'userid'=>314159)));
        $DB->setReturnValueAt(3,'get_record',$sillyrecord);
        $result=$c->get_data($cm,false);
        $this->assertEqual($sillyrecord,$result);
        // Check that updated value is right, then fudge it to make next compare
        // work
        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
        $SESSION->completioncache[42]['updated']=$now;
        // Check things got expired from cache
        $this->assertEqual(array(42=>array(13=>$sillyrecord,'updated'=>$now),
            17=>array('updated'=>$now)),$SESSION->completioncache);

        // 6. Current user, 'whole course' and record not in cache
        unset($SESSION->completioncache);

        // Scenario: Completion data exists for one CMid
        $basicrecord=(object)array('coursemoduleid'=>13);
        $DB->setReturnValueAt(0,'get_records_sql',array(
            1=>$basicrecord
        ));
        $DB->expectAt(0,'get_records_sql',array(new IgnoreWhitespaceExpectation("
SELECT
    cmc.*
FROM
    {course_modules} cm
    INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id
WHERE
    cm.course=? AND cmc.userid=?"),array(42,314159)));

        // There are two CMids in total, the one we had data for and another one
        $modinfo->cms=array((object)array('id'=>13),(object)array('id'=>14));
        $result=$c->get_data($cm,true,0,$modinfo);

        // Check result
        $this->assertEqual($basicrecord,$result);

        // Check the cache contents
        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
        $SESSION->completioncache[42]['updated']=$now;
        $this->assertEqual(array(42=>array(13=>$basicrecord,14=>(object)array(
            'id'=>'0','coursemoduleid'=>14,'userid'=>314159,'completionstate'=>0,
            'viewed'=>0,'timemodified'=>0),'updated'=>$now)),$SESSION->completioncache);

        $DB->tally();
    }

    function test_internal_set_data() {
        global $DB,$SESSION;

        $cm=(object)array('course'=>42,'id'=>13);
        $c=new completion_info((object)array('id'=>42));

        // 1) Test with new data
        $data=(object)array('id'=>0,'userid'=>314159);
        $DB->setReturnValueAt(0,'insert_record',4);
        $DB->expectAt(0,'insert_record',array('course_modules_completion',$data));
        $c->internal_set_data($cm,$data);
        $this->assertEqual(4,$data->id);
        $this->assertEqual(array(42=>array(13=>$data)),$SESSION->completioncache);

        // 2) Test with existing data and for different user (not cached)
        unset($SESSION->completioncache);
        $d2=(object)array('id'=>7,'userid'=>17);
        $DB->expectAt(0,'update_record',array('course_modules_completion',$d2));
        $c->internal_set_data($cm,$d2);
        $this->assertFalse(isset($SESSION->completioncache));

        $DB->tally();
    }

    function test_get_activities() {
        global $DB;

        $c=new completion_info((object)array('id'=>42));

        // Try with no activities
        $DB->expectAt(0,'get_records_select',array('course_modules',
              'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
        $DB->setReturnValueAt(0,'get_records_select',array());
        $result=$c->get_activities();
        $this->assertEqual(array(),$result);

        // Try with an activity (need to fake up modinfo for it as well)
        $DB->expectAt(1,'get_records_select',array('course_modules',
              'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
        $DB->setReturnValueAt(1,'get_records_select',array(
            13=>(object)array('id'=>13)
        ));
        $modinfo=new stdClass;
        $modinfo->sections=array(array(1,2,3),array(12,13,14));
        $modinfo->cms[13]=(object)array('modname'=>'frog','name'=>'kermit');
        $result=$c->get_activities($modinfo);
        $this->assertEqual(array(13=>(object)array('id'=>13,'modname'=>'frog','name'=>'kermit')),$result);

        $DB->tally();
    }

    // get_tracked_users() cannot easily be tested because it uses
    // get_role_users, so skipping that

    function test_get_progress_all() {
        global $DB;

        $c=new completion_cutdown();
        $c->__construct((object)array('id'=>42));

        // 1) Basic usage
        $c->expectAt(0,'get_tracked_users',array(false, array(), 0, '', '', ''));
        $c->setReturnValueAt(0,'get_tracked_users',array(
            (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
            (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
            ));
        $DB->expectAt(0,'get_in_or_equal',array(array(100,201)));
        $DB->setReturnValueAt(0,'get_in_or_equal',array(' IN (100,201)',array()));
        $DB->expectAt(0,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
SELECT
    cmc.*
FROM
    {course_modules} cm
    INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
WHERE
    cm.course=? AND cmc.userid IN (100,201)"),array(42)));
        $progress1=(object)array('userid'=>100,'coursemoduleid'=>13);
        $progress2=(object)array('userid'=>201,'coursemoduleid'=>14);
        $DB->setReturnValueAt(0,'get_recordset_sql',new fake_recordset(array(
            $progress1,$progress2
        )));
        $this->assertEqual(array(

                100 => (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh',
                    'progress'=>array(13=>$progress1)),
                201 => (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy',
                    'progress'=>array(14=>$progress2)),
            ),$c->get_progress_all(false));

        // 2) With more than 1,000 results
        $c->expectAt(1,'get_tracked_users',array(true, 3, 0, '', '', ''));

        $tracked=array();
        $ids=array();
        $progress=array();
        for($i=100;$i<2000;$i++) {
            $tracked[]=(object)array('id'=>$i,'firstname'=>'frog','lastname'=>$i);
            $ids[]=$i;
            $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>13);
            $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>14);
        }
        $c->setReturnValueAt(1,'get_tracked_users',$tracked);

        $DB->expectAt(1,'get_in_or_equal',array(array_slice($ids,0,1000)));
        $DB->setReturnValueAt(1,'get_in_or_equal',array(' IN whatever',array()));
        $DB->expectAt(1,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
SELECT
    cmc.*
FROM
    {course_modules} cm
    INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
WHERE
    cm.course=? AND cmc.userid IN whatever"),array(42)));
        $DB->setReturnValueAt(1,'get_recordset_sql',new fake_recordset(array_slice($progress,0,1000)));
        $DB->expectAt(2,'get_in_or_equal',array(array_slice($ids,1000)));
        $DB->setReturnValueAt(2,'get_in_or_equal',array(' IN whatever2',array()));
        $DB->expectAt(2,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
SELECT
    cmc.*
FROM
    {course_modules} cm
    INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
WHERE
    cm.course=? AND cmc.userid IN whatever2"),array(42)));
        $DB->setReturnValueAt(2,'get_recordset_sql',new fake_recordset(array_slice($progress,1000)));
        $result=$c->get_progress_all(true,3);
        $resultok=true;
        $resultok = $resultok && ($ids==array_keys($result));

        foreach($result as $userid => $data) {
            $resultok = $resultok && $data->firstname=='frog';
            $resultok = $resultok && $data->lastname==$userid;
            $resultok = $resultok && $data->id==$userid;
            $cms=$data->progress;
            $resultok= $resultok && (array(13,14)==array_keys($cms));
            $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>13)==$cms[13]);
            $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>14)==$cms[14]);
        }
        $this->assertTrue($resultok);

        $DB->tally();
        $c->tally();
    }

    function test_inform_grade_changed() {
        $c=new completion_cutdown();
        $c->__construct((object)array('id'=>42));

        $cm=(object)array('course'=>42,'id'=>13,'completion'=>0,'completiongradeitemnumber'=>null);
        $item=(object)array('itemnumber'=>3);
        $grade=(object)array('userid'=>31337);

        // Not enabled (should do nothing)
        $c->setReturnValueAt(0,'is_enabled',false);
        $c->expectAt(0,'is_enabled',array($cm));
        $c->inform_grade_changed($cm,$item,$grade,false);

        // Enabled but still no grade completion required, should still do nothing
        $c->setReturnValueAt(1,'is_enabled',true);
        $c->expectAt(1,'is_enabled',array($cm));
        $c->inform_grade_changed($cm,$item,$grade,false);

        // Enabled and completion required but item number is wrong, does nothing
        $cm->completiongradeitemnumber=7;
        $c->setReturnValueAt(2,'is_enabled',true);
        $c->expectAt(2,'is_enabled',array($cm));
        $c->inform_grade_changed($cm,$item,$grade,false);

        // Enabled and completion required and item number right. It is supposed
        // to call update_state with the new potential state being obtained from
        // internal_get_grade_state.
        $cm->completiongradeitemnumber=3;
        $c->setReturnValueAt(3,'is_enabled',true);
        $c->expectAt(3,'is_enabled',array($cm));
        $c->expectAt(0,'internal_get_grade_state',array($item,$grade));
        $c->setReturnValueAt(0,'internal_get_grade_state',COMPLETION_COMPLETE_PASS);
        $c->expectAt(0,'update_state',array($cm,COMPLETION_COMPLETE_PASS,31337));
        $c->inform_grade_changed($cm,$item,$grade,false);

        // Same as above but marked deleted. It is supposed to call update_state
        // with new potential state being COMPLETION_INCOMPLETE
        $c->setReturnValueAt(4,'is_enabled',false);
        $c->expectAt(4,'is_enabled',array($cm));
        $c->expectAt(1,'update_state',array($cm,COMPLETION_INCOMPLETE,31337));
        $c->inform_grade_changed($cm,$item,$grade,false);

        $c->tally();
    }

    function test_internal_get_grade_state() {
        $item=new stdClass;
        $grade=new stdClass;

        $item->gradepass=4;
        $item->hidden=0;
        $grade->rawgrade=4.0;
        $grade->finalgrade=null;

        // Grade has pass mark and is not hidden, user passes
        $this->assertEqual(
            COMPLETION_COMPLETE_PASS,
            completion_info::internal_get_grade_state($item,$grade));

        // Same but user fails
        $grade->rawgrade=3.9;
        $this->assertEqual(
            COMPLETION_COMPLETE_FAIL,
            completion_info::internal_get_grade_state($item,$grade));

        // User fails on raw grade but passes on final
        $grade->finalgrade=4.0;
        $this->assertEqual(
            COMPLETION_COMPLETE_PASS,
            completion_info::internal_get_grade_state($item,$grade));

        // Item is hidden
        $item->hidden=1;
        $this->assertEqual(
            COMPLETION_COMPLETE,
            completion_info::internal_get_grade_state($item,$grade));

        // Item isn't hidden but has no pass mark
        $item->hidden=0;
        $item->gradepass=0;
        $this->assertEqual(
            COMPLETION_COMPLETE,
            completion_info::internal_get_grade_state($item,$grade));
    }
}

