/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
/* eslint-disable unicorn/no-useless-undefined, @typescript-eslint/no-non-null-assertion */
import {TestBed} from '@angular/core/testing';
import {SCSetting, SCThingOriginType, SCThingType, SCSettingInputType} from '@openstapps/core';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider';
import {SettingsProvider, SettingValuesContainer, STORAGE_KEY_SETTING_VALUES} from './settings.provider';
import {ScheduleSyncService} from '../background/schedule/schedule-sync.service';

describe('SettingsProvider', () => {
  let configProviderSpy: jasmine.SpyObj<ConfigProvider>;
  let settingsProvider: SettingsProvider;
  let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
  let scheduleSyncServiceSpy: jasmine.SpyObj<ScheduleSyncService>;

  beforeEach(async () => {
    storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
    configProviderSpy = jasmine.createSpyObj('ConfigProvider', ['getValue']);
    scheduleSyncServiceSpy = jasmine.createSpyObj('ScheduleSyncService', [
      'getDifferences',
      'postDifferencesNotification',
    ]);

    TestBed.configureTestingModule({
      imports: [],
      providers: [
        SettingsProvider,
        {
          provide: StorageProvider,
          useValue: storageProviderSpy,
        },
        {
          provide: ConfigProvider,
          useValue: configProviderSpy,
        },
        {
          provide: ScheduleSyncService,
          useValue: scheduleSyncServiceSpy,
        },
      ],
    });
    // set settings returned from config
    configProviderSpy.getValue.and.returnValue(CONFIG_SETTINGS_MOCK);
    settingsProvider = TestBed.inject(SettingsProvider);
    storageProviderSpy.has.and.returnValue(Promise.resolve(false));
  });

  it('should provide and get setting', async () => {
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[0]));
    const setting: SCSetting = await settingsProvider.getSetting(
      CONFIG_SETTINGS_MOCK[0].categories[0],
      CONFIG_SETTINGS_MOCK[0].name,
    );
    expect(setting.value).toBeDefined();
  });

  it('should provide and get settings value', async () => {
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[0]));
    const value = await settingsProvider.getValue(
      CONFIG_SETTINGS_MOCK[0].categories[0],
      CONFIG_SETTINGS_MOCK[0].name,
    );
    expect(value).toEqual(CONFIG_SETTINGS_MOCK[0].defaultValue);
  });

  it('should get persisted setting value', async () => {
    // set return values of storage
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(Promise.resolve(SETTING_VALUES_MOCK));

    const value = await settingsProvider.getValue(
      CONFIG_SETTINGS_MOCK[3].categories[0],
      CONFIG_SETTINGS_MOCK[3].name,
    );
    expect(value).toEqual(SETTING_VALUES_MOCK.profile.group as string);
  });

  it('should set default setting value if no persisted value exist', async () => {
    // set return values of spy objects
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(Promise.resolve([]));
    const value = await settingsProvider.getValue(
      CONFIG_SETTINGS_MOCK[3].categories[0],
      CONFIG_SETTINGS_MOCK[3].name,
    );
    expect(value).toEqual(CONFIG_SETTINGS_MOCK[3].defaultValue);
  });

  it('should keep persisted setting values from settings that are not contained in loaded config', async () => {
    const settings = [CONFIG_SETTINGS_MOCK[4], CONFIG_SETTINGS_MOCK[5]];
    configProviderSpy.getValue.and.returnValue(settings);
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(Promise.resolve(SETTING_VALUES_MOCK));
    await settingsProvider.init();
    expect(storageProviderSpy.put).toHaveBeenCalledWith(STORAGE_KEY_SETTING_VALUES, SETTING_VALUES_MOCK);
  });

  it('should set value of a provided setting', async () => {
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[1]));
    await settingsProvider.setSettingValue(
      CONFIG_SETTINGS_MOCK[1].categories[0],
      CONFIG_SETTINGS_MOCK[1].name,
      'updated',
    );
    const value = await settingsProvider.getValue(
      CONFIG_SETTINGS_MOCK[1].categories[0],
      CONFIG_SETTINGS_MOCK[1].name,
    );
    expect(value).toEqual('updated');
  });

  it('should return copy of settingsCache', async () => {
    const category = CONFIG_SETTINGS_MOCK[0].categories[0];
    const name = CONFIG_SETTINGS_MOCK[0].name;
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[0]));
    const settings = await settingsProvider.getCache();
    settings[category].settings[name].value = 'testValue';
    // cached setting value should still be defaultValue
    expect(await settingsProvider.getValue(category, name)).toEqual(CONFIG_SETTINGS_MOCK[0].defaultValue);
  });

  it('should call storage put on setSettingValue', async () => {
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[0]));
    await settingsProvider.setSettingValue(
      CONFIG_SETTINGS_MOCK[0].categories[0],
      CONFIG_SETTINGS_MOCK[0].name,
      '',
    );
    expect(storageProviderSpy.put).toHaveBeenCalled();
  });

  it('should clear settings', async () => {
    await settingsProvider.reset();
    expect(storageProviderSpy.put).toHaveBeenCalledWith(STORAGE_KEY_SETTING_VALUES, {});
  });

  it('should reset settings', async () => {
    const category = CONFIG_SETTINGS_MOCK[0].categories[0];
    const name = CONFIG_SETTINGS_MOCK[0].name;
    await settingsProvider.provideSetting(structuredClone(CONFIG_SETTINGS_MOCK[0]));
    await settingsProvider.setSettingValue(category, name, 'guest');
    await settingsProvider.resetDefault();
    const value = await settingsProvider.getValue(
      CONFIG_SETTINGS_MOCK[0].categories[0],
      CONFIG_SETTINGS_MOCK[0].name,
    );
    expect(value).toEqual(CONFIG_SETTINGS_MOCK[0].defaultValue);
  });

  it('should validate wrong values for inputType text', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[0], 123_456_789);
    await testValue(CONFIG_SETTINGS_MOCK[0], false);
    await testValue(CONFIG_SETTINGS_MOCK[0], []);
  });

  it('should validate wrong values for inputType password', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[0], 123_456_789);
    await testValue(CONFIG_SETTINGS_MOCK[0], false);
    await testValue(CONFIG_SETTINGS_MOCK[0], []);
  });

  it('should validate wrong values for inputType number', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[2], '');
    await testValue(CONFIG_SETTINGS_MOCK[2], false);
    await testValue(CONFIG_SETTINGS_MOCK[2], []);
  });

  it('should validate wrong values for inputType singleChoice text', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[3], '');
    await testValue(CONFIG_SETTINGS_MOCK[3], 123_456);
    await testValue(CONFIG_SETTINGS_MOCK[3], false);
    await testValue(CONFIG_SETTINGS_MOCK[3], []);
  });

  it('should validate wrong values for inputType singleChoice boolean', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[5], '');
    await testValue(CONFIG_SETTINGS_MOCK[5], 123_456);
    await testValue(CONFIG_SETTINGS_MOCK[5], []);
  });

  it('should validate wrong values for inputType multipleChoice', async () => {
    await testValue(CONFIG_SETTINGS_MOCK[6], '');
    await testValue(CONFIG_SETTINGS_MOCK[6], 123_456);
    await testValue(CONFIG_SETTINGS_MOCK[6], false);
    await testValue(CONFIG_SETTINGS_MOCK[6], [1, 9]);
  });

  /**
   * TODO
   */
  async function testValue(setting: SCSetting, value: unknown) {
    let error: Error | undefined = undefined;
    await settingsProvider.provideSetting(structuredClone(setting));
    try {
      await settingsProvider.setSettingValue(setting.categories[0], setting.name, value as never);
    } catch (error_) {
      error = error_ as Error;
    }

    expect(error).toBeDefined();
    expect(error!.message).toMatch(/is not valid/);
  }

  const CONFIG_SETTINGS_MOCK: SCSetting[] = [
    {
      categories: ['credentials'],
      defaultValue: '',
      inputType: SCSettingInputType.Text,
      name: 'username',
      order: 0,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          name: 'Benutzername',
        },
        en: {
          name: 'Username',
        },
      },
      type: SCThingType.Setting,
      uid: '',
    },
    {
      categories: ['credentials'],
      description: '',
      defaultValue: '',
      inputType: SCSettingInputType.Password,
      name: 'password',
      order: 1,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          name: 'Passwort',
        },
        en: {
          name: 'Password',
        },
      },
      type: SCThingType.Setting,
      uid: '',
    },
    {
      categories: ['profile'],
      defaultValue: 0,
      description: '',
      inputType: SCSettingInputType.Number,
      name: 'age',
      order: 0,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          name: 'Alter',
        },
        en: {
          name: 'Age',
        },
      },
      type: SCThingType.Setting,
      uid: '',
    },
    {
      categories: ['profile'],
      description: '',
      defaultValue: 'student',
      inputType: SCSettingInputType.SingleChoice,
      name: 'group',
      order: 1,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          description:
            'Mit welcher Benutzergruppe soll die App verwendet werden?' +
            ' Die Einstellung wird beispielsweise für die Vorauswahl der Preiskategorie der Mensa verwendet.',
          name: 'Gruppe',
        },
        en: {
          description:
            'The user group the app is going to use primarily.' +
            ' This settings for example is getting used for the predefined price category of mensa meals.',
          name: 'Group',
        },
      },
      type: SCThingType.Setting,
      uid: '',
      values: ['student', 'employee', 'guest'],
    },
    {
      categories: ['profile'],
      description: '',
      defaultValue: 'en',
      inputType: SCSettingInputType.SingleChoice,
      name: 'language',
      order: 0,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          description: 'Die Sprache in der die App angezeigt werden soll',
          name: 'Sprache',
        },
        en: {
          description: 'The language this app is going to use',
          name: 'Language',
        },
      },
      type: SCThingType.Setting,
      uid: '',
      values: ['en', 'de'],
    },
    {
      categories: ['privacy'],
      description: '',
      defaultValue: false,
      inputType: SCSettingInputType.SingleChoice,
      name: 'foo',
      order: 0,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          description: 'Foo Beschreibung',
          name: 'Foo',
        },
        en: {
          description: 'Foo Description',
          name: 'Foo',
        },
      },
      type: SCThingType.Setting,
      uid: '',
      values: [true, false],
    },
    {
      categories: ['others'],
      defaultValue: [],
      description: '',
      inputType: SCSettingInputType.MultipleChoice,
      name: 'numbers',
      order: 0,
      origin: {
        indexed: '2018-09-11T12:30:00Z',
        name: 'Dummy',
        type: SCThingOriginType.Remote,
      },
      translations: {
        de: {
          description: 'Test für multiple select Feld',
          name: 'Nummern',
        },
        en: {
          description: 'Test for multiple select field',
          name: 'Numbers',
        },
      },
      type: SCThingType.Setting,
      uid: '',
      values: [1, 2, 3, 4, 5, 6, 7, 8],
    },
  ];
});

const SETTING_VALUES_MOCK: SettingValuesContainer = {
  foo: {
    bar: 'foo-bar',
  },
  privacy: {
    foo: true,
  },
  profile: {
    group: 'employee',
    language: 'de',
  },
};
