/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2018, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * Open MCT includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/

/**
 *  SearchSpec. Created by shale on 07/31/2015.
 */
define([
    "../../src/services/GenericSearchProvider"
], function (
    GenericSearchProvider
) {

    xdescribe('GenericSearchProvider', function () {
        var $q,
            $log,
            modelService,
            models,
            workerService,
            worker,
            topic,
            mutationTopic,
            ROOTS,
            compositionProvider,
            openmct,
            provider;

        beforeEach(function () {
            $q = jasmine.createSpyObj(
                '$q',
                ['defer']
            );
            $log = jasmine.createSpyObj(
                '$log',
                ['warn']
            );
            models = {};
            modelService = jasmine.createSpyObj(
                'modelService',
                ['getModels']
            );
            modelService.getModels.and.returnValue(Promise.resolve(models));
            workerService = jasmine.createSpyObj(
                'workerService',
                ['run']
            );
            worker = jasmine.createSpyObj(
                'worker',
                [
                    'postMessage',
                    'addEventListener'
                ]
            );
            workerService.run.and.returnValue(worker);
            topic = jasmine.createSpy('topic');
            mutationTopic = jasmine.createSpyObj(
                'mutationTopic',
                ['listen']
            );
            topic.and.returnValue(mutationTopic);
            ROOTS = [
                'mine'
            ];
            compositionProvider = jasmine.createSpyObj(
                'compositionProvider',
                ['load', 'appliesTo']
            );
            compositionProvider.load.and.callFake(function (domainObject) {
                return Promise.resolve(domainObject.composition);
            });
            compositionProvider.appliesTo.and.callFake(function (domainObject) {
                return !!domainObject.composition;
            });
            openmct = {
                composition: {
                    registry: [compositionProvider]
                }
            };

            spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');

            provider = new GenericSearchProvider(
                $q,
                $log,
                modelService,
                workerService,
                topic,
                ROOTS,
                openmct
            );
        });

        it('listens for general mutation', function () {
            expect(topic).toHaveBeenCalledWith('mutation');
            expect(mutationTopic.listen)
                .toHaveBeenCalledWith(jasmine.any(Function));
        });

        it('re-indexes when mutation occurs', function () {
            var mockDomainObject =
                    jasmine.createSpyObj('domainObj', [
                        'getId',
                        'getModel',
                        'getCapability'
                    ]),
                testModel = { some: 'model' };
            mockDomainObject.getId.and.returnValue("some-id");
            mockDomainObject.getModel.and.returnValue(testModel);
            spyOn(provider, 'index').and.callThrough();
            mutationTopic.listen.calls.mostRecent().args[0](mockDomainObject);
            expect(provider.index).toHaveBeenCalledWith('some-id', testModel);
        });

        it('starts indexing roots', function () {
            expect(provider.scheduleForIndexing).toHaveBeenCalledWith('mine');
        });

        it('runs a worker', function () {
            expect(workerService.run)
                .toHaveBeenCalledWith('genericSearchWorker');
        });

        it('listens for messages from worker', function () {
            expect(worker.addEventListener)
                .toHaveBeenCalledWith('message', jasmine.any(Function));
            spyOn(provider, 'onWorkerMessage');
            worker.addEventListener.calls.mostRecent().args[1]('mymessage');
            expect(provider.onWorkerMessage).toHaveBeenCalledWith('mymessage');
        });

        it('has a maximum number of concurrent requests', function () {
            expect(provider.MAX_CONCURRENT_REQUESTS).toBe(100);
        });

        describe('scheduleForIndexing', function () {
            beforeEach(function () {
                provider.scheduleForIndexing.and.callThrough();
                spyOn(provider, 'keepIndexing');
            });

            it('tracks ids to index', function () {
                expect(provider.indexedIds.a).not.toBeDefined();
                expect(provider.pendingIndex.a).not.toBeDefined();
                expect(provider.idsToIndex).not.toContain('a');
                provider.scheduleForIndexing('a');
                expect(provider.indexedIds.a).toBeDefined();
                expect(provider.pendingIndex.a).toBeDefined();
                expect(provider.idsToIndex).toContain('a');
            });

            it('calls keep indexing', function () {
                provider.scheduleForIndexing('a');
                expect(provider.keepIndexing).toHaveBeenCalled();
            });
        });

        describe('keepIndexing', function () {
            it('calls beginIndexRequest until at maximum', function () {
                spyOn(provider, 'beginIndexRequest').and.callThrough();
                provider.pendingRequests = 9;
                provider.idsToIndex = ['a', 'b', 'c'];
                provider.MAX_CONCURRENT_REQUESTS = 10;
                provider.keepIndexing();
                expect(provider.beginIndexRequest).toHaveBeenCalled();
                expect(provider.beginIndexRequest.calls.count()).toBe(1);
            });

            it('calls beginIndexRequest for all ids to index', function () {
                spyOn(provider, 'beginIndexRequest').and.callThrough();
                provider.pendingRequests = 0;
                provider.idsToIndex = ['a', 'b', 'c'];
                provider.MAX_CONCURRENT_REQUESTS = 10;
                provider.keepIndexing();
                expect(provider.beginIndexRequest).toHaveBeenCalled();
                expect(provider.beginIndexRequest.calls.count()).toBe(3);
            });

            it('does not index when at capacity', function () {
                spyOn(provider, 'beginIndexRequest');
                provider.pendingRequests = 10;
                provider.idsToIndex.push('a');
                provider.MAX_CONCURRENT_REQUESTS = 10;
                provider.keepIndexing();
                expect(provider.beginIndexRequest).not.toHaveBeenCalled();
            });

            it('does not index when no ids to index', function () {
                spyOn(provider, 'beginIndexRequest');
                provider.pendingRequests = 0;
                provider.MAX_CONCURRENT_REQUESTS = 10;
                provider.keepIndexing();
                expect(provider.beginIndexRequest).not.toHaveBeenCalled();
            });
        });

        describe('index', function () {
            it('sends index message to worker', function () {
                var id = 'anId',
                    model = {};

                provider.index(id, model);
                expect(worker.postMessage).toHaveBeenCalledWith({
                    request: 'index',
                    id: id,
                    model: model
                });
            });

            it('schedules composed ids for indexing', function () {
                var id = 'anId',
                    model = {composition: ['abc', 'def']},
                    resolve,
                    promise = new Promise(function (r) {
                        resolve = r;
                    });

                provider.scheduleForIndexing.and.callFake(resolve);

                provider.index(id, model);

                expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
                    identifier: {key: 'anId', namespace: ''},
                    composition: [jasmine.any(Object), jasmine.any(Object)]
                });

                expect(compositionProvider.load).toHaveBeenCalledWith({
                    identifier:  {key: 'anId', namespace: ''},
                    composition: [jasmine.any(Object), jasmine.any(Object)]
                });

                return promise.then(function () {
                    expect(provider.scheduleForIndexing)
                        .toHaveBeenCalledWith('abc');
                    expect(provider.scheduleForIndexing)
                        .toHaveBeenCalledWith('def');

                });
            });

            it('does not index ROOT, but checks composition', function () {
                var id = 'ROOT',
                    model = {};

                provider.index(id, model);
                expect(worker.postMessage).not.toHaveBeenCalled();
                expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
                    identifier: {key: 'ROOT', namespace: ''}
                });
            });
        });

        describe('beginIndexRequest', function () {

            beforeEach(function () {
                provider.pendingRequests = 0;
                provider.pendingIds = {'abc': true};
                provider.idsToIndex = ['abc'];
                models.abc = {};
                spyOn(provider, 'index');
            });

            it('removes items from queue', function () {
                provider.beginIndexRequest();
                expect(provider.idsToIndex.length).toBe(0);
            });

            it('tracks number of pending requests', function () {
                provider.beginIndexRequest();
                expect(provider.pendingRequests).toBe(1);

                return waitsFor(function () {
                    return provider.pendingRequests === 0;
                }).then(function () {
                    expect(provider.pendingRequests).toBe(0);
                });
            });

            it('indexes objects', function () {
                provider.beginIndexRequest();

                return waitsFor(function () {
                    return provider.pendingRequests === 0;
                }).then(function () {
                    expect(provider.index)
                        .toHaveBeenCalledWith('abc', models.abc);
                });
            });

            function waitsFor(latchFunction) {
                return new Promise(function (resolve, reject) {
                    var maxWait = 2000;
                    var start = Date.now();

                    checkLatchFunction();

                    function checkLatchFunction() {
                        var now = Date.now();
                        var elapsed = now - start;

                        if (latchFunction()) {
                            resolve();
                        } else if (elapsed >= maxWait) {
                            reject("Timeout waiting for latch function to be true");
                        } else {
                            setTimeout(checkLatchFunction);
                        }
                    }
                });
            }
        });


        it('can dispatch searches to worker', function () {
            spyOn(provider, 'makeQueryId').and.returnValue(428);
            expect(provider.dispatchSearch('searchTerm', 100))
                .toBe(428);

            expect(worker.postMessage).toHaveBeenCalledWith({
                request: 'search',
                input: 'searchTerm',
                maxResults: 100,
                queryId: 428
            });
        });

        it('can generate queryIds', function () {
            expect(provider.makeQueryId()).toEqual(jasmine.any(Number));
        });

        it('can query for terms', function () {
            var deferred = {promise: {}};
            spyOn(provider, 'dispatchSearch').and.returnValue(303);
            $q.defer.and.returnValue(deferred);

            expect(provider.query('someTerm', 100)).toBe(deferred.promise);
            expect(provider.pendingQueries[303]).toBe(deferred);
        });

        describe('onWorkerMessage', function () {
            var pendingQuery;
            beforeEach(function () {
                pendingQuery = jasmine.createSpyObj(
                    'pendingQuery',
                    ['resolve']
                );
                provider.pendingQueries[143] = pendingQuery;
            });

            it('resolves pending searches', function () {
                provider.onWorkerMessage({
                    data: {
                        request: 'search',
                        total: 2,
                        results: [
                            {
                                item: {
                                    id: 'abc',
                                    model: {id: 'abc'}
                                },
                                matchCount: 4
                            },
                            {
                                item: {
                                    id: 'def',
                                    model: {id: 'def'}
                                },
                                matchCount: 2
                            }
                        ],
                        queryId: 143
                    }
                });

                expect(pendingQuery.resolve)
                    .toHaveBeenCalledWith({
                        total: 2,
                        hits: [{
                            id: 'abc',
                            model: {id: 'abc'},
                            score: 4
                        }, {
                            id: 'def',
                            model: {id: 'def'},
                            score: 2
                        }]
                    });

                expect(provider.pendingQueries[143]).not.toBeDefined();

            });

        });

    });
});
