function test(this_value, args) {
    try {
        var t = Array.prototype.reduce.apply(this_value, args);
        print(typeof t, t);
    } catch (e) {
        print(e.name);
    }
}

function adder(prev, curr, index, obj) {
    print('adder', typeof prev + ':' + prev, typeof curr + ':' + curr, typeof index, index, typeof obj);
    return prev + curr;
}

/*===
basic
TypeError
number 100
number 1
adder number:100 number:1 number 0 object
number 101
adder number:1 number:2 number 1 object
number 3
adder number:100 number:1 number 0 object
adder number:101 number:2 number 1 object
number 103
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
adder number:6 number:4 number 3 object
adder number:10 number:5 number 4 object
number 15
adder number:100 number:1 number 0 object
adder number:101 number:2 number 1 object
adder number:103 number:3 number 2 object
adder number:106 number:4 number 3 object
adder number:110 number:5 number 4 object
number 115
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
adder number:6 number:4 number 3 object
adder number:10 number:5 number 4 object
adder number:15 number:6 number 5 object
adder number:21 number:7 number 6 object
adder number:28 number:8 number 7 object
adder number:36 number:9 number 8 object
adder number:45 number:10 number 9 object
number 55
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
adder number:6 number:4 number 50 object
adder number:10 number:5 number 100 object
number 15
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
adder number:6 number:4 number 3 object
adder number:10 number:5 number 4 object
number 15
1 true
2 true
3 true
4 true
undefined undefined
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
number 6
adder undefined:undefined number:1 number 0 object
adder number:NaN number:2 number 1 object
adder number:NaN number:3 number 2 object
number NaN
===*/

print('basic');

function basicTest() {
    var obj;

    // basic combinations of different input length and presence of an
    // initial value

    test([], [adder])
    test([], [adder, 100])
    test([1], [adder])
    test([1], [adder, 100])
    test([1,2], [adder])
    test([1,2], [adder, 100])
    test([1,2,3,4,5], [adder])
    test([1,2,3,4,5], [adder, 100])

    // dense array

    test([1,2,3,4,5,6,7,8,9,10], [adder]);

    // sparse array

    obj = [1,2,3];
    obj[100] = 5;
    obj[50] = 4;
    test(obj, [adder]);

    // non-array

    obj = {
        '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, length: 5
    };
    test(obj, [adder]);

    // check callback object argument

    obj = {
        '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, length: 5
    }
    test(obj, [ function (prev, curr, index, o) {
        print(index, obj === o);
    } ]);

    // undefined initialValue is different from an initialValue not
    // given at all

    test([1,2,3], [adder]);
    test([1,2,3], [adder, undefined]);
}

try {
    basicTest();
} catch (e) {
    print(e);
}

/*===
inherited
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
string foobarquux
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
adder string:foobarquux string:baz number 3 object
string foobarquuxbaz
===*/

/* Since [[HasProperty]] and [[Get]] are used, inherited properties
 * must "show through" properly, for both arrays and objects.
 */

print('inherited');

function inheritedTest() {
    var obj;
    var proto;

    obj = [];
    obj[0] = 'foo';
    Array.prototype[1] = 'bar';
    obj[2] = 'quux';
    test(obj, [ adder ]);
    delete Array.prototype[1];

    proto = Object.create(Object.prototype);
    proto[0] = 'foo';
    proto[1] = 'bar';
    proto[3] = 'baz';
    proto.length = 5;
    obj = Object.create(proto);
    obj[2] = 'quux';  // no 'length' in obj own properties
    test(obj, [ adder ]);
}

try {
    inheritedTest();
} catch (e) {
    print(e);
}

/*===
mutation
callback foo bar 1 foo,bar,quux
callback foobar quux 2 foo,bar,quux,baz
string foobarquux
callback foo bar 1 foo,bar,quux
string foobar
callback foo bar 1 [object Object]
callback foobar quux 2 [object Object]
string foobarquux
callback foo bar 1 [object Object]
callback foobar quux 2 [object Object]
string foobarquux
callback foo bar 1 [object Object]
callback foobar quux 2 [object Object]
callback foobarquux baz 3 [object Object]
string foobarquuxbaz
callback foo bar 1 [object Object]
callback foobar baz 3 [object Object]
string foobarbaz
===*/

print('mutation');

