import { addNumberReducer, FakePrefs } from "test/unit/utils";
import { createStore } from "redux";
import injector from "inject!lib/Store.jsm";

describe("Store", () => {
  let Store;
  let sandbox;
  let store;
  let dbStub;
  beforeEach(() => {
    sandbox = sinon.createSandbox();
    function ActivityStreamMessageChannel(options) {
      this.dispatch = options.dispatch;
      this.createChannel = sandbox.spy();
      this.destroyChannel = sandbox.spy();
      this.middleware = sandbox.spy(s => next => action => next(action));
      this.simulateMessagesForExistingTabs = sandbox.stub();
    }
    dbStub = sandbox.stub().resolves();
    function FakeActivityStreamStorage() {
      this.db = {};
      sinon.stub(this, "db").get(dbStub);
    }
    ({ Store } = injector({
      "lib/ActivityStreamMessageChannel.jsm": { ActivityStreamMessageChannel },
      "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs },
      "lib/ActivityStreamStorage.jsm": {
        ActivityStreamStorage: FakeActivityStreamStorage,
      },
    }));
    store = new Store();
    sandbox.stub(store, "_initIndexedDB").resolves();
  });
  afterEach(() => {
    sandbox.restore();
  });
  it("should have a .feeds property that is a Map", () => {
    assert.instanceOf(store.feeds, Map);
    assert.equal(store.feeds.size, 0, ".feeds.size");
  });
  it("should have a redux store at ._store", () => {
    assert.ok(store._store);
    assert.property(store, "dispatch");
    assert.property(store, "getState");
  });
  it("should create a ActivityStreamMessageChannel with the right dispatcher", () => {
    assert.ok(store.getMessageChannel());
    assert.equal(store.getMessageChannel().dispatch, store.dispatch);
    assert.equal(store.getMessageChannel(), store._messageChannel);
  });
  it("should connect the ActivityStreamMessageChannel's middleware", () => {
    store.dispatch({ type: "FOO" });
    assert.calledOnce(store._messageChannel.middleware);
  });
  describe("#initFeed", () => {
    it("should add an instance of the feed to .feeds", () => {
      class Foo {}
      store._prefs.set("foo", true);
      store.init(new Map([["foo", () => new Foo()]]));
      store.initFeed("foo");

      assert.isTrue(store.feeds.has("foo"), "foo is set");
      assert.instanceOf(store.feeds.get("foo"), Foo);
    });
    it("should call the feed's onAction with uninit action if it exists", () => {
      let feed;
      function createFeed() {
        feed = { onAction: sinon.spy() };
        return feed;
      }
      const action = { type: "FOO" };
      store._feedFactories = new Map([["foo", createFeed]]);

      store.initFeed("foo", action);

      assert.calledOnce(feed.onAction);
      assert.calledWith(feed.onAction, action);
    });
    it("should add a .store property to the feed", () => {
      class Foo {}
      store._feedFactories = new Map([["foo", () => new Foo()]]);
      store.initFeed("foo");

      assert.propertyVal(store.feeds.get("foo"), "store", store);
    });
  });
  describe("#uninitFeed", () => {
    it("should not throw if no feed with that name exists", () => {
      assert.doesNotThrow(() => {
        store.uninitFeed("bar");
      });
    });
    it("should call the feed's onAction with uninit action if it exists", () => {
      let feed;
      function createFeed() {
        feed = { onAction: sinon.spy() };
        return feed;
      }
      const action = { type: "BAR" };
      store._feedFactories = new Map([["foo", createFeed]]);
      store.initFeed("foo");

      store.uninitFeed("foo", action);

      assert.calledOnce(feed.onAction);
      assert.calledWith(feed.onAction, action);
    });
    it("should remove the feed from .feeds", () => {
      class Foo {}
      store._feedFactories = new Map([["foo", () => new Foo()]]);

      store.initFeed("foo");
      store.uninitFeed("foo");

      assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
    });
  });
  describe("onPrefChanged", () => {
    beforeEach(() => {
      sinon.stub(store, "initFeed");
      sinon.stub(store, "uninitFeed");
      store._prefs.set("foo", false);
      store.init(new Map([["foo", () => ({})]]));
    });
    it("should initialize the feed if called with true", () => {
      store.onPrefChanged("foo", true);

      assert.calledWith(store.initFeed, "foo");
      assert.notCalled(store.uninitFeed);
    });
    it("should uninitialize the feed if called with false", () => {
      store.onPrefChanged("foo", false);

      assert.calledWith(store.uninitFeed, "foo");
      assert.notCalled(store.initFeed);
    });
    it("should do nothing if not an expected feed", () => {
      store.onPrefChanged("bar", false);

      assert.notCalled(store.initFeed);
      assert.notCalled(store.uninitFeed);
    });
  });
  describe("#init", () => {
    it("should call .initFeed with each key", async () => {
      sinon.stub(store, "initFeed");
      store._prefs.set("foo", true);
      store._prefs.set("bar", true);
      await store.init(
        new Map([
          ["foo", () => {}],
          ["bar", () => {}],
        ])
      );
      assert.calledWith(store.initFeed, "foo");
      assert.calledWith(store.initFeed, "bar");
    });
    it("should call _initIndexedDB", async () => {
      await store.init(new Map());

      assert.calledOnce(store._initIndexedDB);
      assert.calledWithExactly(store._initIndexedDB, "feeds.telemetry");
    });
    it("should access the db property of indexedDB", async () => {
      store._initIndexedDB.restore();
      await store.init(new Map());

      assert.calledOnce(dbStub);
    });
    it("should reset ActivityStreamStorage telemetry if opening the db fails", async () => {
      store._initIndexedDB.restore();
      // Force an IndexedDB error
      dbStub.rejects();

      await store.init(new Map());

      assert.calledOnce(dbStub);
      assert.isNull(store.dbStorage.telemetry);
    });
    it("should not initialize the feed if the Pref is set to false", async () => {
      sinon.stub(store, "initFeed");
      store._prefs.set("foo", false);
      await store.init(new Map([["foo", () => {}]]));
      assert.notCalled(store.initFeed);
    });
    it("should observe the pref branch", async () => {
      sinon.stub(store._prefs, "observeBranch");
      await store.init(new Map());
      assert.calledOnce(store._prefs.observeBranch);
      assert.calledWith(store._prefs.observeBranch, store);
    });
    it("should initialize the ActivityStreamMessageChannel channel", async () => {
      await store.init(new Map());
    });
    it("should emit an initial event if provided", async () => {
      sinon.stub(store, "dispatch");
      const action = { type: "FOO" };

      await store.init(new Map(), action);

      assert.calledOnce(store.dispatch);
      assert.calledWith(store.dispatch, action);
    });
    it("should initialize the telemtry feed first", () => {
      store._prefs.set("feeds.foo", true);
      store._prefs.set("feeds.telemetry", true);
      const telemetrySpy = sandbox.stub().returns({});
      const fooSpy = sandbox.stub().returns({});
      // Intentionally put the telemetry feed as the second item.
      const feedFactories = new Map([
        ["feeds.foo", fooSpy],
        ["feeds.telemetry", telemetrySpy],
      ]);
      store.init(feedFactories);
      assert.ok(telemetrySpy.calledBefore(fooSpy));
    });
    it("should dispatch init/load events", async () => {
      await store.init(new Map(), { type: "FOO" });

      assert.calledOnce(
        store.getMessageChannel().simulateMessagesForExistingTabs
      );
    });
    it("should dispatch INIT before LOAD", async () => {
      const init = { type: "INIT" };
      const load = { type: "TAB_LOAD" };
      sandbox.stub(store, "dispatch");
      store
        .getMessageChannel()
        .simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load));
      await store.init(new Map(), init);

      assert.calledTwice(store.dispatch);
      assert.equal(store.dispatch.firstCall.args[0], init);
      assert.equal(store.dispatch.secondCall.args[0], load);
    });
  });
  describe("#uninit", () => {
    it("should emit an uninit event if provided on init", () => {
      sinon.stub(store, "dispatch");
      const action = { type: "BAR" };
      store.init(new Map(), null, action);

      store.uninit();

      assert.calledOnce(store.dispatch);
      assert.calledWith(store.dispatch, action);
    });
    it("should clear .feeds and ._feedFactories", () => {
      store._prefs.set("a", true);
      store.init(
        new Map([
          ["a", () => ({})],
          ["b", () => ({})],
          ["c", () => ({})],
        ])
      );

      store.uninit();

      assert.equal(store.feeds.size, 0);
      assert.isNull(store._feedFactories);
    });
  });
  describe("#getState", () => {
    it("should return the redux state", () => {
      store._store = createStore((prevState = 123) => prevState);
      const { getState } = store;
      assert.equal(getState(), 123);
    });
  });
  describe("#dispatch", () => {
    it("should call .onAction of each feed", async () => {
      const { dispatch } = store;
      const sub = { onAction: sinon.spy() };
      const action = { type: "FOO" };

      store._prefs.set("sub", true);
      await store.init(new Map([["sub", () => sub]]));

      dispatch(action);

      assert.calledWith(sub.onAction, action);
    });
    it("should call the reducers", () => {
      const { dispatch } = store;
      store._store = createStore(addNumberReducer);

      dispatch({ type: "ADD", data: 14 });

      assert.equal(store.getState(), 14);
    });
  });
  describe("#subscribe", () => {
    it("should subscribe to changes to the store", () => {
      const sub = sinon.spy();
      const action = { type: "FOO" };

      store.subscribe(sub);
      store.dispatch(action);

      assert.calledOnce(sub);
    });
  });
});
