<!doctype html>
<script src="/resources/channel.sub.js"></script>
<script src="serialize-data.js"></script>
<script>

let lastData;

// Hack: these will be converted into testharness AssertionError instances in the test
// This means they will be treated identically to asserts raised by `assert_` in the harness
// In the long term we want to be able to use parts of testharness.js directly in remote
// contexts and automatically send the results over a channel.
function AssertionError(message) {
    this.message = message;
}
AssertionError.prototype = Object.create(Error.prototype);

function compareResult(name, actual) {
    let obj = objects[name];
    // If there's an output property use that, otherwise assume the output is equal to the input
    let expected = obj.hasOwnProperty("output") ? obj.output : obj.input;
    seen = new Set();
    try {
        compareValue(actual, expected, seen);
    } catch(e) {
        throw new AssertionError(e.message);
    }
    return true;
}

function compareValue(actualValue, expectedValue, seen) {
    let seenActual;
    if (typeof actualValue != typeof expectedValue) {
        throw new Error(`Types differ, expected ${typeof expectedValue}, got ${typeof actualValue}`);
    }
    if (["undefined", "string", "boolean", "number", "bigint"].includes(typeof expectedValue) ||
        actualValue === null) {
        if (!Object.is(actualValue, expectedValue)) {
            throw new Error(`Expected ${typeof expected} ${expected}, got ${actual}`);
        }
        return;
    }

    if (expectedValue.constructor && actualValue.constructor && expectedValue.constructor.name !== actualValue.constructor.name) {
        throw new Error(`Constructors differ, expected ${expectedValue.constructor.name}, got ${actualValue.constructor.name}`);
    }
    if (expectedValue.constructor && expectedValue.constructor.name === "SendChannel") {
        if (expectedValue.uuid !== actualValue.uuid) {
            throw new Error(`SendChannels differ, expected uuid ${expectedValue.uuid}, got ${actualValue.uuid}`);
        }
    }
    else if (expectedValue.constructor && expectedValue.constructor.name === "RegExp") {
        if (expectedValue.source !== actualValue.source ||
            expectedValue.flags !== actualValue.flags) {
            throw new Error(`RegExps differ, expected ${expectedValue}, got ${actualValue}`);
        }
    } else if (expectedValue.constructor && expectedValue.constructor.name == "Date") {
        if (expectedValue.valueOf() !== actualValue.valueOf()) {
            throw new Error(`Dates differ, expected ${expectedValue.valueOf()} (${expectedValue.toDateString()}), `
                            `got ${actualValue.valueOf()} (${actualValue.toDateString()})`);
        }
    } else if (expectedValue instanceof Error) {
        if (expectedValue.message !== actualValue.message ||
            expectedValue.lineNumber !== actualValue.lineNumber ||
            expectedValue.columnNumber !== actualValue.columnNumber ||
            expectedValue.fileName !== actualValue.fileName) {
            throw new Error(`Errors differ, expected ${expectedValue}, got ${actualValue}`);
        }
    } else if (Array.isArray(expectedValue)) {
        seenActual = seen.has(actualValue);
        seenExpected = seen.has(expectedValue)
        if (seenActual && seenExpected) {
            return;
        } else if (seenExpected && !seenActual) {
            throw new Error(`Expected cyclic array`);
        } else if (!seenExpected && seenActual) {
            throw new Error(`Got unexpected cyclic array`);
        }
        seen.add(actualValue);
        seen.add(expectedValue);

        if (actualValue.length !== expectedValue.length) {
            throw new Error(`Array lengths differ, expected ${expectedValue.length}, got ${actualValue.length}`);
        }
        for (let i=0; i<actualValue.length; i++) {
            compareValue(actualValue[i], expectedValue[i], seen);
        }
    } else if (expectedValue.constructor && expectedValue.constructor.name === "Set") {
        seenActual = seen.has(actualValue);
        seenExpected = seen.has(expectedValue)
        if (seenActual && seenExpected) {
            return;
        } else if (seenExpected && !seenActual) {
            throw new Error(`Expected cyclic set`);
        } else if (!seenExpected && seenActual) {
            throw new Error(`Got unexpected cyclic set`);
        }
        seen.add(actualValue);
        seen.add(expectedValue);


        if (actualValue.size !== expectedValue.size) {
            throw new Error(`Set sizes differ, expected ${expectedValue.size}, got ${actualValue.size}`);
        }
        // For an arbitary set it's complex to check if two sets are equivalent, since
        // we'd need to compare every object in one set with every object in the
        // other set, so we end up with quadratic complexity. Instead, just support sets
        // containing primitives and rely on the other tests for correct handling of
        // objects.
        for (let entry of expectedValue) {
            if (["undefined", "string", "boolean", "number", "bigint"].includes(typeof entry) || entry === null) {
                if(!actualValue.has(entry)) {
                    throw new Error(`Set missing entry, expected ${entry}`);
                }
            } else {
                throw new Error(`Can't compare non-primitive value ${entry} inside sets`);
            }
        }
    } else if (expectedValue.constructor && expectedValue.constructor.name === "Map") {
        seenActual = seen.has(actualValue);
        seenExpected = seen.has(expectedValue)
        if (seenActual && seenExpected) {
            return;
        } else if (seenExpected && !seenActual) {
            throw new Error(`Expected cyclic map`);
        } else if (!seenExpected && seenActual) {
            throw new Error(`Got unexpected cyclic map`);
        }
        seen.add(actualValue);
        seen.add(expectedValue);

        if (actualValue.size !== expectedValue.size) {
            throw new Error(`Map sizes differ, expected ${expectedValue.size}, got ${actualValue.size}`);
        }
        // So for a set we can't really check if the values are the same
        // except where they're primitives
        for (let [key, value] of expectedValue.entries()) {
            if(!actualValue.has(key)) {
                throw new Error(`Map missing key, expected key ${key} with value ${value}`);
            }
            compareValue(actualValue.get(key), value, seen);
        }
    } else {
        seenActual = seen.has(actualValue);
        seenExpected = seen.has(expectedValue)
        if (seenActual && seenExpected) {
            return;
        } else if (seenExpected && !seenActual) {
            throw new Error(`Expected cyclic object`);
        } else if (!seenExpected && seenActual) {
            throw new Error(`Got unexpected cyclic object`);
        }
        seen.add(actualValue);
        seen.add(expectedValue);


        // Compare as a general Object
        let expectedEntries = Object.entries(expectedValue);
        if (Object.keys(actualValue).length !== expectedEntries.length) {
            throw new Error(`Object keys differ, expected [${Object.keys(expectedValue).join(",")}], got [${Object.keys(actualValue).join(",")}]`);
        }
        // So for a set we can't really check if the values are the same
        // except where they're primitives
        for (let [name, entry] of expectedEntries) {
            if(!actualValue.hasOwnProperty(name)) {
                throw new Error(`Object missing key ${name}`);
            }
            compareValue(actualValue[name], entry, seen);
        }
    }
}

ctx = start_global_channel();
</script>