function mutationTest() {
    var obj;

    // Mutation: adding elements to end of array after iteration starts
    // -> new elements not included

    obj = [ 'foo', 'bar', 'quux' ];
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        obj['3'] = 'baz';  // not included
        return prev + curr;
    }]);

    // Mutation: deleting an element inside the initial length range
    // -> deleted element not included in iteration

    obj = [ 'foo', 'bar', 'quux' ];
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        delete obj['2'];  // not included
        return prev + curr;
    }]);

    // Mutation: if 'length' is updated during processing, the updated
    // length is not respected.

    obj = { '0': 'foo', '1': 'bar', '2': 'quux', length: 3 };
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        obj['3'] = 'baz';
        obj.length = 4;  // not respected
        return prev + curr;
    }]);

    obj = { '0': 'foo', '1': 'bar', '2': 'quux', length: 3 };
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        obj.length = 0;  // not respected
        return prev + curr;
    }]);

    // Mutation: non-existent elements can be added and will be processed
    // as long as they are within the initial length range.

    obj = { '0': 'foo', '1': 'bar', length: 4 };
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        obj['2'] = 'quux';
        obj['3'] = 'baz';
        return prev + curr;
    }]);

    // Mutation: deleted elements are skipped.

    obj = { '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: 4 };
    test(obj, [function(prev, curr, index, obj) {
        print('callback', prev, curr, index, obj);
        delete obj['2'];
        return prev + curr;
    }]);
}

try {
    mutationTest();
} catch (e) {
    print(e);
}

/*===
coercion
TypeError
TypeError
TypeError
TypeError
TypeError
adder string:f string:o number 1 object
adder string:fo string:o number 2 object
string foo
adder number:1 number:2 number 1 object
adder number:3 number:3 number 2 object
number 6
number 123
TypeError
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
string foobarquux
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
string foobarquux
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
adder string:foobarquux string:baz number 3 object
string foobarquuxbaz
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
string foobarquux
length valueOf
adder string:foo string:bar number 1 object
adder string:foobar string:quux number 2 object
string foobarquux
TypeError
TypeError
TypeError
TypeError
TypeError
TypeError
TypeError
TypeError
TypeError
length valueOf
TypeError
length getter
0 getter
callback 0
initialValue toString()
1 getter
callback 1
2 getter
callback 2
string initial1st2nd3rd
===*/

print('coercion');

function coercionTest() {
    var obj;

    // this

    test(undefined, [ adder ]);
    test(null, [ adder ]);
    test(true, [ adder ]);
    test(false, [ adder ]);
    test(123, [ adder ]);
    test('foo', [ adder ]);
    test([1,2,3], [ adder ]);
    test({ foo: 1, bar: 2 }, [ adder, 123 ]);  // object is empty, initial value -> returned as is
    test({ foo: 1, bar: 2 }, [ adder ]);       // object is empty, no initial value -> TypeError

    // length

    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: 3.9 }, [ adder ]);
    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: 256*256*256*256 + 3.9 }, [ adder ]);  // coerces to 3
    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: -256*256*256*256 + 3.9 }, [ adder ]);  // coerces to 4
    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: '3.9' }, [ adder ]);
    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: {
        toString: function() { print('length toString'); return 4; },
        valueOf: function() { print('length valueOf'); return 3; },
    } }, [ adder ]);

    // length is not coerced if this coercion fails

    test(null, [ {
        toString: function() { print('length toString'); return 4; },
        valueOf: function() { print('length valueOf'); return 3; },
    } ]);

    // callback must be callable, otherwise TypeError (occurs after length coercion)

    test([1,2,3], [ undefined ]);
    test([1,2,3], [ null ]);
    test([1,2,3], [ true ]);
    test([1,2,3], [ false ]);
    test([1,2,3], [ 123 ]);
    test([1,2,3], [ 'foo' ]);
    test([1,2,3], [ [1,2] ]);
    test([1,2,3], [ { foo: 1, bar: 2 } ]);

    test({ '0': 'foo', '1': 'bar', '2': 'quux', '3': 'baz', length: {
        toString: function() { print('length toString'); return 4; },
        valueOf: function() { print('length valueOf'); return 3; },
    } }, [ true ]);

    // coercion order test

    obj = Object.create(Object.prototype, {
        '0': {
             get: function() { print('0 getter'); return '1st'; },
             set: function() { print('0 setter'); throw new Error('setter called') },
             enumerable: true,
             configurable: true
        },
        '1': {
             get: function() { print('1 getter'); return '2nd'; },
             set: function() { print('1 setter'); throw new Error('setter called') },
             enumerable: true,
             configurable: true
        },
        '2': {
             get: function() { print('2 getter'); return '3rd'; },
             set: function() { print('2 setter'); throw new Error('setter called') },
             enumerable: true,
             configurable: true
        },
        'length': {
             get: function() { print('length getter'); return '4.9'; },  // coerces to 4
             set: function() { print('length setter'); throw new Error('setter called') },
             enumerable: true,
             configurable: true
        },
    });

    test(obj, [
        function callback(prev, curr, index, obj) {
            print('callback', index);  // avoid side effects here
            return String(prev) + String(curr);
        },
        {
            // initial value is not coerced by the algorithm, but by the callback code
            toString: function() { print('initialValue toString()'); return 'initial'; },
            valueOf: function() { print('initialValue valueOf()'); return 'initial-notused'; }
        }
    ]);
}

try {
    coercionTest();
} catch (e) {
    print(e);
}

