describe("tagged template literal errors", () => {
    test("undefined variables in template expression throw a ReferenceError", () => {
        expect(() => {
            foo`bar${baz}`;
        }).toThrowWithMessage(ReferenceError, "'foo' is not defined");

        expect(() => {
            function foo() {}
            foo`bar${baz}`;
        }).toThrowWithMessage(ReferenceError, "'baz' is not defined");
    });

    test("cannot tag a non-function", () => {
        expect(() => {
            undefined``;
        }).toThrowWithMessage(TypeError, "undefined is not a function");
    });
});

describe("tagged template literal functionality", () => {
    test("empty template tag", () => {
        function test1(strings) {
            expect(strings).toBeInstanceOf(Array);
            expect(strings).toHaveLength(1);
            expect(strings[0]).toBe("");
            return 42;
        }
        expect(test1``).toBe(42);
    });

    test("tagging a template literal", () => {
        function test2(s) {
            return function (strings) {
                expect(strings).toBeInstanceOf(Array);
                expect(strings).toHaveLength(1);
                expect(strings[0]).toBe("bar");
                return s + strings[0];
            };
        }
        expect(test2("foo")`bar`).toBe("foobar");
    });

    test("tagging an object function key", () => {
        var test3 = {
            foo(strings, p1) {
                expect(strings).toBeInstanceOf(Array);
                expect(strings).toHaveLength(2);
                expect(strings[0]).toBe("");
                expect(strings[1]).toBe("");
                expect(p1).toBe("bar");
            },
        };
        test3.foo`${"bar"}`;
    });

    test("tagging with a variable in a template expression", () => {
        function test4(strings, p1) {
            expect(strings).toBeInstanceOf(Array);
            expect(strings).toHaveLength(2);
            expect(strings[0]).toBe("foo");
            expect(strings[1]).toBe("");
            expect(p1).toBe(42);
        }
        var bar = 42;
        test4`foo${bar}`;
    });

    test("template tag result of another template tag", () => {
        function test5(strings, p1, p2) {
            expect(strings).toBeInstanceOf(Array);
            expect(strings).toHaveLength(3);
            expect(strings[0]).toBe("foo");
            expect(strings[1]).toBe("baz");
            expect(strings[2]).toBe("");
            expect(p1).toBe(42);
            expect(p2).toBe("qux");
            return (strings, value) => `${value}${strings[0]}`;
        }
        var bar = 42;
        expect(test5`foo${bar}baz${"qux"}``test${123}`).toBe("123test");
    });

    test("general test", () => {
        function review(strings, name, rating) {
            return `${strings[0]}**${name}**${strings[1]}_${rating}_${strings[2]}`;
        }
        var name = "SerenityOS";
        var rating = "great";
        expect(review`${name} is a ${rating} project!`).toBe(
            "**SerenityOS** is a _great_ project!"
        );
    });

    test("template object structure", () => {
        const getTemplateObject = (...rest) => rest;
        const getRawTemplateStrings = arr => arr.raw;

        let o = getTemplateObject`foo\nbar`;
        expect(Object.getOwnPropertyNames(o[0])).toContain("raw");

        let raw = getRawTemplateStrings`foo${1 + 3}\nbar`;
        expect(Object.getOwnPropertyNames(raw)).not.toContain("raw");
        expect(raw).toHaveLength(2);
        expect(raw[0]).toBe("foo");
        expect(raw[1]).toHaveLength(5);
        expect(raw[1]).toBe("\\nbar");
    });

    test("invalid escapes give undefined cooked values but can be accessed in raw form", () => {
        let calls = 0;
        let lastValue = null;
        function noCookedButRaw(values) {
            ++calls;
            expect(values).not.toBeNull();
            expect(values.raw).toHaveLength(1);
            expect(values.raw[0].length).toBeGreaterThan(0);
            expect(values.raw[0].charAt(0)).toBe("\\");
            expect(values[0]).toBeUndefined();
            lastValue = values.raw[0];
        }
        noCookedButRaw`\u`;
        expect(calls).toBe(1);
        expect(lastValue).toBe("\\u");

        noCookedButRaw`\01`;
        expect(calls).toBe(2);
        expect(lastValue).toBe("\\01");

        noCookedButRaw`\u{10FFFFF}`;
        expect(calls).toBe(3);
        expect(lastValue).toBe("\\u{10FFFFF}");
    });

    test("for multiple values gives undefined only for invalid strings", () => {
        let restValue = null;
        let stringsValue = null;
        let calls = 0;

        function extractArguments(value, ...arguments) {
            ++calls;
            restValue = arguments;
            stringsValue = value;
        }
        extractArguments`valid${1}invalid\u`;

        expect(calls).toBe(1);
        expect(restValue).toHaveLength(1);
        expect(restValue[0]).toBe(1);
        expect(stringsValue).toHaveLength(2);
        expect(stringsValue[0]).toBe("valid");
        expect(stringsValue[1]).toBeUndefined();
        expect(stringsValue.raw).toHaveLength(2);
        expect(stringsValue.raw[0]).toBe("valid");
        expect(stringsValue.raw[1]).toBe("invalid\\u");
    });

    test("string value gets cached per AST node", () => {
        function call(func, val) {
            return func`template${val}second`;
        }

        let firstResult = call(value => value, 1);
        let secondResult = call(value => value, 2);
        expect(firstResult).toBe(secondResult);
    });

    test("this value of call comes from reference", () => {
        let thisValue = null;
        const obj = {
            func() {
                thisValue = this;
            },
        };

        obj.func``;

        expect(thisValue).toBe(obj);
    });
});
