/*
 * 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/>.
 */
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-explicit-any, unicorn/no-thenable */
import {TestBed} from '@angular/core/testing';
import {Client} from '@openstapps/api';
import {
  SCDish,
  SCMessage,
  SCMultiSearchRequest,
  SCSaveableThing,
  SCSearchQuery,
  SCSearchResponse,
  SCSearchValueFilter,
  SCThingOriginType,
  SCThingType,
} from '@openstapps/core';
import {sampleThingsMap} from '../../_helpers/data/sample-things';
import {StorageProvider} from '../storage/storage.provider';
import {DataModule} from './data.module';
import {DataProvider, DataScope} from './data.provider';
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
import {RouterModule} from '@angular/router';

describe('DataProvider', () => {
  let dataProvider: DataProvider;
  let storageProvider: StorageProvider;
  const sampleThing: SCMessage = sampleThingsMap.message[0] as SCMessage;
  const sampleResponse: SCSearchResponse = {
    data: sampleThingsMap.dish as SCDish[],
    facets: [
      {
        buckets: [],
        field: 'foo',
      },
    ],
    pagination: {
      count: 0,
      offset: 0,
      total: 0,
    },
    stats: {
      time: 123,
    },
  };
  const sampleFilter: SCSearchValueFilter = {
    arguments: {
      field: 'type',
      value: 'dish',
    },
    type: 'value',
  };
  const sampleQuery: SCSearchQuery = {
    filter: sampleFilter,
  };

  const sampleSavable: SCSaveableThing = {
    data: sampleThing,
    name: sampleThing.name,
    origin: {
      created: new Date().toISOString(),
      type: SCThingOriginType.User,
    },
    type: SCThingType.Message,
    uid: sampleThing.uid,
  };

  const fakeStorage = new Map([
    ['foo', 'Bar'],
    ['bar', {foo: 'BarFoo'} as any],
  ]);

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [DataModule, LoggerModule.forRoot({level: NgxLoggerLevel.TRACE}), RouterModule.forRoot([])],
      providers: [DataProvider, StAppsWebHttpClient],
    });
    storageProvider = TestBed.inject(StorageProvider);
    dataProvider = TestBed.inject(DataProvider);
  });

  it('should generate data key', async () => {
    dataProvider.storagePrefix = 'foo.data';

    expect(dataProvider.getDataKey('123')).toBe('foo.data.123');
  });

  it('should provide backend data items using search query', async () => {
    spyOn(Client.prototype as any, 'search').and.callFake(() => {
      return {
        then: (callback: any) => {
          return callback(sampleResponse);
        },
      };
    });
    const response = await dataProvider.search(sampleQuery);

    expect(response).toEqual(sampleResponse);
  });

  it('should provide backend data items using multi search query', async () => {
    spyOn(Client.prototype as any, 'multiSearch').and.callFake(() => ({
      then: (callback: any) => {
        return callback({
          a: sampleResponse,
        });
      },
    }));
    const response = await dataProvider.multiSearch({a: sampleQuery});

    expect(response).toEqual({a: sampleResponse});
  });

  it('should partition search requests correctly', async () => {
    const request = {
      a: 'a',
      b: 'b',
      c: 'c',
      d: 'd',
      e: 'e',
    } as SCMultiSearchRequest; // and response...
    const requestCheck = Object.assign({}, request);
    const responseShould = {
      a: 'A',
      b: 'B',
      c: 'C',
      d: 'D',
      e: 'E',
    };

    dataProvider.backendQueriesLimit = 2;
    spyOn(Client.prototype as any, 'multiSearch').and.callFake((request_: SCMultiSearchRequest) => ({
      then: (callback: any) => {
        let i = 0;
        for (const key in request_) {
          if (request_.hasOwnProperty(key)) {
            i++;

            expect(requestCheck[key]).not.toBeNull();
            expect(requestCheck[key]).toEqual(request_[key]);

            // @ts-expect-error is not null
            // eslint-disable-next-line unicorn/no-null
            requestCheck[key] = null;
            // @ts-expect-error is a string for test purposes
            request_[key] = request_[key].toUpperCase();
          }
        }
        expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit);

        return callback(request_);
      },
    }));
    const response = await dataProvider.multiSearch(request);

    // @ts-expect-error same type
    expect(response).toEqual(responseShould);
  });

  it('should put an data item into the local database (storage)', async () => {
    let providedThing: SCSaveableThing;
    spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {
      providedThing = thing;
      providedThing.origin.created = sampleSavable.origin.created;
    });

    expect(storageProvider.put).not.toHaveBeenCalled();
    expect(providedThing!).not.toBeDefined();

    await dataProvider.put(sampleThing);

    expect(providedThing!).toBeDefined();
    expect(providedThing!).toEqual(sampleSavable);
  });

  it('should correctly set and get single data item from the local database (storage)', async () => {
    spyOn(storageProvider, 'get').and.returnValue((async () => sampleSavable)());

    expect(storageProvider.get).not.toHaveBeenCalled();

    const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Local);

    expect(storageProvider.get).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid));
    expect(providedThing).toEqual(sampleSavable);
  });

  it('should provide all data items from the local database (storage)', async () => {
    const fakeStorage = new Map([
      ['foo', 'Bar'],
      ['bar', {foo: 'BarFoo'} as any],
    ]);
    spyOn(storageProvider, 'search').and.callFake(async () => {
      return fakeStorage;
    });

    const result = await dataProvider.getAll();

    expect([...result.keys()].sort()).toEqual([...fakeStorage.keys()].sort());
    expect([...result.values()].sort()).toEqual([...fakeStorage.values()].sort());
  });

  it('should provide single data from the backend', async () => {
    spyOn(Client.prototype, 'getThing' as any).and.callFake(() => {
      return {
        then: (callback: any) => {
          return callback(sampleThing);
        },
      };
    });

    expect(Client.prototype.getThing).not.toHaveBeenCalled();

    const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Remote);

    expect(Client.prototype.getThing).toHaveBeenCalledWith(sampleThing.uid);
    expect(providedThing).toBe(sampleThing);
  });

  it('should get an item from both local and remote database', async () => {
    spyOn(Client.prototype, 'getThing' as any).and.callFake(() => {
      return {
        then: (callback: any) => {
          return callback(sampleThing);
        },
      };
    });
    spyOn(storageProvider, 'get' as any).and.callFake(() => {
      return {
        then: (callback: any) => {
          return callback(sampleSavable);
        },
      };
    });
    const result = await dataProvider.get(sampleThing.uid);

    expect(result.get(DataScope.Local)).toEqual(sampleSavable);
    expect(result.get(DataScope.Remote)).toEqual(sampleThing);
  });

  it('should properly delete a data item from the local database (storage)', async () => {
    spyOn(storageProvider, 'delete');

    await dataProvider.delete(sampleThing.uid);

    expect(storageProvider.delete).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid));
  });

  it('should properly delete all the data items from the local database (storage)', async () => {
    spyOn(storageProvider, 'delete');
    spyOn(storageProvider, 'search').and.callFake(async () => {
      return fakeStorage;
    });
    await dataProvider.deleteAll();

    expect(storageProvider.delete).toHaveBeenCalledWith('foo', 'bar');
  });

  it('should properly check if a data item has already been saved', async () => {
    spyOn(storageProvider, 'has').and.callFake(async storageKey => {
      return (async () => {
        return dataProvider.getDataKey(sampleThing.uid) === storageKey;
      })();
    });

    expect(await dataProvider.isSaved('some-uuid')).toBeFalsy();
    expect(await dataProvider.isSaved(sampleThing.uid)).toBeTruthy();
  });
});
