/*
 * Copyright (C) 2021 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/>.
 */
import {TestBed} from '@angular/core/testing';
import {
  SCAboutPageContentType,
  SCIndexResponse,
  SCSettingInputType,
  SCThingOriginType,
  SCThingType,
} from '@openstapps/core';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
import {
  ConfigFetchError,
  ConfigInitError,
  SavedConfigNotAvailable,
  WrongConfigVersionInStorage,
} from './errors';
import {NGXLogger} from 'ngx-logger';
import packageJson from '../../../../package.json';

describe('ConfigProvider', () => {
  let configProvider: ConfigProvider;
  let storageProviderSpy: jasmine.SpyObj<StorageProvider>;

  beforeEach(() => {
    const storageProviderMethodSpy = jasmine.createSpyObj('StorageProvider', [
      'init',
      'get',
      'has',
      'put',
    ]);
    const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', [
      'request',
    ]);
    const ngxLogger: jasmine.SpyObj<NGXLogger> = jasmine.createSpyObj(
      'NGXLogger',
      ['log', 'error', 'warn'],
    );

    TestBed.configureTestingModule({
      imports: [],
      providers: [
        ConfigProvider,
        {
          provide: StorageProvider,
          useValue: storageProviderMethodSpy,
        },
        {
          provide: StAppsWebHttpClient,
          useValue: webHttpClientMethodSpy,
        },
        {
          provide: NGXLogger,
          useValue: ngxLogger,
        },
      ],
    });

    configProvider = TestBed.get(ConfigProvider);
    storageProviderSpy = TestBed.get(StorageProvider);
  });

  it('should fetch app configuration', async () => {
    spyOn(configProvider.client, 'handshake').and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    const result = await configProvider.fetch();
    expect(result).toEqual(sampleIndexResponse);
  });

  it('should throw error on fetch with error response', async () => {
    spyOn(configProvider.client, 'handshake').and.throwError('');
    // eslint-disable-next-line unicorn/error-message
    let error = new Error('');
    try {
      await configProvider.fetch();
    } catch (error_) {
      error = error_;
    }
    expect(error).toEqual(new ConfigFetchError());
  });

  it('should init from remote and saved config not available', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(false));
    spyOn(configProvider.client, 'handshake').and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    try {
      await configProvider.init();
    } catch (error) {
      expect(error).toEqual(new SavedConfigNotAvailable());
    }
    expect(storageProviderSpy.has).toHaveBeenCalled();
    expect(storageProviderSpy.get).toHaveBeenCalledTimes(0);
    expect(configProvider.client.handshake).toHaveBeenCalled();
    expect(configProvider.initialised).toBe(true);
    expect(await configProvider.getValue('name')).toEqual(
      sampleIndexResponse.app.name,
    );
  });

  it('should init from storage when remote fails', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    spyOn(configProvider.client, 'handshake').and.throwError('');
    // eslint-disable-next-line unicorn/error-message
    let error = new Error('');
    try {
      await configProvider.init();
    } catch (error_) {
      error = error_;
    }
    expect(error).toEqual(new ConfigFetchError());
    expect(storageProviderSpy.has).toHaveBeenCalled();
    expect(storageProviderSpy.get).toHaveBeenCalled();
    expect(configProvider.initialised).toBe(true);
    expect(await configProvider.getValue('name')).toEqual(
      sampleIndexResponse.app.name,
    );
  });

  it('should throw error on failed initialisation', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(false));
    spyOn(configProvider.client, 'handshake').and.throwError('');
    // eslint-disable-next-line unicorn/no-null
    let error = null;
    try {
      await configProvider.init();
    } catch (error_) {
      error = error_;
    }
    expect(error).toEqual(new ConfigInitError());
  });

  it('should throw error on wrong config version in storage', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    const wrongConfig = JSON.parse(JSON.stringify(sampleIndexResponse));
    wrongConfig.backend.SCVersion = '0.1.0';
    storageProviderSpy.get.and.returnValue(wrongConfig);
    spyOn(configProvider.client, 'handshake').and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    // eslint-disable-next-line unicorn/no-null
    let error = null;
    try {
      await configProvider.init();
    } catch (error_) {
      error = error_;
    }
    expect(error).toEqual(new WrongConfigVersionInStorage(scVersion, '0.1.0'));
  });

  it('should throw error on saved app configuration not available', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(false));
    // eslint-disable-next-line unicorn/error-message
    let error = new Error('');
    try {
      await configProvider.loadLocal();
    } catch (error_) {
      error = error_;
    }
    expect(error).toEqual(new SavedConfigNotAvailable());
  });

  it('should save app configuration', async () => {
    await configProvider.save(sampleIndexResponse);
    expect(storageProviderSpy.put).toHaveBeenCalledWith(
      STORAGE_KEY_CONFIG,
      sampleIndexResponse,
    );
  });

  it('should set app configuration', async () => {
    await configProvider.set(sampleIndexResponse);
    expect(storageProviderSpy.put).toHaveBeenCalled();
  });

  it('should return app configuration value', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    spyOn(configProvider.client, 'handshake').and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    await configProvider.init();
    expect(await configProvider.getValue('name')).toEqual(
      sampleIndexResponse.app.name,
    );
  });

  it('should return app configuration value if only saved config is available and fetch fails', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(
      Promise.resolve(sampleIndexResponse),
    );
    spyOn(configProvider.client, 'handshake').and.throwError('');
    expect(await configProvider.getValue('name')).toEqual(
      sampleIndexResponse.app.name,
    );
  });
});

