describe("basic usage", () => {
    test.xfail("disposes after block exit", () => {
        let disposed = false;
        let inBlock = false;
        {
            expect(disposed).toBeFalse();
            using a = {
                [Symbol.dispose]() {
                    disposed = true;
                },
            };
            inBlock = true;
            expect(disposed).toBeFalse();
        }

        expect(inBlock).toBeTrue();
        expect(disposed).toBeTrue();
    });

    test.xfail("disposes in reverse order after block exit", () => {
        const disposed = [];
        {
            expect(disposed).toHaveLength(0);
            using a = {
                [Symbol.dispose]() {
                    disposed.push("a");
                },
            };
            using b = {
                [Symbol.dispose]() {
                    disposed.push("b");
                },
            };
            expect(disposed).toHaveLength(0);
        }

        expect(disposed).toEqual(["b", "a"]);
    });

    test.xfail("disposes in reverse order after block exit even in same declaration", () => {
        const disposed = [];
        {
            expect(disposed).toHaveLength(0);
            using a = {
                    [Symbol.dispose]() {
                        disposed.push("a");
                    },
                },
                b = {
                    [Symbol.dispose]() {
                        disposed.push("b");
                    },
                };
            expect(disposed).toHaveLength(0);
        }

        expect(disposed).toEqual(["b", "a"]);
    });
});

describe("behavior with exceptions", () => {
    function ExpectedError(name) {
        this.name = name;
    }

    test.xfail("is run even after throw", () => {
        let disposed = false;
        let inBlock = false;
        let inCatch = false;
        try {
            expect(disposed).toBeFalse();
            using a = {
                [Symbol.dispose]() {
                    disposed = true;
                },
            };
            inBlock = true;
            expect(disposed).toBeFalse();
            throw new ExpectedError();
            expect().fail();
        } catch (e) {
            expect(disposed).toBeTrue();
            expect(e).toBeInstanceOf(ExpectedError);
            inCatch = true;
        }
        expect(disposed).toBeTrue();
        expect(inBlock).toBeTrue();
        expect(inCatch).toBeTrue();
    });

    test.xfail("throws error if dispose method does", () => {
        let disposed = false;
        let endOfTry = false;
        let inCatch = false;
        try {
            expect(disposed).toBeFalse();
            using a = {
                [Symbol.dispose]() {
                    disposed = true;
                    throw new ExpectedError();
                },
            };
            expect(disposed).toBeFalse();
            endOfTry = true;
        } catch (e) {
            expect(disposed).toBeTrue();
            expect(e).toBeInstanceOf(ExpectedError);
            inCatch = true;
        }
        expect(disposed).toBeTrue();
        expect(endOfTry).toBeTrue();
        expect(inCatch).toBeTrue();
    });

    test.xfail("if block and using throw get suppressed error", () => {
        let disposed = false;
        let inCatch = false;
        try {
            expect(disposed).toBeFalse();
            using a = {
                [Symbol.dispose]() {
                    disposed = true;
                    throw new ExpectedError("dispose");
                },
            };
            expect(disposed).toBeFalse();
            throw new ExpectedError("throw");
        } catch (e) {
            expect(disposed).toBeTrue();
            expect(e).toBeInstanceOf(SuppressedError);
            expect(e.error).toBeInstanceOf(ExpectedError);
            expect(e.error.name).toBe("dispose");
            expect(e.suppressed).toBeInstanceOf(ExpectedError);
            expect(e.suppressed.name).toBe("throw");
            inCatch = true;
        }
        expect(disposed).toBeTrue();
        expect(inCatch).toBeTrue();
    });

    test.xfail("multiple throwing disposes give suppressed error", () => {
        let inCatch = false;
        try {
            {
                using a = {
                    [Symbol.dispose]() {
                        throw new ExpectedError("a");
                    },
                };

                using b = {
                    [Symbol.dispose]() {
                        throw new ExpectedError("b");
                    },
                };
            }

            expect().fail();
        } catch (e) {
            expect(e).toBeInstanceOf(SuppressedError);
            expect(e.error).toBeInstanceOf(ExpectedError);
            expect(e.error.name).toBe("a");
            expect(e.suppressed).toBeInstanceOf(ExpectedError);
            expect(e.suppressed.name).toBe("b");
            inCatch = true;
        }
        expect(inCatch).toBeTrue();
    });

    test.xfail("3 throwing disposes give chaining suppressed error", () => {
        let inCatch = false;
        try {
            {
                using a = {
                    [Symbol.dispose]() {
                        throw new ExpectedError("a");
                    },
                };

                using b = {
                    [Symbol.dispose]() {
                        throw new ExpectedError("b");
                    },
                };

                using c = {
                    [Symbol.dispose]() {
                        throw new ExpectedError("c");
                    },
                };
            }

            expect().fail();
        } catch (e) {
            expect(e).toBeInstanceOf(SuppressedError);
            expect(e.error).toBeInstanceOf(ExpectedError);
            expect(e.error.name).toBe("a");
            expect(e.suppressed).toBeInstanceOf(SuppressedError);

            const inner = e.suppressed;

            expect(inner.error).toBeInstanceOf(ExpectedError);
            expect(inner.error.name).toBe("b");
            expect(inner.suppressed).toBeInstanceOf(ExpectedError);
            expect(inner.suppressed.name).toBe("c");
            inCatch = true;
        }
        expect(inCatch).toBeTrue();
    });

    test.xfail("normal error and multiple disposing errors give chaining suppressed errors", () => {
        let inCatch = false;
        try {
            using a = {
                [Symbol.dispose]() {
                    throw new ExpectedError("a");
                },
            };

            using b = {
                [Symbol.dispose]() {
                    throw new ExpectedError("b");
                },
            };

            throw new ExpectedError("top");
        } catch (e) {
            expect(e).toBeInstanceOf(SuppressedError);
            expect(e.error).toBeInstanceOf(ExpectedError);
            expect(e.error.name).toBe("a");
            expect(e.suppressed).toBeInstanceOf(SuppressedError);

            const inner = e.suppressed;

            expect(inner.error).toBeInstanceOf(ExpectedError);
            expect(inner.error.name).toBe("b");
            expect(inner.suppressed).toBeInstanceOf(ExpectedError);
            expect(inner.suppressed.name).toBe("top");
            inCatch = true;
        }
        expect(inCatch).toBeTrue();
    });
});

