/* eslint-disable no-restricted-properties */
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2008 litl, LLC

const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Signals = imports.signals;

const Foo = new Lang.Class({
    Name: 'Foo',
    Implements: [Signals.WithSignals],
    _init() {},
});

describe('Legacy object with signals', function () {
    testSignals(Foo);
});

class FooWithoutSignals {}
Signals.addSignalMethods(FooWithoutSignals.prototype);

describe('Object with signals added', function () {
    testSignals(FooWithoutSignals);
});

function testSignals(klass) {
    let foo, bar;
    beforeEach(function () {
        foo = new klass();
        bar = jasmine.createSpy('bar');
    });

    it('emit works with no connections', function () {
        expect(() => foo.emit('random-event')).not.toThrow();
    });

    ['connect', 'connectAfter'].forEach(connectMethod => {
        describe(`using ${connectMethod}`, function () {
            it('calls a signal handler when a signal is emitted', function () {
                foo[connectMethod]('bar', bar);
                foo.emit('bar', 'This is a', 'This is b');
                expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
            });

            it('calls remaining handlers after one is disconnected', function () {
                const id1 = foo[connectMethod]('bar', bar);

                const bar2 = jasmine.createSpy('bar2');
                const id2 = foo[connectMethod]('bar', bar2);

                foo.emit('bar');
                expect(bar).toHaveBeenCalledTimes(1);
                expect(bar2).toHaveBeenCalledTimes(1);

                foo.disconnect(id1);

                foo.emit('bar');

                expect(bar).toHaveBeenCalledTimes(1);
                expect(bar2).toHaveBeenCalledTimes(2);

                foo.disconnect(id2);
            });

            it('does not call a signal handler after the signal is disconnected', function () {
                let id = foo[connectMethod]('bar', bar);
                foo.emit('bar', 'This is a', 'This is b');
                bar.calls.reset();
                foo.disconnect(id);
                // this emission should do nothing
                foo.emit('bar', 'Another a', 'Another b');
                expect(bar).not.toHaveBeenCalled();
            });

            it('can disconnect a signal handler during signal emission', function () {
                var toRemove = [];
                let firstId = foo[connectMethod]('bar', function (theFoo) {
                    theFoo.disconnect(toRemove[0]);
                    theFoo.disconnect(toRemove[1]);
                });
                toRemove.push(foo[connectMethod]('bar', bar));
                toRemove.push(foo[connectMethod]('bar', bar));

                // emit signal; what should happen is that the second two handlers are
                // disconnected before they get invoked
                foo.emit('bar');
                expect(bar).not.toHaveBeenCalled();

                // clean up the last handler
                foo.disconnect(firstId);

                expect(() => foo.disconnect(firstId)).toThrowError(
                    `No signal connection ${firstId} found`);

                // poke in private implementation to sanity-check no handlers left
                expect(Object.keys(foo._signalConnections).length).toEqual(0);
            });

            it('distinguishes multiple signals', function () {
                let bonk = jasmine.createSpy('bonk');
                foo[connectMethod]('bar', bar);
                foo[connectMethod]('bonk', bonk);
                foo[connectMethod]('bar', bar);

                foo.emit('bar');
                expect(bar).toHaveBeenCalledTimes(2);
                expect(bonk).not.toHaveBeenCalled();

                foo.emit('bonk');
                expect(bar).toHaveBeenCalledTimes(2);
                expect(bonk).toHaveBeenCalledTimes(1);

                foo.emit('bar');
                expect(bar).toHaveBeenCalledTimes(4);
                expect(bonk).toHaveBeenCalledTimes(1);

                foo.disconnectAll();
                bar.calls.reset();
                bonk.calls.reset();

                // these post-disconnect emissions should do nothing
                foo.emit('bar');
                foo.emit('bonk');
                expect(bar).not.toHaveBeenCalled();
                expect(bonk).not.toHaveBeenCalled();
            });

            it('determines if a signal is connected on a JS object', function () {
                let id = foo[connectMethod]('bar', bar);
                expect(foo.signalHandlerIsConnected(id)).toEqual(true);
                foo.disconnect(id);
                expect(foo.signalHandlerIsConnected(id)).toEqual(false);
            });

            it('does not call a subsequent connected callbacks if stopped by earlier', function () {
                const afterBar = jasmine.createSpy('bar');
                const afterAfterBar = jasmine.createSpy('barBar');
                foo[connectMethod]('bar', bar.and.returnValue(true));
                foo[connectMethod]('bar', afterBar);
                foo[connectMethod]('bar', afterAfterBar);
                foo.emit('bar', 'This is a', 123);
                expect(bar).toHaveBeenCalledWith(foo, 'This is a', 123);
                expect(afterBar).not.toHaveBeenCalled();
                expect(afterAfterBar).not.toHaveBeenCalled();
            });

            describe('with exception in signal handler', function () {
                let bar2;
                beforeEach(function () {
                    bar.and.throwError('Exception we are throwing on purpose');
                    bar2 = jasmine.createSpy('bar');
                    foo[connectMethod]('bar', bar);
                    foo[connectMethod]('bar', bar2);
                    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
                        'JS ERROR: Exception in callback for signal: *');
                    foo.emit('bar');
                });

                it('does not affect other callbacks', function () {
                    expect(bar).toHaveBeenCalledTimes(1);
                    expect(bar2).toHaveBeenCalledTimes(1);
                });

                it('does not disconnect the callback', function () {
                    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
                        'JS ERROR: Exception in callback for signal: *');
                    foo.emit('bar');
                    expect(bar).toHaveBeenCalledTimes(2);
                    expect(bar2).toHaveBeenCalledTimes(2);
                });
            });
        });
    });

    it('using connectAfter calls a signal handler later than when using connect when a signal is emitted', function () {
        const afterBar = jasmine.createSpy('bar');
        foo.connectAfter('bar', (...args) => {
            expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
            afterBar(...args);
        });
        foo.connect('bar', bar);
        foo.emit('bar', 'This is a', 'This is b');
        expect(afterBar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
    });

    it('does not call a connected after handler when stopped by connect', function () {
        const afterBar = jasmine.createSpy('bar');
        foo.connectAfter('bar', afterBar);
        foo.connect('bar', bar.and.returnValue(true));
        foo.emit('bar', 'This is a', 'This is b');
        expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
        expect(afterBar).not.toHaveBeenCalled();
    });
}
