// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/* eslint-disable @typescript-eslint/no-explicit-any */

import { enableJSDOM } from '../test/jsdom';

let disableJSDOM = enableJSDOM();

import * as assert from 'assert';
import { Container } from 'inversify';
import { bindPreferenceService } from '../frontend-application-bindings';
import { bindMockPreferenceProviders, MockPreferenceProvider } from './test';
import { PreferenceScope } from '../../common/preferences/preference-scope';
import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider';
import { PreferenceProxyOptions, PreferenceProxy, PreferenceChangeEvent, createPreferenceProxy } from '../../common/preferences/preference-proxy';
import { PreferenceProxyFactory } from '../../common/preferences/injectable-preference-proxy';

disableJSDOM();

process.on('unhandledRejection', (reason, promise) => {
    console.error(reason);
    throw reason;
});

import { expect } from 'chai';
import { PreferenceProvider } from '../../common/preferences/preference-provider';
import { PreferenceSchema, PreferenceSchemaService } from '../../common/preferences/preference-schema';
import { PreferenceService, PreferenceServiceImpl } from '../../common/preferences';
import { waitForEvent } from '../../common/promise-util';
let testContainer: Container;

function createTestContainer(): Container {
    const result = new Container();
    bindPreferenceService(result.bind.bind(result));
    bindMockPreferenceProviders(result.bind.bind(result), result.unbind.bind(result));
    return result;
}

