import { SpectatorService, createServiceFactory, SpyObject } from '@ngneat/spectator';
import { UISettingsDTO } from '@swagger/cat';
import { UiFilter } from '@ui/filter';
import { SearchComponentStoreService } from './search-component-store.service';
import { SEARCH_STATE_SEARCH_SERVICE, SEARCH_STATE_SETTINGS_LOADER } from './tokens';
import { SearchState, SearchStateSearchResult, SearchStateSearchService, SearchStateSettingsLoader } from './defs';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { delay } from 'rxjs/operators';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from '@core/cache';

describe('SearchComponentStoreService', () => {
  let spectator: SpectatorService<SearchComponentStoreService>;
  let settingsLoader: SearchStateSettingsLoader;
  let searchService: SearchStateSearchService;
  let uiModalService: SpyObject<UiModalService>;
  let cacheService: SpyObject<CacheService>;
  const createService = createServiceFactory({
    service: SearchComponentStoreService,
    providers: [
      { provide: SEARCH_STATE_SETTINGS_LOADER, useValue: { load: () => null } },
      { provide: SEARCH_STATE_SEARCH_SERVICE, useValue: { search: () => null } },
    ],
    mocks: [UiModalService, CacheService],
  });

  beforeEach(() => {
    spectator = createService();
    settingsLoader = spectator.inject(SEARCH_STATE_SETTINGS_LOADER);
    searchService = spectator.inject(SEARCH_STATE_SEARCH_SERVICE);
    uiModalService = spectator.inject(UiModalService);
    cacheService = spectator.inject(CacheService);
    spyOn(spectator.service, 'patchState').and.callThrough();
  });

  it('should create', () => {
    expect(spectator.service).toBeTruthy();
  });

  describe('get state', () => {
    it('should return the state', () => {
      const state = { items: [], hits: 0 };
      spectator.service.setState(state);
      expect(spectator.service.state).toBe(state);
    });
  });

  describe('get state$', () => {
    it('should return the state$', () => {
      const state = { items: [], hits: 0 };
      spectator.service.setState(state);

      spectator.service.state$
        .subscribe((s) => {
          expect(s).toBe(state);
        })
        .unsubscribe();
    });
  });

  describe('get filter', () => {
    it('should return the filter', () => {
      const filter = new UiFilter();
      spectator.service.setFilter(filter);
      expect(spectator.service.filter).toBe(filter);
    });
  });

  describe('get filter$', () => {
    it('should return the filter$', () => {
      const filter = new UiFilter();
      spectator.service.setFilter(filter);

      spectator.service.filter$
        .subscribe((f) => {
          expect(f).toBe(filter);
        })
        .unsubscribe();
    });
  });

  describe('get status', () => {
    it('should return the status', () => {
      spectator.service.setStatus('fetching');
      expect(spectator.service.status).toBe('fetching');
    });
  });

  describe('get status$', () => {
    it('should return the status$', () => {
      spectator.service.setStatus('fetching');

      spectator.service.status$
        .subscribe((s) => {
          expect(s).toBe('fetching');
        })
        .unsubscribe();
    });
  });

  describe('get fetching', () => {
    it('should return the fetching', () => {
      spectator.service.setStatus('fetching');
      expect(spectator.service.fetching).toBe(true);
    });
  });

  describe('get fetching$', () => {
    it('should return the fetching$', () => {
      spectator.service.setStatus('fetching');

      spectator.service.fetching$
        .subscribe((s) => {
          expect(s).toBe(true);
        })
        .unsubscribe();
    });
  });

  describe('get error', () => {
    it('should return the error', () => {
      spectator.service.setStatus('error');
      expect(spectator.service.error).toBe(true);
    });
  });

  describe('get error$', () => {
    it('should return the error$', () => {
      spectator.service.setStatus('error');

      spectator.service.error$
        .subscribe((s) => {
          expect(s).toBe(true);
        })
        .unsubscribe();
    });
  });

  describe('get message', () => {
    it('should return the message', () => {
      spectator.service.setMessage('message');
      expect(spectator.service.message).toBe('message');
    });
  });

  describe('get message$', () => {
    it('should return the message$', () => {
      spectator.service.setMessage('message');

      spectator.service.message$
        .subscribe((s) => {
          expect(s).toBe('message');
        })
        .unsubscribe();
    });
  });

  describe('get items', () => {
    it('should return the items', () => {
      spectator.service.setItems([{}, {}]);
      expect(spectator.service.items).toEqual([{}, {}]);
    });
  });

  describe('get items$', () => {
    it('should return the items$', () => {
      spectator.service.setItems([{}, {}]);

      spectator.service.items$
        .subscribe((s) => {
          expect(s).toEqual([{}, {}]);
        })
        .unsubscribe();
    });
  });

  describe('get hits', () => {
    it('should return the hits', () => {
      spectator.service.setHits(5);
      expect(spectator.service.hits).toEqual(5);
    });
  });

  describe('get hits$', () => {
    it('should return the hits$', () => {
      spectator.service.setHits(5);

      spectator.service.hits$
        .subscribe((s) => {
          expect(s).toEqual(5);
        })
        .unsubscribe();
    });
  });

  describe('setItems()', () => {
    it('should set the items', () => {
      spectator.service.setItems([{}, {}]);
      expect(spectator.service.items).toEqual([{}, {}]);
      expect(spectator.service.patchState).toHaveBeenCalledWith({ items: [{}, {}] });
    });
  });

  describe('appendItems()', () => {
    it('should append the items', () => {
      spyOnProperty(spectator.service, 'items', 'get').and.returnValue([{ name: 'item1' }, { name: 'item2' }]);
      spectator.service.appendItems([{ name: 'item3' }, { name: 'item4' }]);
      expect(spectator.service.patchState).toHaveBeenCalledWith({
        items: [{ name: 'item1' }, { name: 'item2' }, { name: 'item3' }, { name: 'item4' }],
      });
    });
  });

  describe('clearItems()', () => {
    it('should clear the items', () => {
      spectator.service.setItems([{}, {}]);
      spectator.service.clearItems();
      expect(spectator.service.items).toEqual([]);
      expect(spectator.service.patchState).toHaveBeenCalledWith({ items: [] });
    });
  });

  describe('setStatus()', () => {
    it('should set the status fetching', () => {
      spectator.service.setStatus('fetching');
      expect(spectator.service.status).toBe('fetching');
      expect(spectator.service.patchState).toHaveBeenCalledWith({ status: 'fetching' });
    });
    it('should set the status error', () => {
      spectator.service.setStatus('error');
      expect(spectator.service.status).toBe('error');
      expect(spectator.service.patchState).toHaveBeenCalledWith({ status: 'error' });
    });
  });

  describe('setHits()', () => {
    it('should set the hits', () => {
      spectator.service.setHits(2);
      expect(spectator.service.patchState).toHaveBeenCalledWith({ hits: 2 });
    });
  });

  describe('setMessage()', () => {
    it('should set the message', () => {
      spectator.service.setMessage('Test');
      expect(spectator.service.patchState).toHaveBeenCalledWith({ message: 'Test' });
    });
  });

  describe('reset()', () => {
    it('should reset the state without defaults', () => {
      spyOn(spectator.service, 'setState');
      spectator.service.reset();
      expect(spectator.service.setState).toHaveBeenCalledWith({ items: [], hits: 0 });
    });

    it('should reset the state with defaults', () => {
      spyOn(spectator.service, 'setState');
      spectator.service.reset({ hits: 2, message: 'Test' });
      expect(spectator.service.setState).toHaveBeenCalledWith({ items: [], hits: 2, message: 'Test' });
    });
  });

  describe('setDefaultFilter()', () => {
    it('should set the filter with default settings', async () => {
      spyOn(spectator.service, 'getDefaultSettings').and.returnValue(Promise.resolve({ imageUrl: 'testUrl' }));
      spyOn(spectator.service, 'setFilter').and.callThrough();
      await spectator.service.setDefaultFilter();
      expect(spectator.service.getDefaultSettings).toHaveBeenCalled();
      expect(spectator.service.setFilter).toHaveBeenCalledWith({ imageUrl: 'testUrl' });
    });
  });

  describe('setFilter()', () => {
    it('should set the filter with UiFilter as argument', () => {
      const filter = new UiFilter();
      spectator.service.setFilter(filter);
      expect(spectator.service.patchState).toHaveBeenCalledWith({ filter });
    });

    it('should set the filter with UISettingsDTO as argument', () => {
      const settings: UISettingsDTO = { imageUrl: 'testUrl' };
      const filter = new UiFilter();
      spyOn(UiFilter, 'create').and.returnValue(filter);
      spectator.service.setFilter(settings);
      expect(spectator.service.patchState).toHaveBeenCalledWith({ filter });
      expect(UiFilter.create).toHaveBeenCalledWith(settings);
    });
  });

  describe('getDefaultSettings()', () => {
    let settingsLoaderSpy: jasmine.Spy<() => Promise<UISettingsDTO> | Observable<UISettingsDTO> | UISettingsDTO>;

    beforeEach(() => {
      settingsLoaderSpy = spyOn(settingsLoader, 'load');
    });

    it('should call _settingsLoader.load()', () => {
      spectator.service.getDefaultSettings();
      expect(settingsLoader.load).toHaveBeenCalled();
    });

    it('should check if load() returns an Observable and return the result as a Promise', async () => {
      settingsLoaderSpy.and.returnValue(of({ imageUrl: 'testUrl' }));

      const settingsPromise = spectator.service.getDefaultSettings();
      const settings = await settingsPromise;

      expect(settingsPromise).toBeInstanceOf(Promise);
      expect(settings).toEqual({ imageUrl: 'testUrl' });
    });

    it('should check if load() returns a Subject and return the result as a Promise', async () => {
      const subject = new BehaviorSubject({ imageUrl: 'testUrl' });
      settingsLoaderSpy.and.returnValue(subject);

      const settingsPromise = spectator.service.getDefaultSettings();
      subject.complete();
      const settings = await settingsPromise;

      expect(settingsPromise).toBeInstanceOf(Promise);
      expect(settings).toEqual({ imageUrl: 'testUrl' });
    });

    it('should check if load() returns a Promise and return the result as a Promise', async () => {
      settingsLoaderSpy.and.returnValue(Promise.resolve({ imageUrl: 'testUrl' }));

      const settingsPromise = spectator.service.getDefaultSettings();
      const settings = await settingsPromise;

      expect(settingsPromise).toBeInstanceOf(Promise);
      expect(settings).toEqual({ imageUrl: 'testUrl' });
    });

    it('should check if load() returns a Object and return the result as a Promise', async () => {
      settingsLoaderSpy.and.returnValue({ imageUrl: 'testUrl' });

      const settingsPromise = spectator.service.getDefaultSettings();
      const settings = await settingsPromise;

      expect(settingsPromise).toBeInstanceOf(Promise);
      expect(settings).toEqual({ imageUrl: 'testUrl' });
    });
  });

  describe('searchRequest()', () => {
    let searchServiceSpy: jasmine.Spy<() =>
      | Promise<SearchStateSearchResult>
      | Observable<SearchStateSearchResult>
      | SearchStateSearchResult>;

    beforeEach(() => {
      searchServiceSpy = spyOn(searchService, 'search');
    });

    it('should call _searchService.search with filter and skip of items.length', () => {
      const filter = new UiFilter();
      spectator.service.searchRequest(filter, [{ name: 'Test' }]);
      expect(searchService.search).toHaveBeenCalledWith(filter, { skip: 1 });
    });

    it('should check if search() returns an Observable and return the result as an Observable', (done) => {
      const filter = new UiFilter();
      searchServiceSpy.and.returnValue(of({ message: 'Testmessage' }).pipe(delay(1)));

      const searchRequest = spectator.service.searchRequest(filter, []);

      expect(searchRequest).toBeInstanceOf(Observable);

      searchRequest.subscribe((result) => {
        expect(result).toEqual({ message: 'Testmessage' });
        done();
      });
    });

    it('should check if search() returns an Promise and return the result as an Observable', (done) => {
      const filter = new UiFilter();
      searchServiceSpy.and.returnValue(Promise.resolve({ message: 'Testmessage' }));

      const searchRequest = spectator.service.searchRequest(filter, []);

      expect(searchRequest).toBeInstanceOf(Observable);

      searchRequest.subscribe((result) => {
        expect(result).toEqual({ message: 'Testmessage' });
        done();
      });
    });

    it('should check if search() returns an Object and return the result as an Observable', (done) => {
      const filter = new UiFilter();
      searchServiceSpy.and.returnValue({ message: 'Testmessage' });

      const searchRequest = spectator.service.searchRequest(filter, []);

      expect(searchRequest).toBeInstanceOf(Observable);

      searchRequest.subscribe((result) => {
        expect(result).toEqual({ message: 'Testmessage' });
        done();
      });
    });
  });

  describe('handleSearchResponse()', () => {
    it('should set the status "error" and message "Keine Suchergebnisse" if res.hits is 0', () => {
      const res: SearchStateSearchResult = {
        hits: 0,
      };
      spyOn(spectator.service, 'setStatus');
      spyOn(spectator.service, 'setMessage');
      spectator.service.handleSearchResponse(res);
      expect(spectator.service.setStatus).toHaveBeenCalledWith('error');
      expect(spectator.service.setMessage).toHaveBeenCalledWith('Keine Suchergebnisse');
    });

    it('should reset status and message and append items and set hits and if res.hits is not 0', () => {
      const res: SearchStateSearchResult = {
        hits: 3,
        items: [{}],
      };
      spyOn(spectator.service, 'setStatus');
      spyOn(spectator.service, 'setMessage');
      spyOn(spectator.service, 'appendItems');
      spyOn(spectator.service, 'setHits');
      spectator.service.handleSearchResponse(res);
      expect(spectator.service.setStatus).toHaveBeenCalledWith(undefined);
      expect(spectator.service.setMessage).toHaveBeenCalledWith(undefined);
      expect(spectator.service.appendItems).toHaveBeenCalledWith([{}]);
      expect(spectator.service.setHits).toHaveBeenCalledWith(3);
    });
  });

  describe('handleSearchError()', () => {
    it('should call setStatus and open error dialog', () => {
      const err = {
        message: 'Test',
      };
      spyOn(spectator.service, 'setStatus');
      spectator.service.handleSearchError(err);
      expect(spectator.service.setStatus).toHaveBeenCalledWith('error');
      expect(uiModalService.open).toHaveBeenCalledWith({
        content: UiErrorModalComponent,
        title: 'Suche Fehlgeschlagen',
        data: err,
      });
    });
  });

  describe('search effect', () => {
    let setStatusSpy: jasmine.Spy<(status: undefined | 'fetching' | 'error') => void>;
    let searchRequestSpy: jasmine.Spy<(filter: UiFilter, items: any[]) => Observable<SearchStateSearchResult<any>>>;
    let handleSearchResponseSpy: jasmine.Spy<(res: SearchStateSearchResult<any>) => void>;
    let handleSearchErrorSpy: jasmine.Spy<(err: any) => void>;
    let searchCompletedNextSpy: jasmine.Spy<(value: SearchState<any>) => void>;
    let getStateSpy: jasmine.Spy<() => SearchState<any>>;
    let clearItemsSpy: jasmine.Spy<() => void>;

    beforeEach(() => {
      setStatusSpy = spyOn(spectator.service, 'setStatus');
      searchRequestSpy = spyOn(spectator.service, 'searchRequest');
      handleSearchResponseSpy = spyOn(spectator.service, 'handleSearchResponse');
      handleSearchErrorSpy = spyOn(spectator.service, 'handleSearchError');
      searchCompletedNextSpy = spyOn(spectator.service.searchCompleted, 'next');
      getStateSpy = spyOnProperty(spectator.service, 'state', 'get');
      clearItemsSpy = spyOn(spectator.service, 'clearItems');
    });

    it('should call setStatus, searchRequest, handleSearchResponse and searchCompleted.next after a successful search', () => {
      const filter = new UiFilter();
      const items = [{}];
      searchRequestSpy.and.returnValue(of({ items: [{ name: 'Testobject' }], hits: 1 }));
      getStateSpy.and.returnValue({ items: [{ name: 'Testobject' }], hits: 1 });
      spectator.service.setFilter(filter);
      spectator.service.setItems(items);
      spectator.service.search({});
      expect(spectator.service.clearItems).not.toHaveBeenCalled();
      expect(spectator.service.handleSearchError).not.toHaveBeenCalled();
      expect(spectator.service.setStatus).toHaveBeenCalledWith('fetching');
      expect(spectator.service.searchRequest).toHaveBeenCalledWith(filter, items);
      expect(spectator.service.handleSearchResponse).toHaveBeenCalledWith({ items: [{ name: 'Testobject' }], hits: 1 });
      expect(spectator.service.searchCompleted.next).toHaveBeenCalledWith({ items: [{ name: 'Testobject' }], hits: 1 });
    });

    it('should call clearItems and setStatus, searchRequest, handleSearchResponse and searchCompleted.next after a successful search with options.clear true', () => {
      const filter = new UiFilter();
      const items = [{}];
      searchRequestSpy.and.returnValue(of({ items: [{ name: 'Testobject' }], hits: 1 }));
      getStateSpy.and.returnValue({ items: [{ name: 'Testobject' }], hits: 1 });
      spectator.service.setFilter(filter);
      spectator.service.setItems(items);
      spectator.service.search({ clear: true });
      expect(spectator.service.clearItems).toHaveBeenCalled();
      expect(spectator.service.handleSearchError).not.toHaveBeenCalled();
      expect(spectator.service.setStatus).toHaveBeenCalledWith('fetching');
      expect(spectator.service.searchRequest).toHaveBeenCalledWith(filter, items);
      expect(spectator.service.handleSearchResponse).toHaveBeenCalledWith({ items: [{ name: 'Testobject' }], hits: 1 });
      expect(spectator.service.searchCompleted.next).toHaveBeenCalledWith({ items: [{ name: 'Testobject' }], hits: 1 });
    });

    it('should call setStatus, searchRequest, handleSearchError and searchCompleted.next after a failed search', () => {
      const filter = new UiFilter();
      const items = [{}];
      searchRequestSpy.and.returnValue(throwError({ message: 'error' }));
      getStateSpy.and.returnValue({ items: [], hits: 0, message: 'error' });
      spectator.service.setFilter(filter);
      spectator.service.setItems(items);
      spectator.service.search({});
      expect(spectator.service.clearItems).not.toHaveBeenCalled();
      expect(spectator.service.handleSearchError).toHaveBeenCalledWith({ message: 'error' });
      expect(spectator.service.setStatus).toHaveBeenCalledWith('fetching');
      expect(spectator.service.searchRequest).toHaveBeenCalledWith(filter, items);
      expect(spectator.service.handleSearchResponse).not.toHaveBeenCalled();
      expect(spectator.service.searchCompleted.next).toHaveBeenCalledWith({ items: [], hits: 0, message: 'error' });
    });
  });

  describe('resetFilter()', () => {
    it('should call getDefaultSettings()', () => {
      spyOn(spectator.service, 'getDefaultSettings').and.returnValue(Promise.resolve({ imageUrl: 'testUrl' }));
      spectator.service.resetFilter();
      expect(spectator.service.getDefaultSettings).toHaveBeenCalled();
    });

    it('should create the UiFilter and set it from queryParams', async () => {
      const queryParams = { skip: '5' };
      const filter = new UiFilter();
      spyOn(UiFilter, 'create').and.returnValue(filter);
      spyOn(filter, 'fromQueryParams');
      await spectator.service.resetFilter(queryParams);

      spectator.service.filter$
        .subscribe((f) => {
          expect(f).toBeDefined();
          expect(f.fromQueryParams).toHaveBeenCalledWith(queryParams);
        })
        .unsubscribe();
    });
  });

  describe('restoreResults()', () => {
    it('should call _cache.get() with the filter queryParams', () => {
      const filter = new UiFilter();
      spectator.service.setFilter(filter);
      spectator.service.restoreResults();
      expect(cacheService.get).toHaveBeenCalledWith(filter.getQueryParams());
    });

    it('should set hits and items from the cachedResult', () => {
      const hits = 5;
      const items = [{ name: 'item1' }, { name: 'item2' }];
      cacheService.get.and.returnValue({ hits, items });
      const filter = new UiFilter();
      spectator.service.setFilter(filter);
      spectator.service.restoreResults();
      expect(spectator.service.items).toEqual(items);
      expect(spectator.service.hits).toEqual(hits);
    });
  });
});