describe("works in a bunch of scopes", () => {
    test.xfail("works in block", () => {
        let dispose = false;
        expect(dispose).toBeFalse();
        {
            expect(dispose).toBeFalse();
            using a = {
                [Symbol.dispose]() {
                    dispose = true;
                },
            };
            expect(dispose).toBeFalse();
        }
        expect(dispose).toBeTrue();
    });

    test.xfail("works in static class block", () => {
        let dispose = false;
        expect(dispose).toBeFalse();
        class A {
            static {
                expect(dispose).toBeFalse();
                using a = {
                    [Symbol.dispose]() {
                        dispose = true;
                    },
                };
                expect(dispose).toBeFalse();
            }
        }
        expect(dispose).toBeTrue();
    });

    test.xfail("works in function", () => {
        let dispose = [];
        function f(val) {
            const disposeLength = dispose.length;
            using a = {
                [Symbol.dispose]() {
                    dispose.push(val);
                },
            };
            expect(dispose.length).toBe(disposeLength);
        }
        expect(dispose).toEqual([]);
        f(0);
        expect(dispose).toEqual([0]);
        f(1);
        expect(dispose).toEqual([0, 1]);
    });

    test.xfail("switch block is treated as full block in function", () => {
        let disposeFull = [];
        let disposeInner = false;

        function pusher(val) {
            return {
                val,
                [Symbol.dispose]() {
                    disposeFull.push(val);
                },
            };
        }

        switch (2) {
            case 3: {
                using notDisposed = {
                    [Symbol.dispose]() {
                        expect().fail("not-disposed 1");
                    },
                };
            }
            case 2: {
                expect(disposeFull).toEqual([]);
                using a = pusher("a");
                expect(disposeFull).toEqual([]);

                using b = pusher("b");
                expect(disposeFull).toEqual([]);
                expect(b.val).toBe("b");

                expect(disposeInner).toBeFalse();
            }
            // fallthrough
            case 1:
                {
                    expect(disposeFull).toEqual([]);
                    expect(disposeInner).toBeFalse();

                    using inner = {
                        [Symbol.dispose]() {
                            disposeInner = true;
                        },
                    };

                    expect(disposeInner).toBeFalse();
                }
                expect(disposeInner).toBeTrue();
                {
                    using c = pusher("c");
                    expect(c.val).toBe("c");
                }
                break;
            case 0: {
                using notDisposed2 = {
                    [Symbol.dispose]() {
                        expect().fail("not-disposed 2");
                    },
                };
            }
        }

        expect(disposeInner).toBeTrue();
        expect(disposeFull).toEqual(["c", "b", "a"]);
    });
});