const scVersion = packageJson.dependencies['@openstapps/core'];

const sampleIndexResponse: SCIndexResponse = {
  app: {
    aboutPages: {
      about: {
        title: 'About',
        content: [
          {
            value: 'This is the about page',
            type: SCAboutPageContentType.MARKDOWN,
            translations: {
              en: {
                value: 'This is the about page',
              },
            },
          },
        ],
        translations: {
          en: {
            title: 'About',
          },
        },
      },
    },
    campusPolygon: {
      coordinates: [[[1, 2]], [[1, 2]]],
      type: 'Polygon',
    },
    features: {},
    menus: [
      {
        icon: 'icon',
        id: 'main',
        items: [
          {
            icon: 'icon',
            route: '/index',
            title: 'start',
            translations: {
              de: {
                title: 'Start',
              },
              en: {
                title: 'start',
              },
            },
          },
        ],
        name: 'main',
        translations: {
          de: {
            name: 'Haupt',
          },
          en: {
            name: 'main',
          },
        },
      },
    ],
    name: 'StApps',
    privacyPolicyUrl: 'foo.bar',
    settings: [
      {
        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: '',
      },
    ],
  },
  auth: {},
  backend: {
    SCVersion: scVersion,
    externalRequestTimeout: 5000,
    hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
    mappingIgnoredTags: [],
    maxMultiSearchRouteQueries: 5,
    maxRequestBodySize: 512 * 1024,
    name: 'Technische Universität Berlin',
    namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
    sortableFields: [
      {
        fieldName: 'name',
        sortTypes: ['ducet'],
      },
      {
        fieldName: 'type',
        sortTypes: ['ducet'],
      },
      {
        fieldName: 'categories',
        onlyOnTypes: [
          SCThingType.AcademicEvent,
          SCThingType.Building,
          SCThingType.Catalog,
          SCThingType.Dish,
          SCThingType.PointOfInterest,
          SCThingType.Room,
        ],
        sortTypes: ['ducet'],
      },
      {
        fieldName: 'geo.point.coordinates',
        onlyOnTypes: [
          SCThingType.Building,
          SCThingType.PointOfInterest,
          SCThingType.Room,
        ],
        sortTypes: ['distance'],
      },
      {
        fieldName: 'geo.point.coordinates',
        onlyOnTypes: [
          SCThingType.Building,
          SCThingType.PointOfInterest,
          SCThingType.Room,
        ],
        sortTypes: ['distance'],
      },
      {
        fieldName: 'inPlace.geo.point.coordinates',
        onlyOnTypes: [
          SCThingType.DateSeries,
          SCThingType.Dish,
          SCThingType.Floor,
          SCThingType.Organization,
          SCThingType.PointOfInterest,
          SCThingType.Room,
          SCThingType.Ticket,
        ],
        sortTypes: ['distance'],
      },
      {
        fieldName: 'offers',
        onlyOnTypes: [SCThingType.Dish],
        sortTypes: ['price'],
      },
    ],
  },
};
