import { TestBed } from '@angular/core/testing';

import { provideMockActions } from '@ngrx/effects/testing';
import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { Observable } from 'rxjs';
import { cold, hot } from 'jasmine-marbles';
import { ToastrService } from 'ngx-toastr';

import { SearchEffects } from './search.effects';
import { SearchService } from '../services/search.service';
import * as searchActions from '../actions/search.actions';
import * as fromSearch from '../reducers/search.reducer';
import * as datasetSelector from '../../../metamodel/selectors/dataset.selector';
import * as searchSelector from '../selectors/search.selector';
import * as attributeActions from '../../../metamodel/actions/attribute.actions';
import * as criteriaFamilyActions from '../../../metamodel/actions/criteria-family.actions';
import * as outputFamilyActions from '../../../metamodel/actions/output-family.actions';
import * as outputCategoryActions from '../../../metamodel/actions/output-category.actions';
import * as attributeSelector from '../../../metamodel/selectors/attribute.selector';
import * as coneSearchSelector from '../selectors/cone-search.selector';
import * as coneSearchActions from '../actions/cone-search.actions';
import { Criterion, PaginationOrder } from '../models';

describe('SearchEffects', () => {
    let actions = new Observable();
    let effects: SearchEffects;
    let metadata: EffectsMetadata<SearchEffects>;
    let searchService: SearchService;
    let toastr: ToastrService;
    let store: MockStore;
    const initialState = { search: { ...fromSearch.initialState } };
    let mockDatasetSelectorSelectDatasetNameByRoute;
    let mockSearchSelectorSelectCurrentDataset;
    let mockSearchSelectorSelectPristine;
    let mockSearchSelectorSelectStepsByRoute;
    let mockAttributeSelectorSelectAllAttributes;
    let mockSearchSelectorSelectCriteriaListByRoute;
    let mockSearchSelectorSelectCriteriaList;
    let mockConeSearchSelectorSelectConeSearchByRoute;
    let mockConeSearchSelectorSelectConeSearch;
    let mockSearchSelectorSelectOutputListByRoute;
    let mockSearchSelectorSelectOutputList;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                SearchEffects,
                { provide: SearchService, useValue: {
                        retrieveData: jest.fn(),
                        retrieveDataLength: jest.fn()
                    }},
                { provide: ToastrService, useValue: { error: jest.fn() }},
                provideMockActions(() => actions),
                provideMockStore({ initialState }),
            ]
        }).compileComponents();
        effects = TestBed.inject(SearchEffects);
        metadata = getEffectsMetadata(effects);
        searchService = TestBed.inject(SearchService);
        toastr = TestBed.inject(ToastrService);
        store = TestBed.inject(MockStore);
        mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
            datasetSelector.selectDatasetNameByRoute,''
        );
        mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
            searchSelector.selectCurrentDataset,''
        );
        mockSearchSelectorSelectPristine = store.overrideSelector(
            searchSelector.selectPristine,true
        );
        mockAttributeSelectorSelectAllAttributes = store.overrideSelector(
            attributeSelector.selectAllAttributes,[]
        );
        mockSearchSelectorSelectStepsByRoute = store.overrideSelector(
            searchSelector.selectStepsByRoute,''
        );
        mockSearchSelectorSelectCriteriaListByRoute = store.overrideSelector(
            searchSelector.selectCriteriaListByRoute,''
        );
        mockSearchSelectorSelectCriteriaList = store.overrideSelector(
            searchSelector.selectCriteriaList,[]
        );
        mockConeSearchSelectorSelectConeSearchByRoute = store.overrideSelector(
            coneSearchSelector.selectConeSearchByRoute,''
        );
        mockConeSearchSelectorSelectConeSearch = store.overrideSelector(
            coneSearchSelector.selectConeSearch,{ ra: 1, dec: 2, radius: 3 }
        );
        mockSearchSelectorSelectOutputListByRoute = store.overrideSelector(
            searchSelector.selectOutputListByRoute,''
        );
        mockSearchSelectorSelectOutputList = store.overrideSelector(
            searchSelector.selectOutputList,[]
        );
    });

    it('should be created', () => {
        expect(effects).toBeTruthy();
    });

    describe('initSearch$ effect', () => {
        it('should dispatch the restartSearch action when dataset changed', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, 'myNewDataset'
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset,'myOldDataset'
            );

            const action = searchActions.initSearch();
            const outcome = searchActions.restartSearch();

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.initSearch$).toBeObservable(expected);
        });

        it('should dispatch a bunch of actions when a dataset is selected or a page is reloaded', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, 'myDatasetName'
            );
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine,true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset,'myDatasetName'
            );

            const action = searchActions.initSearch();
            actions = hot('-a', { a: action });
            const expected = cold('-(bcdef)', {
                b: searchActions.changeCurrentDataset({ currentDataset: 'myDatasetName' }),
                c: attributeActions.loadAttributeList(),
                d: criteriaFamilyActions.loadCriteriaFamilyList(),
                e: outputFamilyActions.loadOutputFamilyList(),
                f: outputCategoryActions.loadOutputCategoryList()
            });

            expect(effects.initSearch$).toBeObservable(expected);
        });

        it('should dispatch a bunch of actions when a dataset is selected or a page is reloaded with steps checked', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, 'myDatasetName'
            );
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine,true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset,'myDatasetName'
            );
            mockSearchSelectorSelectStepsByRoute = store.overrideSelector(
                searchSelector.selectStepsByRoute, '111'
            );

            const action = searchActions.initSearch();
            actions = hot('-a', { a: action });
            const expected = cold('-(bcdefghi)', {
                b: searchActions.changeCurrentDataset({ currentDataset: 'myDatasetName' }),
                c: attributeActions.loadAttributeList(),
                d: criteriaFamilyActions.loadCriteriaFamilyList(),
                e: outputFamilyActions.loadOutputFamilyList(),
                f: outputCategoryActions.loadOutputCategoryList(),
                g: searchActions.checkCriteria(),
                h: searchActions.checkOutput(),
                i: searchActions.checkResult()
            });

            expect(effects.initSearch$).toBeObservable(expected);
        });

        it('should dispatch a resetSearch action when user get back to search module', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, ''
            );
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine,false
            );

            const action = searchActions.initSearch();
            const outcome = searchActions.resetSearch();

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.initSearch$).toBeObservable(expected);
        });

        it('should not dispatch action when step changed on same search', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, ''
            );

            const action = searchActions.initSearch();
            const outcome = { type: '[No Action] Init Search' };

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.initSearch$).toBeObservable(expected);
        });
    });

    describe('restartSearch$ effect', () => {
        it('should dispatch the initSearch action', () => {
            const action = searchActions.restartSearch();
            const outcome = searchActions.initSearch();

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.restartSearch$).toBeObservable(expected);
        });
    });

    describe('loadDefaultFormParameters$ effect', () => {
        it('should not dispatch action if params already loaded', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, false
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );

            const action = searchActions.loadDefaultFormParameters();
            const outcome = { type: '[No Action] Load Default Form Parameters' };

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should not dispatch action if no dataset selected', () => {
            const action = searchActions.loadDefaultFormParameters();
            const outcome = { type: '[No Action] Load Default Form Parameters' };

            actions = hot('-a', { a: action });
            const expected = cold('-b', { b: outcome });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should dispatch a bunch of actions to update search', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const defaultCriteriaList = [];
            const defaultConeSearch = null;
            const defaultOutputList = [];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                d: searchActions.updateOutputList({ outputList: defaultOutputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should set a default criteria list', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );
            mockAttributeSelectorSelectAllAttributes = store.overrideSelector(
                attributeSelector.selectAllAttributes, [
                    {
                        id: 1,
                        name: 'att1',
                        label: 'attribute1',
                        form_label: 'Attribute 1',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        min: 'one',
                        display_detail: 1,
                        id_criteria_family: 1,
                        id_output_category: 1
                    },
                    {
                        id: 2,
                        name: 'att2',
                        label: 'attribute2',
                        form_label: 'Attribute 2',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        min: 'two',
                        display_detail: 1,
                        id_criteria_family: 2,
                        id_output_category: 1
                    }
                ]
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const defaultCriteriaList = [
                {'id':1,'type':'field','operator':'eq','value':'one'},
                {'id':2,'type':'field','operator':'eq','value':'two'}
            ];
            const defaultConeSearch = null;
            const defaultOutputList = [];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                d: searchActions.updateOutputList({ outputList: defaultOutputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should set criteria list from URL', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );
            mockAttributeSelectorSelectAllAttributes = store.overrideSelector(
                attributeSelector.selectAllAttributes, [
                    {
                        id: 1,
                        name: 'att1',
                        label: 'attribute1',
                        form_label: 'Attribute 1',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        min: 'one',
                        display_detail: 1,
                        id_criteria_family: 1,
                        id_output_category: 1
                    },
                    {
                        id: 2,
                        name: 'att2',
                        label: 'attribute2',
                        form_label: 'Attribute 2',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        min: 'two',
                        display_detail: 1,
                        id_criteria_family: 2,
                        id_output_category: 1
                    }
                ]
            );
            mockSearchSelectorSelectCriteriaListByRoute = store.overrideSelector(
                searchSelector.selectCriteriaListByRoute, '1::eq::un;2::eq::deux'
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const criteriaList = [
                {'id':1,'type':'field','operator':'eq','value':'un'},
                {'id':2,'type':'field','operator':'eq','value':'deux'}
            ];
            const defaultConeSearch = null;
            const defaultOutputList = [];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: criteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                d: searchActions.updateOutputList({ outputList: defaultOutputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should set cone search from URL', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );
            mockConeSearchSelectorSelectConeSearchByRoute = store.overrideSelector(
                coneSearchSelector.selectConeSearchByRoute, '1:2:3'
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const defaultCriteriaList = [];
            const coneSearch = { ra: 1, dec: 2, radius: 3 };
            const defaultOutputList = [];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: coneSearch }),
                d: searchActions.updateOutputList({ outputList: defaultOutputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should set a default output list', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );
            mockAttributeSelectorSelectAllAttributes = store.overrideSelector(
                attributeSelector.selectAllAttributes, [
                    {
                        id: 1,
                        name: 'att1',
                        label: 'attribute1',
                        form_label: 'Attribute 1',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        display_detail: 1,
                        selected: true,
                        id_criteria_family: 1,
                        id_output_category: 1
                    },
                    {
                        id: 2,
                        name: 'att2',
                        label: 'attribute2',
                        form_label: 'Attribute 2',
                        output_display: 1,
                        criteria_display: 1,
                        search_type: 'field',
                        operator: 'eq',
                        type: 'string',
                        display_detail: 1,
                        selected: true,
                        id_criteria_family: 2,
                        id_output_category: 1
                    }
                ]
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const defaultCriteriaList = [];
            const defaultConeSearch = null;
            const defaultOutputList = [1, 2];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                d: searchActions.updateOutputList({ outputList: defaultOutputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });

        it('should set output list from URL', () => {
            mockSearchSelectorSelectPristine = store.overrideSelector(
                searchSelector.selectPristine, true
            );
            mockSearchSelectorSelectCurrentDataset = store.overrideSelector(
                searchSelector.selectCurrentDataset, 'myDataset'
            );
            mockSearchSelectorSelectOutputListByRoute = store.overrideSelector(
                searchSelector.selectOutputListByRoute, '1;2;3'
            );

            const action = searchActions.loadDefaultFormParameters();
            actions = hot('-a', { a: action });

            const defaultCriteriaList = [];
            const defaultConeSearch = null;
            const outputList = [1, 2, 3];
            const expected = cold('-(bcde)', {
                b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
                c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                d: searchActions.updateOutputList({ outputList: outputList }),
                e: searchActions.markAsDirty()
            });

            expect(effects.loadDefaultFormParameters$).toBeObservable(expected);
        });
    });

    describe('retrieveDataLength$ effect', () => {
        it('should dispatch the retrieveDataLengthSuccess action on success', () => {
            const action = searchActions.retrieveDataLength();
            const outcome = searchActions.retrieveDataLengthSuccess({ length: 5 });

            actions = hot('-a', { a: action });
            const response = cold('-b|', { b: [{ nb: 5 }] });
            const expected = cold('--c', { c: outcome });
            searchService.retrieveDataLength = jest.fn(() => response);

            expect(effects.retrieveDataLength$).toBeObservable(expected);
        });

        it('should dispatch the retrieveDataLengthFail action on failure', () => {
            const action = searchActions.retrieveDataLength();
            const error = new Error();
            const outcome = searchActions.retrieveDataLengthFail();

            actions = hot('-a', { a: action });
            const response = cold('-#|', {}, error);
            const expected = cold('--b', { b: outcome });
            searchService.retrieveDataLength = jest.fn(() => response);

            expect(effects.retrieveDataLength$).toBeObservable(expected);
        });

        it('should pass correct query to the service', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, 'myDataset'
            );
            mockSearchSelectorSelectCriteriaList = store.overrideSelector(
                searchSelector.selectCriteriaList, [{'id':1,'type':'field','operator':'eq','value':'one'} as Criterion]
            );
            mockConeSearchSelectorSelectConeSearch = store.overrideSelector(
                coneSearchSelector.selectConeSearch, { ra: 1, dec: 2, radius: 3 }
            );

            jest.spyOn(searchService, 'retrieveDataLength');

            const action = searchActions.retrieveDataLength();
            const outcome = searchActions.retrieveDataLengthSuccess({ length: 5 });

            actions = hot('-a', { a: action });
            const response = cold('-b|', { b: [{ nb: 5 }] });
            const expected = cold('--c', { c: outcome });
            searchService.retrieveDataLength = jest.fn(() => response);

            expect(effects.retrieveDataLength$).toBeObservable(expected);
            expect(searchService.retrieveDataLength).toHaveBeenCalledTimes(1);
            expect(searchService.retrieveDataLength).toHaveBeenCalledWith('myDataset?a=count&c=1::eq::one&cs=1:2:3');
        });
    });

    describe('retrieveDataLengthFail$ effect', () => {
        it('should not dispatch', () => {
            expect(metadata.retrieveDataLengthFail$).toEqual(
                expect.objectContaining({ dispatch: false })
            );
        });

        it('should display a error notification', () => {
            const spy = jest.spyOn(toastr, 'error');
            const action = searchActions.retrieveDataLengthFail();

            actions = hot('a', { a: action });
            const expected = cold('a', { a: action });

            expect(effects.retrieveDataLengthFail$).toBeObservable(expected);
            expect(spy).toHaveBeenCalledTimes(1);
            expect(spy).toHaveBeenCalledWith('Loading Failed', 'The search data length loading failed');
        });
    });

    describe('retrieveData$ effect', () => {
        it('should dispatch the retrieveDataSuccess action on success', () => {
            const action = searchActions.retrieveData( {
                pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a }
            });
            const outcome = searchActions.retrieveDataSuccess({ data: ['data'] });

            actions = hot('-a', { a: action });
            const response = cold('-b|', { b: ['data'] });
            const expected = cold('--c', { c: outcome });
            searchService.retrieveData = jest.fn(() => response);

            expect(effects.retrieveData$).toBeObservable(expected);
        });

        it('should dispatch the retrieveDataFail action on failure', () => {
            const action = searchActions.retrieveData({
                pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a }
            });
            const error = new Error();
            const outcome = searchActions.retrieveDataFail();

            actions = hot('-a', { a: action });
            const response = cold('-#|', {}, error);
            const expected = cold('--b', { b: outcome });
            searchService.retrieveData = jest.fn(() => response);

            expect(effects.retrieveData$).toBeObservable(expected);
        });

        it('should pass correct query to the service', () => {
            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                datasetSelector.selectDatasetNameByRoute, 'myDataset'
            );
            mockSearchSelectorSelectCriteriaList = store.overrideSelector(
                searchSelector.selectCriteriaList, [{'id':1,'type':'field','operator':'eq','value':'one'} as Criterion]
            );
            mockConeSearchSelectorSelectConeSearch = store.overrideSelector(
                coneSearchSelector.selectConeSearch, { ra: 1, dec: 2, radius: 3 }
            );
            mockSearchSelectorSelectOutputList = store.overrideSelector(
                searchSelector.selectOutputList, [1, 2]
            );

            jest.spyOn(searchService, 'retrieveData');

            const action = searchActions.retrieveData({
                pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a }
            });
            const outcome = searchActions.retrieveDataSuccess({ data: ['data'] });

            actions = hot('-a', { a: action });
            const response = cold('-b|', { b: ['data'] });
            const expected = cold('--c', { c: outcome });
            searchService.retrieveData = jest.fn(() => response);

            expect(effects.retrieveData$).toBeObservable(expected);
            expect(searchService.retrieveData).toHaveBeenCalledTimes(1);
            expect(searchService.retrieveData).toHaveBeenCalledWith('myDataset?a=1;2&c=1::eq::one&cs=1:2:3&p=10:1&o=1:a');
        });
    });

    describe('retrieveDataFail$ effect', () => {
        it('should not dispatch', () => {
            expect(metadata.retrieveDataFail$).toEqual(
                expect.objectContaining({ dispatch: false })
            );
        });

        it('should display a error notification', () => {
            const spy = jest.spyOn(toastr, 'error');
            const action = searchActions.retrieveDataFail();

            actions = hot('a', { a: action });
            const expected = cold('a', { a: action });

            expect(effects.retrieveDataFail$).toBeObservable(expected);
            expect(spy).toHaveBeenCalledTimes(1);
            expect(spy).toHaveBeenCalledWith('Loading Failed', 'The search data loading failed');
        });
    });
});