/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/*global loop, sinon, React */
/* jshint newcap:false */

var expect = chai.expect;
var l10n = document.webL10n || document.mozL10n;
var TestUtils = React.addons.TestUtils;

describe("loop.shared.views", function() {
  "use strict";

  var sharedModels = loop.shared.models,
      sharedViews = loop.shared.views,
      getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass,
      sandbox;

  beforeEach(function() {
    sandbox = sinon.sandbox.create();
    sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
  });

  afterEach(function() {
    $("#fixtures").empty();
    sandbox.restore();
  });

  describe("L10nView", function() {
    beforeEach(function() {
      sandbox.stub(l10n, "translate");
    });

    it("should translate generated contents on render()", function() {
      var TestView = loop.shared.views.L10nView.extend();

      var view = new TestView();
      view.render();

      sinon.assert.calledOnce(l10n.translate);
      sinon.assert.calledWithExactly(l10n.translate, view.el);
    });
  });

  describe("MediaControlButton", function() {
    it("should render an enabled local audio button", function() {
      var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({
        scope: "local",
        type: "audio",
        action: function(){},
        enabled: true
      }));

      expect(comp.getDOMNode().classList.contains("muted")).eql(false);
    });

    it("should render a muted local audio button", function() {
      var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({
        scope: "local",
        type: "audio",
        action: function(){},
        enabled: false
      }));

      expect(comp.getDOMNode().classList.contains("muted")).eql(true);
    });

    it("should render an enabled local video button", function() {
      var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({
        scope: "local",
        type: "video",
        action: function(){},
        enabled: true
      }));

      expect(comp.getDOMNode().classList.contains("muted")).eql(false);
    });

    it("should render a muted local video button", function() {
      var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({
        scope: "local",
        type: "video",
        action: function(){},
        enabled: false
      }));

      expect(comp.getDOMNode().classList.contains("muted")).eql(true);
    });
  });

  describe("ConversationToolbar", function() {
    var hangup, publishStream;

    function mountTestComponent(props) {
      return TestUtils.renderIntoDocument(
        sharedViews.ConversationToolbar(props));
    }

    beforeEach(function() {
      hangup = sandbox.stub();
      publishStream = sandbox.stub();
    });

    it("should hangup when hangup button is clicked", function() {
      var comp = mountTestComponent({
        hangup: hangup,
        publishStream: publishStream,
        audio: {enabled: true}
      });

      TestUtils.Simulate.click(
        comp.getDOMNode().querySelector(".btn-hangup"));

      sinon.assert.calledOnce(hangup);
      sinon.assert.calledWithExactly(hangup);
    });

    it("should unpublish audio when audio mute btn is clicked", function() {
      var comp = mountTestComponent({
        hangup: hangup,
        publishStream: publishStream,
        audio: {enabled: true}
      });

      TestUtils.Simulate.click(
        comp.getDOMNode().querySelector(".btn-mute-audio"));

      sinon.assert.calledOnce(publishStream);
      sinon.assert.calledWithExactly(publishStream, "audio", false);
    });

    it("should publish audio when audio mute btn is clicked", function() {
      var comp = mountTestComponent({
        hangup: hangup,
        publishStream: publishStream,
        audio: {enabled: false}
      });

      TestUtils.Simulate.click(
        comp.getDOMNode().querySelector(".btn-mute-audio"));

      sinon.assert.calledOnce(publishStream);
      sinon.assert.calledWithExactly(publishStream, "audio", true);
    });

    it("should unpublish video when video mute btn is clicked", function() {
      var comp = mountTestComponent({
        hangup: hangup,
        publishStream: publishStream,
        video: {enabled: true}
      });

      TestUtils.Simulate.click(
        comp.getDOMNode().querySelector(".btn-mute-video"));

      sinon.assert.calledOnce(publishStream);
      sinon.assert.calledWithExactly(publishStream, "video", false);
    });

    it("should publish video when video mute btn is clicked", function() {
      var comp = mountTestComponent({
        hangup: hangup,
        publishStream: publishStream,
        video: {enabled: false}
      });

      TestUtils.Simulate.click(
        comp.getDOMNode().querySelector(".btn-mute-video"));

      sinon.assert.calledOnce(publishStream);
      sinon.assert.calledWithExactly(publishStream, "video", true);
    });
  });

  describe("ConversationView", function() {
    var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model;

    function mountTestComponent(props) {
      return TestUtils.renderIntoDocument(sharedViews.ConversationView(props));
    }

    beforeEach(function() {
      fakeSessionData = {
        sessionId:    "sessionId",
        sessionToken: "sessionToken",
        apiKey:       "apiKey"
      };
      fakeSession = _.extend({
        connection: {connectionId: 42},
        connect: sandbox.spy(),
        disconnect: sandbox.spy(),
        publish: sandbox.spy(),
        unpublish: sandbox.spy(),
        subscribe: sandbox.spy()
      }, Backbone.Events);
      fakePublisher = _.extend({
        publishAudio: sandbox.spy(),
        publishVideo: sandbox.spy()
      }, Backbone.Events);
      fakeSDK = {
        initPublisher: sandbox.stub().returns(fakePublisher),
        initSession: sandbox.stub().returns(fakeSession)
      };
      model = new sharedModels.ConversationModel(fakeSessionData, {
        sdk: fakeSDK,
        pendingCallTimeout: 1000
      });
    });

    describe("#componentDidMount", function() {
      it("should start a session", function() {
        sandbox.stub(model, "startSession");

        mountTestComponent({
          sdk: fakeSDK,
          model: model,
          video: {enabled: true}
        });

        sinon.assert.calledOnce(model.startSession);
      });

      it("should set the correct stream publish options", function() {

        var component = mountTestComponent({
          sdk: fakeSDK,
          model: model,
          video: {enabled: false}
        });

        expect(component.publisherConfig.publishVideo).to.eql(false);

      });
    });

    describe("constructed", function() {
      var comp;

      beforeEach(function() {
        comp = mountTestComponent({
          sdk: fakeSDK,
          model: model,
          video: {enabled: false}
        });
      });

      describe("#hangup", function() {
        beforeEach(function() {
          comp.startPublishing();
        });

        it("should disconnect the session", function() {
          sandbox.stub(model, "endSession");

          comp.hangup();

          sinon.assert.calledOnce(model.endSession);
        });

        it("should stop publishing local streams", function() {
          comp.hangup();

          sinon.assert.calledOnce(fakeSession.unpublish);
        });
      });

      describe("#startPublishing", function() {
        beforeEach(function() {
          sandbox.stub(fakePublisher, "on");
        });

        it("should publish local stream", function() {
          comp.startPublishing();

          sinon.assert.calledOnce(fakeSDK.initPublisher);
          sinon.assert.calledOnce(fakeSession.publish);
        });

        it("should start listening to OT publisher accessDialogOpened and " +
          " accessDenied events",
          function() {
            comp.startPublishing();

            sinon.assert.called(fakePublisher.on);
            sinon.assert.calledWith(fakePublisher.on,
                                    "accessDialogOpened accessDenied");
          });
      });

      describe("#stopPublishing", function() {
        beforeEach(function() {
          sandbox.stub(fakePublisher, "off");
          comp.startPublishing();
        });

        it("should stop publish local stream", function() {
          comp.stopPublishing();

          sinon.assert.calledOnce(fakeSession.unpublish);
        });

        it("should unsubscribe from publisher events",
          function() {
            comp.stopPublishing();

            // Note: Backbone.Events#stopListening calls off() on passed object.
            sinon.assert.calledOnce(fakePublisher.off);
          });
      });

      describe("#publishStream", function() {
        var comp;

        beforeEach(function() {
          comp = mountTestComponent({
            sdk: fakeSDK,
            model: model,
            video: {enabled: false}
          });
          comp.startPublishing();
        });

        it("should start streaming local audio", function() {
          comp.publishStream("audio", true);

          sinon.assert.calledOnce(fakePublisher.publishAudio);
          sinon.assert.calledWithExactly(fakePublisher.publishAudio, true);
        });

        it("should stop streaming local audio", function() {
          comp.publishStream("audio", false);

          sinon.assert.calledOnce(fakePublisher.publishAudio);
          sinon.assert.calledWithExactly(fakePublisher.publishAudio, false);
        });

        it("should start streaming local video", function() {
          comp.publishStream("video", true);

          sinon.assert.calledOnce(fakePublisher.publishVideo);
          sinon.assert.calledWithExactly(fakePublisher.publishVideo, true);
        });

        it("should stop streaming local video", function() {
          comp.publishStream("video", false);

          sinon.assert.calledOnce(fakePublisher.publishVideo);
          sinon.assert.calledWithExactly(fakePublisher.publishVideo, false);
        });
      });

      describe("Model events", function() {
        it("should start streaming on session:connected", function() {
          model.trigger("session:connected");

          sinon.assert.calledOnce(fakeSDK.initPublisher);
        });

        it("should publish remote stream on session:stream-created",
          function() {
            var s1 = {connection: {connectionId: 42}};

            model.trigger("session:stream-created", {stream: s1});

            sinon.assert.calledOnce(fakeSession.subscribe);
            sinon.assert.calledWith(fakeSession.subscribe, s1);
          });

        it("should unpublish local stream on session:ended", function() {
          comp.startPublishing();

          model.trigger("session:ended");

          sinon.assert.calledOnce(fakeSession.unpublish);
        });

        it("should unpublish local stream on session:peer-hungup", function() {
          comp.startPublishing();

          model.trigger("session:peer-hungup");

          sinon.assert.calledOnce(fakeSession.unpublish);
        });

        it("should unpublish local stream on session:network-disconnected",
          function() {
            comp.startPublishing();

            model.trigger("session:network-disconnected");

            sinon.assert.calledOnce(fakeSession.unpublish);
          });
      });

      describe("Publisher events", function() {
        beforeEach(function() {
          comp.startPublishing();
        });

        it("should set audio state on streamCreated", function() {
          fakePublisher.trigger("streamCreated", {stream: {hasAudio: true}});
          expect(comp.state.audio.enabled).eql(true);

          fakePublisher.trigger("streamCreated", {stream: {hasAudio: false}});
          expect(comp.state.audio.enabled).eql(false);
        });

        it("should set video state on streamCreated", function() {
          fakePublisher.trigger("streamCreated", {stream: {hasVideo: true}});
          expect(comp.state.video.enabled).eql(true);

          fakePublisher.trigger("streamCreated", {stream: {hasVideo: false}});
          expect(comp.state.video.enabled).eql(false);
        });

        it("should set media state on streamDestroyed", function() {
          fakePublisher.trigger("streamDestroyed");

          expect(comp.state.audio.enabled).eql(false);
          expect(comp.state.video.enabled).eql(false);
        });
      });
    });
  });

  describe("FeedbackView", function() {
    var comp, fakeFeedbackApiClient;

    beforeEach(function() {
      sandbox.stub(l10n, "get", function(x) {
        return x;
      });
      fakeFeedbackApiClient = {send: sandbox.stub()};
      comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
        feedbackApiClient: fakeFeedbackApiClient
      }));
    });

    // local test helpers
    function clickHappyFace(comp) {
      var happyFace = comp.getDOMNode().querySelector(".face-happy");
      TestUtils.Simulate.click(happyFace);
    }

    function clickSadFace(comp) {
      var sadFace = comp.getDOMNode().querySelector(".face-sad");
      TestUtils.Simulate.click(sadFace);
    }

    function fillSadFeedbackForm(comp, category, text) {
      TestUtils.Simulate.change(
        comp.getDOMNode().querySelector("[value='" + category + "']"));

      if (text) {
        TestUtils.Simulate.change(
          comp.getDOMNode().querySelector("[name='description']"), {
            target: {value: "fake reason"}
          });
      }
    }

    function submitSadFeedbackForm(comp, category, text) {
      TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
    }

    describe("Happy feedback", function() {
      it("should send feedback data when clicking on the happy face",
        function() {
          clickHappyFace(comp);

          sinon.assert.calledOnce(fakeFeedbackApiClient.send);
          sinon.assert.calledWith(fakeFeedbackApiClient.send, {happy: true});
        });

      it("should thank the user once happy feedback data is sent", function() {
        fakeFeedbackApiClient.send = function(data, cb) {
          cb();
        };

        clickHappyFace(comp);

        expect(comp.getDOMNode()
                   .querySelectorAll(".feedback .thank-you").length).eql(1);
        expect(comp.getDOMNode().querySelector("button.back")).to.be.a("null");
      });
    });

    describe("Sad feedback", function() {
      it("should bring the user to feedback form when clicking on the sad face",
        function() {
          clickSadFace(comp);

          expect(comp.getDOMNode().querySelectorAll("form").length).eql(1);
        });

      it("should disable the form submit button when no category is chosen",
        function() {
          clickSadFace(comp);

          expect(comp.getDOMNode()
                     .querySelector("form button").disabled).eql(true);
        });

      it("should disable the form submit button when the 'other' category is " +
         "chosen but no description has been entered yet",
        function() {
          clickSadFace(comp);
          fillSadFeedbackForm(comp, "other");

          expect(comp.getDOMNode()
                     .querySelector("form button").disabled).eql(true);
        });

      it("should enable the form submit button when the 'other' category is " +
         "chosen and a description is entered",
        function() {
          clickSadFace(comp);
          fillSadFeedbackForm(comp, "other", "fake");

          expect(comp.getDOMNode()
                     .querySelector("form button").disabled).eql(false);
        });

      it("should empty the description field when a predefined category is " +
         "chosen",
        function() {
          clickSadFace(comp);

          fillSadFeedbackForm(comp, "confusing");

          expect(comp.getDOMNode()
                     .querySelector("form input[type='text']").value).eql("");
        });

      it("should enable the form submit button once a predefined category is " +
         "chosen",
        function() {
          clickSadFace(comp);

          fillSadFeedbackForm(comp, "confusing");

          expect(comp.getDOMNode()
                     .querySelector("form button").disabled).eql(false);
        });

      it("should disable the form submit button once the form is submitted",
        function() {
          clickSadFace(comp);
          fillSadFeedbackForm(comp, "confusing");

          submitSadFeedbackForm(comp);

          expect(comp.getDOMNode()
                     .querySelector("form button").disabled).eql(true);
        });

      it("should send feedback data when the form is submitted", function() {
        clickSadFace(comp);
        fillSadFeedbackForm(comp, "confusing");

        submitSadFeedbackForm(comp);

        sinon.assert.calledOnce(fakeFeedbackApiClient.send);
        sinon.assert.calledWithMatch(fakeFeedbackApiClient.send, {
          happy: false,
          category: "confusing"
        });
      });

      it("should send feedback data when user has entered a custom description",
        function() {
          clickSadFace(comp);

          fillSadFeedbackForm(comp, "other", "fake reason");
          submitSadFeedbackForm(comp);

          sinon.assert.calledOnce(fakeFeedbackApiClient.send);
          sinon.assert.calledWith(fakeFeedbackApiClient.send, {
            happy: false,
            category: "other",
            description: "fake reason"
          });
        });

      it("should thank the user when feedback data has been sent", function() {
        fakeFeedbackApiClient.send = function(data, cb) {
          cb();
        };
        clickSadFace(comp);
        fillSadFeedbackForm(comp, "confusing");
        submitSadFeedbackForm(comp);

        expect(comp.getDOMNode()
                   .querySelectorAll(".feedback .thank-you").length).eql(1);
      });
    });
  });

  describe("NotificationView", function() {
    var collection, model, view;

    beforeEach(function() {
      $("#fixtures").append('<div id="test-notif"></div>');
      model = new sharedModels.NotificationModel({
        level: "error",
        message: "plop"
      });
      collection = new sharedModels.NotificationCollection([model]);
      view = new sharedViews.NotificationView({
        el: $("#test-notif"),
        collection: collection,
        model: model
      });
    });

    describe("#dismiss", function() {
      it("should automatically dismiss notification after 500ms", function() {
        view.render().dismiss({preventDefault: sandbox.spy()});

        expect(view.$(".message").text()).eql("plop");

        sandbox.clock.tick(500);

        expect(collection).to.have.length.of(0);
        expect($("#test-notif").html()).eql(undefined);
      });
    });

    describe("#render", function() {
      it("should render template with model attribute values", function() {
        view.render();

        expect(view.$(".message").text()).eql("plop");
      });
    });
  });

  describe("NotificationListView", function() {
    var coll, notifData, testNotif;

    beforeEach(function() {
      sandbox.stub(l10n, "get", function(x) {
        return "translated:" + x;
      });
      notifData = {level: "error", message: "plop"};
      testNotif = new sharedModels.NotificationModel(notifData);
      coll = new sharedModels.NotificationCollection();
    });

    describe("#initialize", function() {
      it("should accept a collection option", function() {
        var view = new sharedViews.NotificationListView({collection: coll});

        expect(view.collection).to.be.an.instanceOf(
          sharedModels.NotificationCollection);
      });

      it("should set a default collection when none is passed", function() {
        var view = new sharedViews.NotificationListView();

        expect(view.collection).to.be.an.instanceOf(
          sharedModels.NotificationCollection);
      });
    });

    describe("#clear", function() {
      it("should clear all notifications from the collection", function() {
        var view = new sharedViews.NotificationListView();
        view.notify(testNotif);

        view.clear();

        expect(coll).to.have.length.of(0);
      });
    });

    describe("#notify", function() {
      var view;

      beforeEach(function() {
        view = new sharedViews.NotificationListView({collection: coll});
      });

      describe("adds a new notification to the stack", function() {
        it("using a plain object", function() {
          view.notify(notifData);

          expect(coll).to.have.length.of(1);
        });

        it("using a NotificationModel instance", function() {
          view.notify(testNotif);

          expect(coll).to.have.length.of(1);
        });
      });
    });

    describe("#notifyL10n", function() {
      var view;

      beforeEach(function() {
        view = new sharedViews.NotificationListView({collection: coll});
      });

      it("should translate a message string identifier", function() {
        view.notifyL10n("fakeId", "warning");

        sinon.assert.calledOnce(l10n.get);
        sinon.assert.calledWithExactly(l10n.get, "fakeId");
      });

      it("should notify end user with the provided message", function() {
        sandbox.stub(view, "notify");

        view.notifyL10n("fakeId", "warning");

        sinon.assert.calledOnce(view.notify);
        sinon.assert.calledWithExactly(view.notify, {
          message: "translated:fakeId",
          level: "warning"
        });
      });
    });

    describe("#warn", function() {
      it("should add a warning notification to the stack", function() {
        var view = new sharedViews.NotificationListView({collection: coll});

        view.warn("watch out");

        expect(coll).to.have.length.of(1);
        expect(coll.at(0).get("level")).eql("warning");
        expect(coll.at(0).get("message")).eql("watch out");
      });
    });

    describe("#warnL10n", function() {
      it("should warn using a l10n string id", function() {
        var view = new sharedViews.NotificationListView({collection: coll});
        sandbox.stub(view, "notify");

        view.warnL10n("fakeId");

        sinon.assert.called(view.notify);
        sinon.assert.calledWithExactly(view.notify, {
          message: "translated:fakeId",
          level: "warning"
        });
      });
    });

    describe("#error", function() {
      it("should add an error notification to the stack", function() {
        var view = new sharedViews.NotificationListView({collection: coll});

        view.error("wrong");

        expect(coll).to.have.length.of(1);
        expect(coll.at(0).get("level")).eql("error");
        expect(coll.at(0).get("message")).eql("wrong");
      });
    });

    describe("#errorL10n", function() {
      it("should notify an error using a l10n string id", function() {
        var view = new sharedViews.NotificationListView({collection: coll});
        sandbox.stub(view, "notify");

        view.errorL10n("fakeId");

        sinon.assert.called(view.notify);
        sinon.assert.calledWithExactly(view.notify, {
          message: "translated:fakeId",
          level: "error"
        });
      });
    });

    describe("Collection events", function() {
      var view;

      beforeEach(function() {
        sandbox.stub(sharedViews.NotificationListView.prototype, "render");
        view = new sharedViews.NotificationListView({collection: coll});
      });

      it("should render when a notification is added to the collection",
        function() {
          coll.add(testNotif);

          sinon.assert.calledOnce(view.render);
        });

      it("should render when a notification is removed from the collection",
        function() {
          coll.add(testNotif);
          coll.remove(testNotif);

          sinon.assert.calledTwice(view.render);
        });

      it("should render when the collection is reset", function() {
        coll.reset();

        sinon.assert.calledOnce(view.render);
      });
    });
  });
});

