/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

function checkDataProperty(object, propertyKey, value, writable, enumerable, configurable) {
    var desc = Object.getOwnPropertyDescriptor(object, propertyKey);
    assertEq(desc === undefined, false);
    assertEq('value' in desc, true);
    assertEq(desc.value, value);
    assertEq(desc.writable, writable);
    assertEq(desc.enumerable, enumerable);
    assertEq(desc.configurable, configurable);
}

/* 19.1.2.1  Object.assign ( target, ...sources ) */
assertEq(Object.assign.length, 2);

// Basic functionality works with multiple sources
function basicMultipleSources() {
    var a = {};
    var b = { bProp : 7 };
    var c = { cProp : 8 };
    Object.assign(a, b, c);
    assertEq(a.bProp, 7);
    assertEq(a.cProp, 8);
}
basicMultipleSources();

// Basic functionality works with symbols (Bug 1052358)
function basicSymbols() {
    var a = {};
    var b = { bProp : 7 };
    var aSymbol = Symbol("aSymbol");
    b[aSymbol] = 22;
    Object.assign(a, b);
    assertEq(a.bProp, 7);
    assertEq(a[aSymbol], 22);
}
basicSymbols();

// Calls ToObject() for target, skips null/undefined sources, uses
// ToObject(source) otherwise.
function testToObject() {
    assertThrowsInstanceOf(() => Object.assign(null, null), TypeError);
    assertThrowsInstanceOf(() => Object.assign(), TypeError);
    assertThrowsInstanceOf(() => Object.assign(null, {}), TypeError);
    assertEq(Object.assign({}, null) instanceof Object, true);
    assertEq(Object.assign({}, undefined) instanceof Object, true);

    // Technically an embedding could have this as extension acting differently
    // from ours, so a feature-test is inadequate.  We can move this subtest
    // into extensions/ if that ever matters.
    if (typeof objectEmulatingUndefined === "function") {
        var falsyObject = objectEmulatingUndefined();
        falsyObject.foo = 7;

        var obj = Object.assign({}, falsyObject);
        assertEq(obj instanceof Object, true);
        assertEq(obj.foo, 7);
    }

    assertEq(Object.assign(true, {}) instanceof Boolean, true);
    assertEq(Object.assign(1, {}) instanceof Number, true);
    assertEq(Object.assign("string", {}) instanceof String, true);
    var o = {};
    assertEq(Object.assign(o, {}), o);
}
testToObject();

// Invokes [[OwnPropertyKeys]] on ToObject(source)
function testOwnPropertyKeys() {
    assertThrowsInstanceOf(() => Object.assign(null, new Proxy({}, {
        getOwnPropertyNames: () => { throw new Error("not called"); }
    })), TypeError);

    var ownKeysCalled = false;
    Object.assign({}, new Proxy({}, {
        ownKeys: function() {
            ownKeysCalled = true;
            return [];
        }
    }));
    assertEq(ownKeysCalled, true);
};
testOwnPropertyKeys();

// Ensure correct property traversal
function correctPropertyTraversal() {
    var log = "";
    var source = new Proxy({a: 1, b: 2}, {
        ownKeys: () => ["b", "c", "a"],
        getOwnPropertyDescriptor: function(t, pk) {
            log += "#" + pk;
            return Object.getOwnPropertyDescriptor(t, pk);
        },
        get: function(t, pk, r) {
            log += "-" + pk;
            return t[pk];
        },
    });
    Object.assign({}, source);
    assertEq(log, "#b-b#c#a-a");
}
correctPropertyTraversal();

// Only [[Enumerable]] properties are assigned to target
function onlyEnumerablePropertiesAssigned() {
    var source = Object.defineProperties({}, {
        a: {value: 1, enumerable: true},
        b: {value: 2, enumerable: false},
    });
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, false);
}
onlyEnumerablePropertiesAssigned();


// Enumerability is decided on-time, not before main loop (1)
function testEnumerabilityDeterminedInLoop1()
{
    var getterCalled = false;
    var sourceTarget = {
        get a() { getterCalled = true },
        get b() { Object.defineProperty(sourceTarget, "a", {enumerable: false}) },
    };
    var source = new Proxy(sourceTarget, { ownKeys: () => ["b", "a"] });
    Object.assign({}, source);
    assertEq(getterCalled, false);
}
testEnumerabilityDeterminedInLoop1();

// Enumerability is decided on-time, not before main loop (2)
function testEnumerabilityDeterminedInLoop2()
{
    var getterCalled = false;
    var sourceTarget = {
        get a() { getterCalled = true },
        get b() { Object.defineProperty(sourceTarget, "a", {enumerable: true}) },
    };
    var source = new Proxy(sourceTarget, {
        ownKeys: () => ["b", "a"]
    });
    Object.defineProperty(sourceTarget, "a", {enumerable: false});
    Object.assign({}, source);
    assertEq(getterCalled, true);
}
testEnumerabilityDeterminedInLoop2();

