/*
 * 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 @typescript-eslint/no-explicit-any */
import {TestBed} from '@angular/core/testing';
import {Storage} from '@ionic/storage-angular';
import {StorageModule} from './storage.module';
import {StorageProvider} from './storage.provider';

describe('StorageProvider', () => {
  let storageProvider: StorageProvider;
  let storage: Storage;
  let sampleEntries: Map<string, any>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [StorageModule],
      providers: [StorageProvider],
    });
    storageProvider = TestBed.inject(StorageProvider);
    storage = TestBed.inject(Storage);

    sampleEntries = new Map([
      ['foo', 'Bar'],
      ['bar', {foo: 'BarFoo'} as any],
      ['foo.bar', 123],
    ]);

    spyOn(storage, 'forEach').and.callFake(function_ => {
      let i = 0;
      for (const key of sampleEntries.keys()) {
        function_(sampleEntries.get(key), key, i);
        i++;
      }
      return (async () => {
        /* just return a promise */
      })();
    });
  });

  it('should call ready method of storage on init', async () => {
    // @ts-expect-error no need to return storage for this case
    spyOn(storage, 'create').and.callFake(() => Promise.resolve());
    await storageProvider.init();

    expect(storage.create).toHaveBeenCalled();
  });

  it('should call set method of storage to put a value', async () => {
    spyOn(storage, 'set').and.callFake(() => Promise.resolve());
    await storageProvider.put('some-uid', {some: 'thing'});

    expect(storage.set).toHaveBeenCalledWith('some-uid', {some: 'thing'});
  });

  it('should call get method of storage to get a value', async () => {
    spyOn(storage, 'get').and.callFake(() => Promise.resolve());
    try {
      await storageProvider.get('some-uid');
    } catch {
      // if not caught, causes issues and tests fail
    }

    expect(storage.get).toHaveBeenCalledWith('some-uid');
  });

  it('should properly put and get a value', async () => {
    const fakeStorageSystem = new Map<string, any>();

    spyOn(storage, 'set').and.callFake((id, value) => {
      return (async () => fakeStorageSystem.set(id, value))();
    });
    spyOn(storage, 'get').and.callFake(id => {
      return (async () => fakeStorageSystem.get(id))();
    });

    await storageProvider.init();
    await storageProvider.put('some-uid', {some: 'thing'});
    const result = await storageProvider.get('some-uid');

    expect(result).toEqual({some: 'thing'});
  });

  it('should throw an error when value is null', async () => {
    // eslint-disable-next-line unicorn/error-message
    let error: Error = new Error();
    // eslint-disable-next-line unicorn/no-null
    spyOn(storage, 'get').and.returnValue((async () => null)());
    try {
      await storageProvider.get('something-else');
    } catch (error_) {
      error = error_ as Error;
    }
    expect(error).toEqual(new Error('Value not found.'));
  });

  it('should put multiple values into the storage', async () => {
    spyOn(storageProvider, 'put').and.callFake(() => Promise.resolve());
    await storageProvider.putMultiple(sampleEntries);

    expect(storageProvider.put).toHaveBeenCalledTimes(sampleEntries.size);

    for (const key of sampleEntries.keys()) {
      expect(storageProvider.put).toHaveBeenCalledWith(key, sampleEntries.get(key));
    }
  });

  it('should get multiple values from the storage', async () => {
    spyOn(storageProvider, 'get').and.callFake(id => {
      return (async () => sampleEntries.get(id))();
    });
    const entries = await storageProvider.getMultiple(['foo', 'bar']);

    expect(storageProvider.get).toHaveBeenCalledTimes(2);
    expect(storageProvider.get).toHaveBeenCalledWith('foo');
    expect(storageProvider.get).toHaveBeenCalledWith('bar');

    expect([...entries.values()]).toEqual(['Bar', {foo: 'BarFoo'}]);
    expect([...entries.keys()]).toEqual(['foo', 'bar']);
  });

  it('should get all values from the storage', async () => {
    const allValuesMap = await storageProvider.getAll();

    for (const key of sampleEntries.keys()) {
      expect(allValuesMap.get(key)).toEqual(sampleEntries.get(key));
    }
  });

  it('should delete one or more entries from the storage', async () => {
    const storageRemoveSpy = spyOn(storage, 'remove').and.callFake(() => Promise.resolve());

    await storageProvider.delete('bar');
    expect(storage.remove).toHaveBeenCalledTimes(1);

    storageRemoveSpy.calls.reset();

    await storageProvider.delete('foo', 'foo.bar');
    expect(storage.remove).toHaveBeenCalledTimes(2);
  });

  it('should delete all entries in the storage', async () => {
    spyOn(storage, 'clear').and.callFake(() => Promise.resolve());

    await storageProvider.deleteAll();
    expect(storage.clear).toHaveBeenCalled();
  });

  it('should provide information if storage is empty', async () => {
    let n: number;
    spyOn(storage, 'length').and.callFake(async () => n);

    n = 0;

    const testEmpty = await storageProvider.isEmpty();

    expect(testEmpty).toBeTruthy();

    n = 1;
    expect(await storageProvider.isEmpty()).toBeFalsy();

    expect(storage.length).toHaveBeenCalledTimes(2);
  });

  it('should provide number of entries', async () => {
    const n = 5;
    spyOn(storage, 'length').and.callFake(async () => n);

    expect(await storageProvider.length()).toBe(n);
  });

  it('should provide information if storage contains a specific entry (key)', async () => {
    spyOn(storage, 'keys').and.returnValue((async () => [...sampleEntries.keys()])());

    expect(await storageProvider.has('foo')).toBeTruthy();
    expect(await storageProvider.has('something-else')).toBeFalsy();
  });

  it('should allow search by regex', async () => {
    const found: Map<string, any> = await storageProvider.search<any>(/bar/);

    expect([...found.keys()].sort()).toEqual(['bar', 'foo.bar']);
    expect([...found.values()]).toEqual([{foo: 'BarFoo'}, 123]);
  });

  it('should allow search by string', async () => {
    spyOn(storage, 'get').and.callFake(id => {
      return (async () => sampleEntries.get(id))();
    });
    const found: Map<string, any> = await storageProvider.search<any>('foo.ba');

    expect([...found.keys()]).toEqual(['foo.bar']);
    expect([...found.values()]).toEqual([123]);
  });
});
