/*global angular*/

/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2017, 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.
 *****************************************************************************/

/**
 * MergeModelsSpec. Created by vwoeltje on 11/6/14.
 */
define(
    ["../src/PlotController"],
    function (PlotController) {

        describe("The plot controller", function () {
            var mockScope,
                mockElement,
                mockExportImageService,
                mockFormatter,
                mockHandler,
                mockThrottle,
                mockHandle,
                mockDomainObject,
                mockSeries,
                mockStatusCapability,
                controller,
                mockConductor;

            function bind(method, thisObj) {
                return function () {
                    return method.apply(thisObj, arguments);
                };
            }

            function fireEvent(name, args) {
                mockScope.$on.calls.forEach(function (call) {
                    if (call.args[0] === name) {
                        call.args[1].apply(null, args || []);
                    }
                });
            }

            function fireWatch(expr, value) {
                mockScope.$watch.calls.forEach(function (call) {
                    if (call.args[0] === expr) {
                        call.args[1].apply(null, [value]);
                    }
                });
            }


            beforeEach(function () {
                mockScope = jasmine.createSpyObj(
                    "$scope",
                    ["$watch", "$on", "$emit"]
                );
                mockElement = angular.element('<div />');
                mockExportImageService = jasmine.createSpyObj(
                    "ExportImageService",
                    ["exportJPG", "exportPNG"]
                );
                mockFormatter = jasmine.createSpyObj(
                    "formatter",
                    ["formatDomainValue", "formatRangeValue"]
                );
                mockDomainObject = jasmine.createSpyObj(
                    "domainObject",
                    ["getId", "getModel", "getCapability", "hasCapability"]
                );
                mockHandler = jasmine.createSpyObj(
                    "telemetrySubscriber",
                    ["handle"]
                );
                mockThrottle = jasmine.createSpy("throttle");
                mockHandle = jasmine.createSpyObj(
                    "subscription",
                    [
                        "unsubscribe",
                        "getTelemetryObjects",
                        "getMetadata",
                        "getDomainValue",
                        "getRangeValue",
                        "getDatum",
                        "request"
                    ]
                );
                mockSeries = jasmine.createSpyObj(
                    'series',
                    ['getPointCount', 'getDomainValue', 'getRangeValue']
                );

                mockStatusCapability = jasmine.createSpyObj(
                    "statusCapability",
                    ["set"]
                );

                mockHandler.handle.andReturn(mockHandle);
                mockThrottle.andCallFake(function (fn) {
                    return fn;
                });
                mockHandle.getTelemetryObjects.andReturn([mockDomainObject]);
                mockHandle.getMetadata.andReturn([{}]);
                mockHandle.getDomainValue.andReturn(123);
                mockHandle.getRangeValue.andReturn(42);
                mockScope.domainObject = mockDomainObject;

                mockConductor = jasmine.createSpyObj('conductor', [
                    'on',
                    'off',
                    'bounds',
                    'timeSystem',
                    'timeOfInterest',
                    'follow'
                ]);

                mockConductor.bounds.andReturn({});

                controller = new PlotController(
                    mockScope,
                    mockElement,
                    mockExportImageService,
                    mockFormatter,
                    mockHandler,
                    mockThrottle,
                    undefined,
                    {time: mockConductor}
                );
            });

            it("provides plot colors", function () {
                // PlotPalette will have its own tests
                expect(controller.getColor(0))
                    .toEqual(jasmine.any(String));

                // Colors should be unique
                expect(controller.getColor(0))
                    .not.toEqual(controller.getColor(1));
            });

            it("subscribes to telemetry when a domain object appears in scope", function () {
                // Make sure we're using the right watch here
                expect(mockScope.$watch.mostRecentCall.args[0])
                    .toEqual("domainObject");
                // Make an object available
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                // Should have subscribed
                expect(mockHandler.handle).toHaveBeenCalledWith(
                    mockDomainObject,
                    jasmine.any(Function),
                    true // Lossless
                );
            });

            it("draws lines when data becomes available", function () {
                // Make an object available
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);

                // Verify precondition
                controller.getSubPlots().forEach(function (subplot) {
                    expect(subplot.getDrawingObject().lines)
                        .not.toBeDefined();
                });

                // Make sure there actually are subplots being verified
                expect(controller.getSubPlots().length > 0).toBeTruthy();

                // Broadcast data
                mockHandler.handle.mostRecentCall.args[1]();

                controller.getSubPlots().forEach(function (subplot) {
                    expect(subplot.getDrawingObject().lines)
                        .toBeDefined();
                });
            });

            it("unsubscribes when domain object changes", function () {
                // Make an object available
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                // Verify precondition - shouldn't unsubscribe yet
                expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
                // Remove the domain object
                mockScope.$watch.mostRecentCall.args[1](undefined);
                // Should have unsubscribed
                expect(mockHandle.unsubscribe).toHaveBeenCalled();
            });


            it("changes modes depending on number of objects", function () {
                // Act like one object is available
                mockHandle.getTelemetryObjects.andReturn([
                    mockDomainObject
                ]);

                // Make an object available; invoke handler's callback
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                mockHandler.handle.mostRecentCall.args[1]();

                expect(controller.getModeOptions().length).toEqual(1);

                // Act like one object is available
                mockHandle.getTelemetryObjects.andReturn([
                    mockDomainObject,
                    mockDomainObject,
                    mockDomainObject
                ]);

                // Make an object available; invoke handler's callback
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                mockHandler.handle.mostRecentCall.args[1]();

                expect(controller.getModeOptions().length).toEqual(2);
            });

            // Interface tests follow; these will be delegated (mostly
            // to PlotModeOptions, which is tested separately).
            it("provides access to available plot mode options", function () {
                expect(Array.isArray(controller.getModeOptions()))
                    .toBeTruthy();
            });

            it("provides a current plot mode", function () {
                expect(controller.getMode().name)
                    .toEqual(jasmine.any(String));
            });

            it("allows plot mode to be changed", function () {
                expect(function () {
                    controller.setMode(controller.getMode());
                }).not.toThrow();
            });

            it("provides an array of sub-plots", function () {
                expect(Array.isArray(controller.getSubPlots()))
                    .toBeTruthy();
            });

            it("allows plots to be updated", function () {
                expect(bind(controller.update, controller)).not.toThrow();
            });

            it("allows changing pan-zoom state", function () {
                expect(bind(controller.isZoomed, controller)).not.toThrow();
                expect(bind(controller.stepBackPanZoom, controller)).not.toThrow();
                expect(bind(controller.unzoom, controller)).not.toThrow();
            });

            it("sets status when plot becomes detached from time conductor", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);

                function boundsEvent() {
                    fireEvent("telemetry:display:bounds", [
                        {},
                        { start: 10, end: 100 },
                        true
                    ]);
                }

                mockDomainObject.hasCapability.andCallFake(function (name) {
                    return name === "status";
                });
                mockDomainObject.getCapability.andReturn(mockStatusCapability);
                spyOn(controller, "isZoomed");

                //Mock zoomed in state
                controller.isZoomed.andReturn(true);
                boundsEvent();
                expect(mockStatusCapability.set).toHaveBeenCalledWith("timeconductor-unsynced", true);

                //"Reset" zoom
                controller.isZoomed.andReturn(false);
                boundsEvent();
                expect(mockStatusCapability.set).toHaveBeenCalledWith("timeconductor-unsynced", false);
            });

            it("indicates if a request is pending", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                expect(controller.isRequestPending()).toBeTruthy();
                mockHandle.request.mostRecentCall.args[1](
                    mockDomainObject,
                    mockSeries
                );
                expect(controller.isRequestPending()).toBeFalsy();
            });

            it("requests historical telemetry", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                expect(mockHandle.request).toHaveBeenCalled();
                mockHandle.request.mostRecentCall.args[1](
                    mockDomainObject,
                    mockSeries
                );
            });

            it("unsubscribes when destroyed", function () {
                // Make an object available
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                // Make sure $destroy is what's listened for
                expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
                // Also verify precondition
                expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
                // Destroy the scope
                fireEvent("$destroy");
                // Should have unsubscribed
                expect(mockHandle.unsubscribe).toHaveBeenCalled();
            });

            it("requeries when displayable bounds change", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                expect(mockHandle.request.calls.length).toEqual(1);
                fireEvent("telemetry:display:bounds", [
                    {},
                    { start: 10, end: 100 }
                ]);
                expect(mockHandle.request.calls.length).toEqual(2);
            });

            it("requeries when user changes domain selection", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                expect(mockHandle.request.calls.length).toEqual(1);
                fireWatch("axes[0].active.key", 'someNewKey');
                expect(mockHandle.request.calls.length).toEqual(2);
            });

            it("requeries when user changes range selection", function () {
                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                expect(mockHandle.request.calls.length).toEqual(1);
                fireWatch("axes[1].active.key", 'someNewKey');
                expect(mockHandle.request.calls.length).toEqual(2);
            });

            it("maintains externally-provided domain axis bounds after data is received", function () {
                mockSeries.getPointCount.andReturn(3);
                mockSeries.getRangeValue.andReturn(42);
                mockSeries.getDomainValue.andCallFake(function (i) {
                    return 2500 + i * 2500;
                });

                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                fireEvent("telemetry:display:bounds", [
                    {},
                    {start: 0, end: 10000}
                ]);
                mockHandle.request.mostRecentCall.args[1](
                    mockDomainObject,
                    mockSeries
                );

                // Pan-zoom state should reflect bounds set externally;
                // domain axis should not have shrunk to fit data.
                expect(
                    controller.getSubPlots()[0].panZoomStack.getOrigin()[0]
                ).toEqual(0);
                expect(
                    controller.getSubPlots()[0].panZoomStack.getDimensions()[0]
                ).toEqual(10000);
            });

            it("provides classes for legends based on limit state", function () {
                var mockTelemetryObjects = mockHandle.getTelemetryObjects();

                mockHandle.getDatum.andReturn({});
                mockTelemetryObjects.forEach(function (mockObject, i) {
                    var id = 'object-' + i,
                        mockLimitCapability =
                            jasmine.createSpyObj('limit-' + id, ['evaluate']);

                    mockObject.getId.andReturn(id);
                    mockObject.getCapability.andCallFake(function (key) {
                        return (key === 'limit') && mockLimitCapability;
                    });

                    mockLimitCapability.evaluate
                        .andReturn({ cssClass: 'alarm-' + id });
                });

                mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
                mockHandler.handle.mostRecentCall.args[1]();

                mockTelemetryObjects.forEach(function (mockTelemetryObject) {
                    expect(controller.getLegendClass(mockTelemetryObject))
                        .toEqual('alarm-' + mockTelemetryObject.getId());
                });
            });
        });
    }
);