// Properties are retrieved through Get() and assigned onto
// the target as data properties, not in any sense cloned over as descriptors
function testPropertiesRetrievedThroughGet() {
    var getterCalled = false;
    Object.assign({}, {get a() { getterCalled = true }});
    assertEq(getterCalled, true);
}
testPropertiesRetrievedThroughGet();

// Properties are retrieved through Get()
// Properties are assigned through Put()
function testPropertiesAssignedThroughPut() {
    var setterCalled = false;
    Object.assign({set a(v) { setterCalled = v }}, {a: true});
    assertEq(setterCalled, true);
}
testPropertiesAssignedThroughPut();

// Properties are retrieved through Get()
// Properties are assigned through Put(): Existing property attributes are not altered
function propertiesAssignedExistingNotAltered() {
    var source = {a: 1, b: 2, c: 3};
    var target = {a: 0, b: 0, c: 0};
    Object.defineProperty(target, "a", {enumerable: false});
    Object.defineProperty(target, "b", {configurable: false});
    Object.defineProperty(target, "c", {enumerable: false, configurable: false});
    Object.assign(target, source);
    checkDataProperty(target, "a", 1, true, false, true);
    checkDataProperty(target, "b", 2, true, true, false);
    checkDataProperty(target, "c", 3, true, false, false);
}
propertiesAssignedExistingNotAltered();

// Properties are retrieved through Get()
// Properties are assigned through Put(): Throws TypeError if non-writable
function propertiesAssignedTypeErrorNonWritable() {
    var source = {a: 1};
    var target = {a: 0};
    Object.defineProperty(target, "a", {writable: false});
    assertThrowsInstanceOf(() => Object.assign(target, source), TypeError);
    checkDataProperty(target, "a", 0, false, true, true);
}
propertiesAssignedTypeErrorNonWritable();

// Properties are retrieved through Get()
// Put() creates standard properties; Property attributes from source are ignored
function createsStandardProperties() {
    var source = {a: 1, b: 2, c: 3, get d() { return 4 }};
    Object.defineProperty(source, "b", {writable: false});
    Object.defineProperty(source, "c", {configurable: false});
    var target = Object.assign({}, source);
    checkDataProperty(target, "a", 1, true, true, true);
    checkDataProperty(target, "b", 2, true, true, true);
    checkDataProperty(target, "c", 3, true, true, true);
    checkDataProperty(target, "d", 4, true, true, true);
}
createsStandardProperties();

// Properties created during traversal are not copied
function propertiesCreatedDuringTraversalNotCopied() {
    var source = {get a() { this.b = 2 }};
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, false);
}
propertiesCreatedDuringTraversalNotCopied();

// Properties deleted during traversal are not copied
function testDeletePropertiesNotCopied() {
    var source = new Proxy({
        get a() { delete this.b },
        b: 2,
    }, {
        getOwnPropertyNames: () => ["a", "b"]
    });
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, false);
}
testDeletePropertiesNotCopied();

function testDeletionExposingShadowedProperty()
{
    var srcProto = { b: 42 };
    var src =
        Object.create(srcProto,
                        { a: { enumerable: true, get: function() { delete this.b; } },
                          b: { value: 2, configurable: true, enumerable: true } });
    var source = new Proxy(src, { getOwnPropertyNames: () => ["a", "b"] });
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, false);
}
testDeletionExposingShadowedProperty();

// Properties first deleted and then recreated during traversal are copied (1)
function testDeletedAndRecreatedPropertiesCopied1() {
    var source = new Proxy({
        get a() { delete this.c },
        get b() { this.c = 4 },
        c: 3,
    }, {
        getOwnPropertyNames: () => ["a", "b", "c"]
    });
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, true);
    assertEq("c" in target, true);
    checkDataProperty(target, "c", 4, true, true, true);
}
testDeletedAndRecreatedPropertiesCopied1();

// Properties first deleted and then recreated during traversal are copied (2)
function testDeletedAndRecreatedPropertiesCopied2() {
    var source = new Proxy({
        get a() { delete this.c },
        get b() { this.c = 4 },
        c: 3,
    }, {
        ownKeys: () => ["a", "c", "b"]
    });
    var target = Object.assign({}, source);
    assertEq("a" in target, true);
    assertEq("b" in target, true);
    assertEq("c" in target, false);
}
testDeletedAndRecreatedPropertiesCopied2();

// String and Symbol valued properties are copied
function testStringAndSymbolPropertiesCopied() {
    var keyA = "str-prop";
    var source = {"str-prop": 1};
    var target = Object.assign({}, source);
    checkDataProperty(target, keyA, 1, true, true, true);
}
testStringAndSymbolPropertiesCopied();

// Intermediate exceptions stop traversal and throw exception
function testExceptionsDoNotStopFirstReported1() {
    var TestError = function TestError() {};
    var source = new Proxy({}, {
        getOwnPropertyDescriptor: function(t, pk) {
            assertEq(pk, "b");
            throw new TestError();
        },
        ownKeys: () => ["b", "a"]
    });
    assertThrowsInstanceOf(() => Object.assign({}, source), TestError);
}
testExceptionsDoNotStopFirstReported1();


if (typeof reportCompare == "function")
    reportCompare(true, true);
