// -*- mode: js; indent-tabs-mode: nil -*-
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2017 Canonical, Ltd.

imports.gi.versions.Gtk = '3.0';

const {GLib, Gio, GjsTestTools, GObject, Gtk} = imports.gi;
const {system: System} = imports;

describe('Access to destroyed GObject', function () {
    let destroyedWindow;

    beforeAll(function () {
        Gtk.init(null);
    });

    beforeEach(function () {
        destroyedWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL});
        destroyedWindow.destroy();
    });

    it('Get property', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(destroyedWindow.title).toBeUndefined();

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectPropertyGet');
    });

    it('Set property', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        destroyedWindow.title = 'I am dead';

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectPropertySet');
    });

    it('Add expando property', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        destroyedWindow.expandoProperty = 'Hello!';

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectExpandoPropertySet');
    });

    it('Access to unset expando property', function () {
        expect(destroyedWindow.expandoProperty).toBeUndefined();
    });

    it('Access previously set expando property', function () {
        destroyedWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL});
        destroyedWindow.expandoProperty = 'Hello!';
        destroyedWindow.destroy();

        expect(destroyedWindow.expandoProperty).toBe('Hello!');
    });

    it('Access to getter method', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');
        GLib.test_expect_message('Gtk', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*GTK_IS_WINDOW*');

        expect(destroyedWindow.get_title()).toBeNull();

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectMethodGet');
    });

    it('Access to setter method', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');
        GLib.test_expect_message('Gtk', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*GTK_IS_WINDOW*');

        destroyedWindow.set_title('I am dead');

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectMethodSet');
    });

    it('Proto function connect', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(destroyedWindow.connect('foo-signal', () => {})).toBe(0);

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectConnect');
    });

    it('Proto function connect_after', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(destroyedWindow.connect_after('foo-signal', () => {})).toBe(0);

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectConnectAfter');
    });

    it('Proto function emit', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(destroyedWindow.emit('foo-signal')).toBeUndefined();

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectEmit');
    });

    it('Proto function signals_disconnect', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(GObject.signal_handlers_disconnect_by_func(destroyedWindow, () => {})).toBe(0);

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectSignalsDisconnect');
    });

    it('Proto function signals_block', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(GObject.signal_handlers_block_by_func(destroyedWindow, () => {})).toBe(0);

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectSignalsBlock');
    });

    it('Proto function signals_unblock', function () {
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            'Object Gtk.Window (0x*');

        expect(GObject.signal_handlers_unblock_by_func(destroyedWindow, () => {})).toBe(0);

        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'testExceptionInDestroyedObjectSignalsUnblock');
    });

    it('Proto function toString', function () {
        expect(destroyedWindow.toString()).toMatch(
            /\[object \(DISPOSED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
    });

    it('Proto function toString before/after', function () {
        var validWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL});

        expect(validWindow.toString()).toMatch(
            /\[object instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);

        validWindow.destroy();

        expect(validWindow.toString()).toMatch(
            /\[object \(DISPOSED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
    });
});

describe('Disposed or finalized GObject', function () {
    beforeAll(function () {
        GjsTestTools.init();
    });

    afterEach(function () {
        GjsTestTools.reset();
    });

    it('is marked as disposed when it is a manually disposed property', function () {
        const emblem = new Gio.EmblemedIcon({
            gicon: new Gio.ThemedIcon({ name: 'alarm' }),
        });

        let { gicon } = emblem;
        gicon.run_dispose();
        gicon = null;
        System.gc();

        expect(emblem.gicon.toString()).toMatch(
            /\[object \(DISPOSED\) instance wrapper .* jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
    });

    it('generates a warn on object garbage collection', function () {
        Gio.File.new_for_path('/').unref();

        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*Object 0x* has been finalized *');
        System.gc();
        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'generates a warn on object garbage collection');
    });

    it('generates a warn on object garbage collection if has expando property', function () {
        let file = Gio.File.new_for_path('/');
        file.toggleReferenced = true;
        file.unref();
        expect(file.toString()).toMatch(
            /\[object \(FINALIZED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
        file = null;
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*Object 0x* has been finalized *');
        System.gc();
        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'generates a warn on object garbage collection if has expando property');
    });

    it('generates a warn if already disposed at garbage collection', function () {
        const loop = new GLib.MainLoop(null, false);

        let file = Gio.File.new_for_path('/');
        GjsTestTools.delayed_unref(file, 1);  // Will happen after dispose
        file.run_dispose();

        let done = false;
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => (done = true));
        while (!done)
            loop.get_context().iteration(true);

        file = null;
        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*Object 0x* has been finalized *');
        System.gc();
        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'generates a warn if already disposed at garbage collection');
    });

    it('created from other function is marked as disposed', function () {
        let file = Gio.File.new_for_path('/');
        GjsTestTools.save_object(file);
        file.run_dispose();
        file = null;
        System.gc();

        expect(GjsTestTools.get_saved()).toMatch(
            /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
    });

    it('returned from function is marked as disposed', function () {
        expect(GjsTestTools.get_disposed(Gio.File.new_for_path('/'))).toMatch(
            /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);
    });

    it('returned from function is marked as disposed and then as finalized', function () {
        let file = Gio.File.new_for_path('/');
        GjsTestTools.save_object(file);
        GjsTestTools.delayed_unref(file, 30);
        file.run_dispose();

        let disposedFile = GjsTestTools.get_saved();
        expect(disposedFile).toEqual(file);
        expect(disposedFile).toMatch(
            /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);

        file = null;
        System.gc();

        const loop = new GLib.MainLoop(null, false);
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit());
        loop.run();

        expect(disposedFile).toMatch(
            /\[object \(FINALIZED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/);

        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*Object 0x* has been finalized *');
        disposedFile = null;
        System.gc();
        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'returned from function is marked as disposed and then as finalized');
    });

    it('ignores toggling queued unref toggles', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        file.ref();
        GjsTestTools.unref_other_thread(file);
        file.run_dispose();
    });

    it('ignores toggling queued toggles', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.ref_other_thread(file);
        GjsTestTools.unref_other_thread(file);
        file.run_dispose();
    });

    it('can be disposed from other thread', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        file.ref();
        GjsTestTools.unref_other_thread(file);
        GjsTestTools.run_dispose_other_thread(file);
    });

    it('can be garbage collected once disposed from other thread', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.run_dispose_other_thread(file);
        file = null;
        System.gc();
    });
});

describe('GObject with toggle references', function () {
    beforeAll(function () {
        GjsTestTools.init();
    });

    afterEach(function () {
        GjsTestTools.reset();
    });

    it('can be re-reffed from other thread delayed', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        const objectAddress = System.addressOfGObject(file);
        GjsTestTools.save_object_unreffed(file);
        GjsTestTools.delayed_ref_other_thread(file, 10);
        file = null;
        System.gc();

        const loop = new GLib.MainLoop(null, false);
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit());
        loop.run();

        // We need to cleanup the extra ref we added before now.
        // However, depending on whether the thread ref happens the object
        // may be already finalized, and in such case we need to throw
        try {
            file = GjsTestTools.steal_saved();
            if (file) {
                expect(System.addressOfGObject(file)).toBe(objectAddress);
                expect(file instanceof Gio.File).toBeTruthy();
                file.unref();
            }
        } catch (e) {
            expect(() => {
                throw e;
            }).toThrowError(/.*Unhandled GType.*/);
        }
    });

    it('can be re-reffed and unreffed again from other thread', function () {
        let file = Gio.File.new_for_path('/');
        const objectAddress = System.addressOfGObject(file);
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_object(file);
        GjsTestTools.delayed_unref_other_thread(file.ref(), 10);
        file = null;
        System.gc();

        const loop = new GLib.MainLoop(null, false);
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit());
        loop.run();

        file = GjsTestTools.get_saved();
        expect(System.addressOfGObject(file)).toBe(objectAddress);
        expect(file instanceof Gio.File).toBeTruthy();
    });

    it('can be re-reffed and unreffed again from other thread with delay', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.delayed_ref_unref_other_thread(file, 10);
        file = null;
        System.gc();

        const loop = new GLib.MainLoop(null, false);
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit());
        loop.run();
    });

    it('can be toggled up by getting a GWeakRef', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_weak(file);
        GjsTestTools.get_weak();
    });

    it('can be toggled up by getting a GWeakRef from another thread', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_weak(file);
        GjsTestTools.get_weak_other_thread();
    });

    it('can be toggled up by getting a GWeakRef from another thread and re-reffed in main thread', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_weak(file);
        GjsTestTools.get_weak_other_thread();

        // Ok, let's play more dirty now...
        file.ref(); // toggle up
        file.unref(); // toggle down

        file.ref();
        file.ref();
        file.unref();
        file.unref();
    });

    it('can be toggled up by getting a GWeakRef from another and re-reffed from various threads', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_weak(file);
        GjsTestTools.get_weak_other_thread();

        GjsTestTools.ref_other_thread(file);
        GjsTestTools.unref_other_thread(file);

        file.ref();
        file.unref();

        GjsTestTools.ref_other_thread(file);
        file.unref();

        file.ref();
        GjsTestTools.unref_other_thread(file);
    });

    it('can be finalized while queued in toggle queue', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        file.ref();
        GjsTestTools.unref_other_thread(file);
        GjsTestTools.unref_other_thread(file);

        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
            '*Object 0x* has been finalized *');
        file = null;
        System.gc();
        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0,
            'can be finalized while queued in toggle queue');
    });

    it('can be toggled up-down from various threads while getting a GWeakRef from main', function () {
        let file = Gio.File.new_for_path('/');
        file.expandMeWithToggleRef = true;
        GjsTestTools.save_weak(file);

        const ids = [];
        let threads = [];
        ids.push(GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            threads = threads.slice(-50);
            try {
                threads.push(GjsTestTools.delayed_ref_unref_other_thread(file, 1));
            } catch (e) {
                // If creating the thread failed we're almost going out of memory
                // so let's first wait for the ones allocated to complete.
                threads.forEach(th => th.join());
                threads = [];
            }
            return true;
        }));

        const loop = new GLib.MainLoop(null, false);
        ids.push(GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            expect(GjsTestTools.get_weak()).toEqual(file);
            return true;
        }));

        // We must not timeout due to deadlock #404 and finally not crash per #297
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, () => loop.quit());
        loop.run();
        ids.forEach(id => GLib.source_remove(id));

        // We also check that late thread events won't affect the destroyed wrapper
        GjsTestTools.save_object(file);
        file = null;
        System.gc();
        threads.forEach(th => th.join());
        expect(GjsTestTools.get_saved_ref_count()).toBeGreaterThan(0);

        GjsTestTools.clear_saved();
        System.gc();
        expect(GjsTestTools.get_weak()).toBeNull();
    });
});
