/*
 * Copyright (C) 2023 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 {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 {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
import {BehaviorSubject} from 'rxjs';
import {InternetConnectionService} from '../../util/internet-connection.service';

describe('ConfigProvider', () => {
  let internetConnectionServiceMock: {offline$: BehaviorSubject<boolean>};
  let configProvider: ConfigProvider;
  let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
  let ngxLogger: jasmine.SpyObj<NGXLogger>;

  beforeEach(() => {
    internetConnectionServiceMock = {offline$: new BehaviorSubject<boolean>(false)};
    storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
    const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', ['request']);
    ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);

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

    configProvider = TestBed.inject(ConfigProvider);
  });

  it('should fetch app configuration', async () => {
    spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
    await configProvider.fetch();
    const result = configProvider.config;
    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_ as Error;
    }
    expect(error).toEqual(new ConfigFetchError());
  });

  it('should throw device offline error when offline', async () => {
    // eslint-disable-next-line unicorn/error-message
    let error = new Error('');
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    internetConnectionServiceMock.offline$ = new BehaviorSubject<boolean>(true);

    try {
      await configProvider.fetch();
    } catch (error_) {
      error = error_ as Error;
      expect(error).toBeInstanceOf(ConfigFetchError);
      expect(error.message).toContain('Device is offline.');
    }
  });

  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.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 = structuredClone(sampleIndexResponse);
    wrongConfig.backend.SCVersion = '0.1.0';
    storageProviderSpy.get.and.returnValue(Promise.resolve(wrongConfig));
    spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
    // sets StApps core version which has a different major version
    configProvider.scVersion = '1.0.0';
    await configProvider.init();

    expect(ngxLogger.warn).toHaveBeenCalledWith(
      new WrongConfigVersionInStorage(configProvider.scVersion, wrongConfig.backend.SCVersion),
    );
  });

  it('should load from storage if offline and same major version stored', async () => {
    internetConnectionServiceMock = {offline$: new BehaviorSubject<boolean>(true)};
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    const configInStorage = structuredClone(sampleIndexResponse);
    configInStorage.backend.SCVersion = '1.0.0';
    storageProviderSpy.get.and.returnValue(Promise.resolve(configInStorage));
    // sets StApps core version which has the same major version
    configProvider.scVersion = '1.1.1';
    await configProvider.init();

    expect(configProvider.getAnyValue('app')).toEqual(configInStorage.app);
  });

  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_ as 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 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('');
    await configProvider.init();

    expect(configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);
  });

  it('should fetch new config from remote on init', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(Promise.resolve(sampleIndexResponse));
    spyOn(configProvider, 'fetch');
    await configProvider.init();

    expect(configProvider.fetch).toHaveBeenCalled();
  });

  it('should update the local config with the one from remote', async () => {
    storageProviderSpy.has.and.returnValue(Promise.resolve(true));
    storageProviderSpy.get.and.returnValue(Promise.resolve(sampleIndexResponse));
    const newConfig = structuredClone(sampleIndexResponse);
    newConfig.app.name = 'New app name';
    spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(newConfig));
    await configProvider.init();

    // Validate that the initial configuration is loaded
    expect(configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);

    // Fetch the new configuration from the remote
    await configProvider.fetch();

    // Validate that the new configuration is now set
    expect(configProvider.getValue('name')).toEqual(newConfig.app.name);
  });
});
