/// Copyright (c) 2012 Ecma International.  All rights reserved. 
/// Ecma International makes this code available under the terms and conditions set
/// forth on http://hg.ecmascript.org/tests/test262/raw-file/tip/LICENSE (the 
/// "Use Terms").   Any redistribution of this code must retain the above 
/// copyright and this notice and otherwise comply with the Use Terms.

// Do not cache any JSON files - see
// https://bugs.ecmascript.org/show_bug.cgi?id=87
$.ajaxSetup( {cache:false});

/*
 * Run a test in the browser. Works by injecting an iframe with the test code.
 *
 * Public Methods:
 * * run(id, test): Runs the test specified.
 *
 * Callbacks:
 * * onComplete(test): Called when the test is run. Test object
 *                     contains result and error strings describing how the
 *                     test ran.
 */
function BrowserRunner() {
    var iframe,             // injected iframe
        currentTest,        // Current test being run.
        scriptCache = {},   // Holds the various includes required to run certain tests.
        instance    = this,
        errorDetectorFileContents,
        simpleTestAPIContents,
        globalScopeContents,
        harnessDir = "harness/";

    $.ajax({async: false,
            dataType: "text",
            success: function(data){errorDetectorFileContents = data;},
            url:harnessDir+"ed.js"});

    $.ajax({async: false,
            dataType: "text",
            success: function(data){simpleTestAPIContents = data;},
            url:harnessDir+"sta.js"});

    $.ajax({async: false,
            dataType: "text",
            success: function(data){globalScopeContents = data;},
            url:harnessDir+"gs.js"});

    /* Called by the child window to notify that the test has
     * finished. This function call is put in a separate script block
     * at the end of the page so errors in the test script block
     * should not prevent this function from being called.
     */
    function testFinished() {
        if((typeof currentTest.result) === "undefined") {
            // We didn't get a call to testRun, which likely means the
            // test failed to load.
            currentTest.result = "fail";
            currentTest.error  = "Failed to load test case (probable parse error).";
            currentTest.description = "Failed to load test case!";
        } else if((typeof currentTest.error) !== "undefined") {
            // We have an error logged from testRun.
            if(currentTest.error instanceof Test262Error) {
                currentTest.error = currentTest.message;
            } else {
            currentTest.error = currentTest.error.name + ": " + currentTest.error.message;
            }
        } else if ((typeof currentTest.error === "undefined") && (currentTest.result === "fail")) {
            currentTest.error = "Test case returned non-true value.";
        }

        document.body.removeChild(iframe);

        instance.onComplete(currentTest);
    }

    /* Called from the child window after the test has run. */
    function testRun(id, path, description, codeString, result, error) {
        currentTest.id = id;
        currentTest.path = path;
        currentTest.description = description;
        currentTest.result = result;
        currentTest.error = error;
        currentTest.code = codeString;
    }


    /* Run the test. */
    this.run = function (test, code) {
        var start = new Date();
        
        //--Detect proper window.onerror support
        if (instance.supportsWindowOnerror===undefined) {
            var iframePrereqs = document.createElement("iframe");
            iframePrereqs.setAttribute("id", "prereqsIframe");
            if (!/firefox/i.test(navigator.userAgent)) {
                iframePrereqs.setAttribute("style", "display:none");
            }
            document.body.appendChild(iframePrereqs);

            var iwinPrereqs = iframePrereqs.contentWindow; 
            var idocPrereqs = iwinPrereqs.document;
            idocPrereqs.open();
    
            iwinPrereqs.failCount = 0;
            
            var stuff = [
                         "window.onerror = function(a, b, c) { this.failCount++; }",
                         "va xyz =",
                         "throw Error();"
            ];
    
            for(var i in stuff) {
                idocPrereqs.writeln("<script type='text/javascript'>");
                idocPrereqs.writeln(stuff[i]);
                idocPrereqs.writeln("</script>");
            }
            idocPrereqs.close();
            
            //TODO - 500ms *should* be a sufficient delay
            setTimeout(function() {
                instance.supportsWindowOnerror = iwinPrereqs.failCount === 2;
                //alert(iwinPrereqs.failCount);
                document.body.removeChild(iframePrereqs);
                instance.run(test, code);
            }, 500);
            return 0; // initial config, ignore this timing.
        }
        
        currentTest = {};
        for (var tempIndex in test) {
            if (test.hasOwnProperty(tempIndex)) {
                currentTest[tempIndex] = test[tempIndex];
            }
        }
        currentTest.code = code;
              


        iframe = document.createElement("iframe");
        iframe.setAttribute("id", "runnerIframe");
        //FireFox has a defect where it doesn't fire window.onerror for an iframe if the iframe
        //is invisible.
        if (!/firefox/i.test(navigator.userAgent)) {
            iframe.setAttribute("style", "display:none");
        }
        document.body.appendChild(iframe);

        var iwin = window.frames[window.frames.length - 1];
        var idoc = iwin.document;
        idoc.open();

        // Set up some globals.
        iwin.testRun = testRun;
        iwin.testFinished = testFinished;

        //TODO: these should be moved to sta.js
        var includes = code.match(/\$INCLUDE\(([^\)]+)\)/g), // find all of the $INCLUDE statements
            include;

        if (includes !== null) {
            // We have some includes, so loop through each include and
            // pull in the dependencies.
            for (var i = 0; i < includes.length; i++) {
                include = includes[i].replace(/.*\(('|")(.*)('|")\)/, "$2");

                // First check to see if we have this script cached
                // already, and if not, grab it.
                if (typeof scriptCache[include] === "undefined") {
                    $.ajax({
                        async: false,
                        url: 'harness/' + include,
                        success: function (s) { scriptCache[include] = s; }
                    });
                }

                // Finally, write the required script to the window.
                idoc.writeln("<script type='text/javascript'>" + scriptCache[include] + "</script>");
            }
        }

        //Write out all of our helper functions
        //idoc.writeln("<script type='text/javascript' src='harness/sta.js'>" + "</script>");
        idoc.writeln("<script type='text/javascript'>");
        idoc.writeln(simpleTestAPIContents);
        idoc.writeln("</script>");

        iwin.iframeError = undefined;
        iwin.onerror = undefined;
        iwin.testDescrip = currentTest;

        //Add an error handler capable of catching so-called early errors
        //idoc.writeln("<script type='text/javascript' src='harness/ed.js'>" + "</script>");
        idoc.writeln("<script type='text/javascript'>");
        idoc.writeln(errorDetectorFileContents);
        idoc.writeln("</script>");

        //Run the code
        idoc.writeln("<script type='text/javascript'>");
        if (! instance.supportsWindowOnerror) {
            idoc.writeln("try {eval(\"" + this.convertForEval(code) + "\");} catch(e) {window.onerror(e.toString(), null, null);}");
        } else {
            idoc.writeln(code);
        }
        idoc.writeln("</script>");

        //Validate the results
        //idoc.writeln("<script type='text/javascript' src='harness/gs.js' defer>" + "</script>");
        idoc.writeln("<script type='text/javascript'>");
        idoc.writeln(globalScopeContents);
        idoc.writeln("</script>");
        idoc.close();
        
        var elapsed = new Date() - start;

        return elapsed;
    };

    //--Helper functions-------------------------------------------------------
    this.convertForEval = function(txt) {
        txt = txt.replace(/\\/g,"\\\\");
        txt = txt.replace(/\"/g,"\\\"");
        txt = txt.replace(/\'/g,"\\\'");
        txt = txt.replace(/\r/g,"\\r");
        txt = txt.replace(/\n/g,"\\n");
        return txt;
    };
}

/* Loads tests from the sections specified in testcases.json.
 * Public Methods:
 * * getNextTest() - Start loading the next test.
 * * reset() - Start over at the first test.
 *
 * Callbacks:
 * * onLoadingNextSection(path): Called after a request is sent for the next section json, with the path to that json.
 * * onInitialized(totalTests): Called after the testcases.json is loaded and parsed.
 * * onTestReady(id, code): Called when a test is ready with the
 *       test's id and code.
 * * onTestsExhausted(): Called when there are no more tests to run.
 */
function TestLoader() {
    var testGroups       = [],
        testGroupIndex   = 0,
        currentTestIndex = 0,
        loader           = this,
        mode             = "all";

    this.loadedFiles = 0;
    this.version     = undefined;
    this.date        = undefined;
    this.totalTests  = 0;
    this.runningTests = 0;

    /* Get the XML for the next section */
    function getNextXML() {
        currentTestIndex = 0;

        // already loaded this section.
        if(testGroups[testGroupIndex].status == 'loaded') {
            testGroups[testGroupIndex].onLoaded = function(){};
            loader.getNextTest();
            return;
        }
        // not loaded, so we attach a callback to come back here when the file loads.
        else {
            presenter.updateStatus("Loading file: " + testGroups[testGroupIndex].path);
            testGroups[testGroupIndex].onLoaded = getNextXML;
            
        }
    }
    
    /* Get the test list xml */
    function loadTestXML() {
        var testsListLoader = new XMLHttpRequest();

        $.ajax({url: TEST_LIST_PATH, dataType: 'json', success: function(data) {
            var testSuite = data.testSuite;

            loader.version    = data.version;
            loader.date       = data.date;
            loader.totalTests = data.numTests;

            for (var i = 0; i < testSuite.length; i++) {
                testGroups[i] = {
                    path: testSuite[i],
                    tests: [],
                    selected: false,
                    status: 'waiting',
                    onLoaded: function(){}
                };
                presenter.setTestWaiting(i, testSuite[i]);
                
                var tr = $('#chapterSelector table tr').filter(':nth-child(' + (i+1) + ')');
                tr.find('img').filter('[alt="Run"]').bind('click', {index:i}, function(event){
                    controller.runIndividualTest(event.data.index);
                });
            }
            loader.onInitialized(loader.totalTests);
            getFile();
        }});
    }
    
    /* Get the test file. Handles all the logic of figuring out the next file to load. */
    function getFile(index) {
        index = (arguments.length == 0) ? -1 : index;
        
        // Look for selected waiting chapters (priority because you'll probably want to run them soon)
        for(var i = 0; index == -1 && i < testGroups.length; i++) {
            if(testGroups[i].status == 'waiting' && testGroups[i].selected) {
                index = i;
            }
        }
        
        // Look for just chapters waiting to be loaded.
        for(var i = 0; index == -1 && i < testGroups.length; i++) {
            if(testGroups[i].status == 'waiting') {
                index = i;
            }
        }
            
        if(index == -1) {
            // Still -1? No more tests are waiting to be loaded then.
            if(controller.state == 'loading') {
                presenter.setState('loaded');
            }
            return;
        }
        
        presenter.setTestLoading(index, testGroups[index].path);
        // the only other status that should be set when we get here is 'priorityloading'
        if(testGroups[index].status == 'waiting') {
            testGroups[index].status = 'loading';
        }
        
        loader.onTestStartLoading(index, testGroups[index].path);
        // Create the AJAX call to grab the file.
        $.ajax({
            url: testGroups[index].path,
            dataType: 'json',
            // Callback with the chapter name and number of tests.
            success: function(data, status, xhr) {
                // Save the data for later usage
                testGroups[index].tests = data.testsCollection.tests;
                onTestLoaded(index, data.testsCollection.name, data.testsCollection.tests.length);
            },
            error: function(xhr, textStatus, errorThrown) {
                // TODO: Catch this error and update UI accordingly. Unlikely to happen, but errors would be 404 or 5-- errors.
                
            }
        });
    }
    
    /* Executes when a test file finishes loading. */
    function onTestLoaded(index, name, numTests) {
        presenter.setTestLoaded(index, name, numTests);

        if(testGroups[index].selected && mode == "multiple") {
            loader.runningTests += numTests;
            loader.onInitialized( loader.runningTests );
        }
        
        // The loading status is only assigned when downloading files in sequence, otherwise it
        // gets the status of priorityloading. When loading out of order, we only want to download
        // the single file, so we'll only tell it to get the next file when we see a status of
        // loading.
        if(testGroups[index].status == 'loading') {
            getFile(); // triggers downloading the next file
            testGroups[index].status = 'loaded';
        }
        else if(testGroups[index].status == 'priorityloading') {
            // Run the test
            testGroups[index].status = 'loaded';
            loader.setChapter(index);
        }
        
        testGroups[index].onLoaded();
    };
    
    function getIdFromPath (path) {
        //path is of the form "a/b/c.js"

        var id = path.split("/");
        //id is now of the form ["a", "b", "c.js"];

        id = id[id.length-1];
        //id is now of the form "c.js"

        id = id.replace(/\.js$/i, "");
        //id is now of the form "c"

        return id;
    }

    /* Move on to the next test */
    this.getNextTest = function() {
        // If the file is loaded
        if(testGroups[testGroupIndex].status == "loaded")
        {
            // And if we have tests left in this file
            if(currentTestIndex < testGroups[testGroupIndex].tests.length) {
                // Run the next test
                var test = testGroups[testGroupIndex].tests[currentTestIndex++];
                var scriptCode = test.code;
                test.id = getIdFromPath(test.path);

                loader.onTestReady(test, $.base64Decode(scriptCode));
            }
            // If there are no tests left and we aren't just running one file
            else if(testGroupIndex < testGroups.length - 1 && mode !== "one") {
                // And if we are running multiple chapters
                if(mode == "multiple") {
                    var i = testGroupIndex + 1;
                    testGroupIndex = -1;
                    for(; i < testGroups.length && testGroupIndex == -1; i++) {
                        if(testGroups[i].selected === true) {
                            testGroupIndex = i;
                        }
                    }
                    if(testGroupIndex == -1) { // we couldn't find a test we haven't run yet
                        loader.onTestsExhausted();
                        return;
                    }
                }
                // And if 
                else {
                    // We don't have tests left in this test group, so move on
                    // to the next.
                    testGroupIndex++;
                }
                getNextXML();
            }
            // 
            else {
                // We're done.
                loader.onTestsExhausted();
            }
        }
        else {
            presenter.updateStatus("Loading test file: " + testGroups[testGroupIndex].path);
            testGroups[testGroupIndex].onLoaded = getNextXML;
        }
    };

    /* Reset counters that track the next test (so we test from the beginning) */
    this.reset = function() {
        mode = "all";
        currentTestIndex = 0;
        testGroupIndex = 0;
    };
    
    /* Begin downloading test files. */
    this.startLoadingTests = function() {
        loadTestXML();
    };
    
    /* Prepare for testing a single chapter. */
    this.setChapter = function(index) {
        currentTestIndex = 0;
        testGroupIndex = index;
        mode = "one";
        
        if(testGroups[index].status == 'loaded') {
            loader.onInitialized(testGroups[index].tests.length);
        }
        else {
            testGroups[index].status = 'priorityloading';
            getFile(index);
            loader.onInitialized(0);
        }
    };
    
    /* Prepare for testing multiple chapters. Returns true if at least one chapter was selected. */
    this.setMultiple = function() {
        // Find the index of the first selection
        var firstSelectedIndex = -1;
        for(var i = 0; firstSelectedIndex == -1 && i < testGroups.length; i++) {
            if(testGroups[i].selected) {
                firstSelectedIndex = i;
            }
        }
        // If we didn't find a selected index, just quit.
        if(firstSelectedIndex == -1) {
            return false;
        }
        
        // Begin loading the file immediately, if necessary
        if(testGroups[firstSelectedIndex].status == 'waiting') {
            getFile(firstSelectedIndex);
        }
    
        mode = "multiple";
        testGroupIndex = firstSelectedIndex; // start at this chapter
        currentTestIndex = 0; // start at test 0
        
        // Count the number of tests
        runningTests = 0;
        for(var i = 0; i < testGroups.length; i++) {
            runningTests += (testGroups[i].selected && testGroups[i].status == 'loaded') ? testGroups[i].tests.length : 0;
        }
        loader.onInitialized(runningTests);
        return true;
    };
    
    this.getNumTestFiles = function() {
        return testGroups.length;
    };
    
    /* Toggle the selection of a file. */
    this.toggleSelection = function(index) {
        testGroups[index].selected = !testGroups[index].selected;
    }
    
}

/* Controls test generation and running, and sends results to the presenter. */
function Controller() {
    var state  = 'undefined';
    var runner = new BrowserRunner();
    var loader = new TestLoader();
    var controller = this;
    var startTime;
    var elapsed = 0;
    //Hook which allows browser implementers to hook their own test harness API
    //into this test framework to handle test case failures and passes in their
    //own way (e.g., logging failures to the filesystem)
    this.implementerHook = {
        //Adds a test result
        addTestResult: function (test) { },

        //Called whenever all tests have finished running.  Provided with the
        //elapsed time in milliseconds.
        finished: function(elapsed) { }
    };

    /* Executes when a test case finishes executing. */
    runner.onComplete = function(test) {
        presenter.addTestResult(test);
        try {
            controller.implementerHook.addTestResult(test);
        } catch(e) { /*no-op*/}

        if(state === 'running') {
            setTimeout(loader.getNextTest, 10);
        }
    };

    /* Executes when the loader has been initialized. */
    loader.onInitialized = function(totalTests) {
        if(arguments.length == 0) {
            totalTests = loader.totalTests;
        }
        presenter.setTotalTests(totalTests);
    };
    
    /* Executes when a test file starts loading. */
    loader.onTestStartLoading = function(index, path) {
        presenter.setTestLoading(index, path);
    }

    /* Executes when a test is ready to run. */
    loader.onTestReady = function(testObj, testSrc) {
        presenter.updateStatus("Running Test: " + testObj.id);
        elapsed += runner.run(testObj, testSrc);
    };

    /* Executes when there are no more tests to run. */
    loader.onTestsExhausted = function() {
        elapsed = elapsed/(1000*60);  //minutes
        elapsed = elapsed.toFixed(3);
        
        state = (loader.loadedFiles == loader.getNumTestFiles()) ? 'loaded' : 'loading';
        presenter.setState(state);
        presenter.finished(elapsed);
        try {
            controller.implementerHook.finished(elapsed);
        } catch(e) { /*no-op*/}
    };
    
    /* Start the test execution. */
    this.start = function() {
        elapsed = 0;
        state = 'running';
        presenter.setState(state);
        loader.getNextTest();
    };

    /* Pause the test execution. */
    this.pause = function() {
        state = 'paused';
        presenter.setState(state);
    };

    /* Reset the testing status. */
    this.reset = function() {
        loader.onInitialized();
        loader.reset();
        presenter.reset();
        
        state = (loader.loadedFiles == loader.getNumTestFiles()) ? 'loaded' : 'loading';
        presenter.setState(state);
    };
    
    /* Start loading tests. */
    this.startLoadingTests = function() {
        state = 'loading';
        presenter.setState(state);
        loader.startLoadingTests();
    }
    
    /* Set the individual chapter in the laoder and start the controller. */
    this.runIndividualTest = function(index) {
        controller.reset();
        loader.setChapter(index);
        controller.start();
    }
    
    /* Compile a list of the selected tests and start the controller. */
    this.runSelected = function() {
        controller.reset();
        if(loader.setMultiple()) {
            controller.start();
        }
    }
    
    this.runAll = function() {
        controller.reset();
        controller.start();
    }
    
    this.toggleSelection = function(index) {
        loader.toggleSelection(index);
    }
}

var controller = new Controller();

/* Helper function which shows if we're in the 'debug' mode of the Test262 site.
   This mode is only useful for debugging issues with the test harness and
   website. */
function isSiteDebugMode() {
    var str=window.location.href.substring(window.location.href.indexOf("?")+1);
    if(str.indexOf("sitedebug") > -1) {
        return true;
    }
    else {
        return false;
    }
}


$(function () {
    presenter.setup();
    $('.content-home').show();
    // Adding attribute to the tabs (e.g. Home, Run etc.) and
    // attaching the click event on buttons (e.g. Reset, Start etc.)
    $('.nav-link').each(function (index) {
        //Adding "targetDiv" attribute to the header tab and on that
        //basis the div related to header tabs are displayed
        if (index === 0) {
            $(this).attr('targetDiv', '.content-home');
        } else if (index === 1) {
            $(this).attr('targetDiv', '.content-tests');
        } else if (index === 2) {
            $(this).attr('targetDiv', '.content-results');
            $(this).attr('testRunning', 'false');
        } else if (index === 3) {
            $(this).attr('targetDiv', '.content-dev');
        }
        else {
            $(this).attr('targetDiv', '.content-browsers');
        }

        //Attaching the click event to the header tab that shows the
        //respective div of header
        $(this).click(function () {
            var target = $(this).attr('targetDiv');
            $('#contentContainer > div:visible').hide();
            $('.navBar .selected').toggleClass('selected');
            $(this).addClass('selected');
            $(target).show();

            //If clicked tab is Result, it generates the results.
            if ($(target).hasClass('content-results')) {
                presenter.refresh();
            }
        });
    });

    // Attach click events to all the control buttons.
    $('#btnRunAll').click(controller.runAll);
    $('#btnReset').click(controller.reset);
    $('#btnRunSelected').click(controller.runSelected);
    $('#btnPause').click(controller.pause);
    $('#btnResume').click(controller.start);

    var SUITE_DESCRIP_PATH = "json/suiteDescrip.json";
    $.ajax({ url: SUITE_DESCRIP_PATH, dataType: 'json', success: function (data) {
        presenter.setVersion(data.version);
        presenter.setDate(data.date);
    }
    });
    
    // Start loading the files right away.
    controller.startLoadingTests();

});