describe('Preference Proxy', () => {
    let prefService: PreferenceServiceImpl;
    let prefSchema: PreferenceSchemaService;

    before(() => {
        disableJSDOM = enableJSDOM();
        FrontendApplicationConfigProvider.set({});
    });

    after(() => {
        disableJSDOM();
    });

    beforeEach(async () => {
        testContainer = createTestContainer();
        prefSchema = testContainer.get(PreferenceSchemaService);
        prefService = testContainer.get<PreferenceService>(PreferenceService) as PreferenceServiceImpl;
        getProvider(PreferenceScope.User).markReady();
        getProvider(PreferenceScope.Workspace).markReady();
        getProvider(PreferenceScope.Folder).markReady();
        try {
            await prefService.ready;
        } catch (e) {
            console.error(e);
        }
    });

    afterEach(() => {
    });

    // Actually run the test suite with different parameters:
    testPreferenceProxy('Synchronous Schema Definition + createPreferenceProxy', { asyncSchema: false });
    testPreferenceProxy('Asynchronous Schema Definition (1s delay) + createPreferenceProxy', { asyncSchema: true });
    testPreferenceProxy('Synchronous Schema Definition + Injectable Preference Proxy', { asyncSchema: false, useFactory: true });
    testPreferenceProxy('Asynchronous Schema Definition (1s delay) + Injectable Preference Proxy', { asyncSchema: true, useFactory: true });

    function getProvider(scope: PreferenceScope): MockPreferenceProvider {
        return testContainer.getNamed(PreferenceProvider, scope) as MockPreferenceProvider;
    }

    function testPreferenceProxy(testDescription: string, testOptions: { asyncSchema: boolean, useFactory?: boolean }): void {

        describe(testDescription, () => {

            function getProxy(schema?: PreferenceSchema, options?: PreferenceProxyOptions): {
                proxy: PreferenceProxy<{ [key: string]: any }>,
                /** Only set if we are using a schema asynchronously. */
                promisedSchema?: Promise<PreferenceSchema>
            } {
                const s: PreferenceSchema = schema || {
                    scope: PreferenceScope.Folder,
                    properties: {
                        'my.pref': {
                            type: 'string',
                            default: 'foo'
                        }
                    }
                };
                if (testOptions.asyncSchema) {
                    const promisedSchema = new Promise<PreferenceSchema>(resolve => setTimeout(() => {
                        prefSchema.addSchema(s);
                        resolve(s);
                    }, 500));
                    const proxy = testOptions.useFactory
                        ? testContainer.get<PreferenceProxyFactory>(PreferenceProxyFactory)(promisedSchema, options)
                        : createPreferenceProxy(prefService, promisedSchema, options);
                    return { proxy, promisedSchema };
                } else {
                    prefSchema.addSchema(s);
                    const proxy = testOptions.useFactory
                        ? testContainer.get<PreferenceProxyFactory>(PreferenceProxyFactory)(s, options)
                        : createPreferenceProxy(prefService, s, options);
                    return { proxy };
                }
            }

            if (testOptions.asyncSchema) {
                it('using the proxy before the schema is set should be no-op', async () => {
                    const { proxy, promisedSchema } = getProxy();
                    let changed = 0;
                    proxy.onPreferenceChanged(event => {
                        changed += 1;
                    });
                    expect(proxy['my.pref']).to.equal(undefined);
                    expect(Object.keys(proxy).length).to.equal(0);
                    // The proxy doesn't know the schema, so events shouldn't be forwarded:
                    await getProvider(PreferenceScope.User).setPreference('my.pref', 'bar');
                    expect(changed).to.equal(0);
                    expect(proxy['my.pref']).to.equal(undefined);
                    expect(Object.keys(proxy).length).to.equal(0);
                    // Once the schema is resolved, operations should be working:
                    await promisedSchema!;
                    changed = 0;
                    expect(proxy['my.pref']).to.equal('bar');
                    expect(Object.keys(proxy)).members(['my.pref']);
                    await getProvider(PreferenceScope.User).setPreference('my.pref', 'fizz');
                    expect(changed).to.equal(1);
                    expect(proxy['my.pref']).to.equal('fizz');

                });
            }

            it('by default, it should provide access in flat style but not deep', async () => {
                const { proxy, promisedSchema } = getProxy();
                if (promisedSchema) {
                    await promisedSchema;
                }
                expect(proxy['my.pref']).to.equal('foo');
                expect(proxy.my).to.equal(undefined);
                expect(Object.keys(proxy).join()).to.equal(['my.pref'].join());
            });

            it('supports object introspection through Symbol.toStringTag', async () => {
                const { proxy, promisedSchema } = getProxy();
                if (promisedSchema) {
                    await promisedSchema;
                }
                expect(Object.prototype.toString.call(proxy)).to.equal('[object Object]');
            });

            it('it should provide access in deep style but not flat', async () => {
                const { proxy, promisedSchema } = getProxy(undefined, { style: 'deep' });
                if (promisedSchema) {
                    await promisedSchema;
                }
                expect(proxy['my.pref']).to.equal(undefined);
                expect(proxy.my.pref).to.equal('foo');
                expect(Object.keys(proxy).join()).equal('my');
            });

            it('it should provide access in to both styles', async () => {
                const { proxy, promisedSchema } = getProxy(undefined, { style: 'both' });
                if (promisedSchema) {
                    await promisedSchema;
                }
                expect(proxy['my.pref']).to.equal('foo');
                expect(proxy.my.pref).to.equal('foo');
                expect(Object.keys(proxy).join()).to.equal(['my', 'my.pref'].join());
            });

            it('it should forward change events', async () => {
                const { proxy, promisedSchema } = getProxy(undefined, { style: 'both' });
                if (promisedSchema) {
                    await promisedSchema;
                }
                await waitForEvent(prefService.onPreferenceChanged, 500);
                let theChange: PreferenceChangeEvent<{ [key: string]: any }>;
                proxy.onPreferenceChanged(change => {
                    expect(theChange).to.equal(undefined);
                    theChange = change;
                });
                let theSecondChange: PreferenceChangeEvent<{ [key: string]: any }>;
                (proxy.my as PreferenceProxy<{ [key: string]: any }>).onPreferenceChanged(change => {
                    expect(theSecondChange).to.equal(undefined);
                    theSecondChange = change;
                });

                await getProvider(PreferenceScope.User).setPreference('my.pref', 'bar');

                expect(theChange!.preferenceName).to.equal('my.pref');
                expect(theSecondChange!.preferenceName).to.equal('my.pref');
            });

            it("should not forward changes that don't match the proxy's language override", async () => {
                const { proxy, promisedSchema } = getProxy({
                    scope: PreferenceScope.User,
                    properties: {
                        'my.pref': {
                            type: 'string',
                            default: 'foo',
                            overridable: true,
                        }
                    }
                }, { style: 'both', overrideIdentifier: 'typescript' });
                await promisedSchema;
                let changeEventsEmittedByProxy = 0;
                let changeEventsEmittedByService = 0;
                prefSchema.registerOverrideIdentifier('swift');
                prefSchema.registerOverrideIdentifier('typescript');
                // The service will emit events related to updating the overrides - those are irrelevant
                await waitForEvent(prefService.onPreferenceChanged, 500);

                prefService.onPreferencesChanged(() => changeEventsEmittedByService++);
                proxy.onPreferenceChanged(() => changeEventsEmittedByProxy++);
                await prefService.set(prefService.overridePreferenceName({ overrideIdentifier: 'swift', preferenceName: 'my.pref' }), 'boo', PreferenceScope.User);
                expect(changeEventsEmittedByService, 'The service should have emitted an event for the non-matching override.').to.equal(1);
                expect(changeEventsEmittedByProxy, 'The proxy should not have emitted an event for the non-matching override.').to.equal(0);
                await prefService.set('my.pref', 'far', PreferenceScope.User);
                expect(changeEventsEmittedByService, 'The service should have emitted an event for the base name.').to.equal(2);
                expect(changeEventsEmittedByProxy, 'The proxy should have emitted for an event for the base name.').to.equal(1);
                await prefService.set(prefService.overridePreferenceName({ preferenceName: 'my.pref', overrideIdentifier: 'typescript' }), 'faz', PreferenceScope.User);
                expect(changeEventsEmittedByService, 'The service should have emitted an event for the matching override.').to.equal(3);
                expect(changeEventsEmittedByProxy, 'The proxy should have emitted an event for the matching override.').to.equal(2);
                await prefService.set('my.pref', 'yet another value', PreferenceScope.User);
                expect(changeEventsEmittedByService, 'The service should have emitted another event for the base name.').to.equal(4);
                expect(changeEventsEmittedByProxy, 'The proxy should not have emitted an event, because the value for TS has been overridden.').to.equal(2);
            });

            it('`affects` should only return `true` if the language overrides match', async () => {
                const { proxy, promisedSchema } = getProxy({
                    scope: PreferenceScope.User,
                    properties: {
                        'my.pref': {
                            type: 'string',
                            default: 'foo',
                            overridable: true,
                        }
                    }
                }, { style: 'both' });
                await promisedSchema;
                prefSchema.registerOverrideIdentifier('swift');
                prefSchema.registerOverrideIdentifier('typescript');
                let changesNotAffectingTypescript = 0;
                let changesAffectingTypescript = 0;
                proxy.onPreferenceChanged(change => {
                    if (change.affects(undefined, 'typescript')) {
                        changesAffectingTypescript++;
                    } else {
                        changesNotAffectingTypescript++;
                    }
                });
                await prefService.set('my.pref', 'bog', PreferenceScope.User);
                expect(changesNotAffectingTypescript, 'Two events (one for `my.pref` and one for `[swift].my.pref`) should not have affected TS').to.equal(2);
                expect(changesAffectingTypescript, 'One event should have been fired that does affect typescript.').to.equal(1);
            });

            it('toJSON with deep', async () => {
                const { proxy, promisedSchema } = getProxy({
                    scope: PreferenceScope.Default,
                    properties: {
                        'foo.baz': {
                            type: 'number',
                            default: 4
                        },
                        'foo.bar.x': {
                            type: 'boolean',
                            default: true
                        },
                        'foo.bar.y': {
                            type: 'boolean',
                            default: false
                        },
                        'a': {
                            type: 'string',
                            default: 'a'
                        }
                    }
                }, { style: 'deep' });
                if (promisedSchema) {
                    await promisedSchema;
                }

                const result = JSON.stringify(proxy, undefined, 2);
                assert.equal(result, JSON.stringify({
                    foo: {
                        baz: 4,
                        bar: {
                            x: true,
                            y: false
                        }
                    },
                    a: 'a'
                }, undefined, 2), 'there should not be foo.bar.x to avoid sending excessive data to remote clients');
            });

            it('get nested default', async () => {
                const { proxy, promisedSchema } = getProxy({
                    scope: PreferenceScope.Default,
                    properties: {
                        'foo': {
                            'anyOf': [
                                {
                                    'enum': [
                                        false
                                    ]
                                },
                                {
                                    'properties': {
                                        'bar': {
                                            'anyOf': [
                                                {
                                                    'enum': [
                                                        false
                                                    ]
                                                },
                                                {
                                                    'properties': {
                                                        'x': {
                                                            type: 'boolean'
                                                        },
                                                        'y': {
                                                            type: 'boolean'
                                                        }
                                                    }
                                                }
                                            ]
                                        }
                                    }
                                }
                            ],
                            default: {
                                bar: {
                                    x: true,
                                    y: false
                                }
                            }
                        }
                    }
                }, { style: 'both' });
                if (promisedSchema) {
                    await promisedSchema;
                }
                assert.deepStrictEqual(proxy['foo'], {
                    bar: {
                        x: true,
                        y: false
                    }
                });
                assert.deepStrictEqual(proxy['foo.bar'], {
                    x: true,
                    y: false
                });
                assert.strictEqual(proxy['foo.bar.x'], true);
                assert.strictEqual(proxy['foo.bar.y'], false);
            });
        });
    }

});