describe("invalid using bindings", () => {
    test.xfail("nullish values do not throw", () => {
        using a = null,
            b = undefined;
        expect(a).toBeNull();
        expect(b).toBeUndefined();
    });

    test.xfail("non-object throws", () => {
        [0, "a", true, NaN, 4n, Symbol.dispose].forEach(value => {
            expect(() => {
                using v = value;
            }).toThrowWithMessage(TypeError, "is not an object");
        });
    });

    test.xfail("object without dispose throws", () => {
        expect(() => {
            using a = {};
        }).toThrowWithMessage(TypeError, "does not have dispose method");
    });

    test.xfail("object with non callable dispose throws", () => {
        [0, "a", true, NaN, 4n, Symbol.dispose, [], {}].forEach(value => {
            expect(() => {
                using a = { [Symbol.dispose]: value };
            }).toThrowWithMessage(TypeError, "is not a function");
        });
    });
});

describe("using is still a valid variable name", () => {
    test("var", () => {
        "use strict";
        var using = 1;
        expect(using).toBe(1);
    });

    test("const", () => {
        "use strict";
        const using = 1;
        expect(using).toBe(1);
    });

    test("let", () => {
        "use strict";
        let using = 1;
        expect(using).toBe(1);
    });

    test.xfail("using", () => {
        "use strict";
        using using = null;
        expect(using).toBeNull();
    });

    test("function", () => {
        "use strict";
        function using() {
            return 1;
        }
        expect(using()).toBe(1);
    });
});

describe("syntax errors / weird artifacts which remain valid", () => {
    test("no patterns in using", () => {
        expect("using {a} = {}").not.toEval();
        expect("using a, {a} = {}").not.toEval();
        expect("using a = null, [b] = [null]").not.toEval();
    });

    test("using with array pattern is valid array access", () => {
        const using = [0, 9999];
        const a = 1;

        expect(eval("using [a] = 1")).toBe(1);
        expect(using[1]).toBe(1);

        expect(eval("using [{a: a}, a] = 2")).toBe(2);
        expect(using[1]).toBe(2);

        expect(eval("using [a, a] = 3")).toBe(3);
        expect(using[1]).toBe(3);

        expect(eval("using [[a, a], a] = 4")).toBe(4);
        expect(using[1]).toBe(4);

        expect(eval("using [2, 1, a] = 5")).toBe(5);
        expect(using[1]).toBe(5);
    });

    test("declaration without initializer", () => {
        expect("using a").not.toEval();
    });

    test("no repeat declarations in single using", () => {
        expect("using a = null, a = null;").not.toEval();
    });

    test("cannot have a using declaration named let", () => {
        expect("using let = null").not.toEval();
    });
});
