import {
  Thread,
  Actions,
  Contact,
  Message,
  Account,
  DraftStore,
  DraftHelpers,
  DatabaseStore,
  SoundRegistry,
  DestroyDraftTask,
  ComposerExtension,
  ExtensionRegistry,
  FocusedContentStore,
  DatabaseWriter,
} from 'nylas-exports';

import {remote} from 'electron';
import DraftFactory from '../../src/flux/stores/draft-factory';

class TestExtension extends ComposerExtension {
  static prepareNewDraft({draft}) {
    draft.body = `Edited by TestExtension! ${draft.body}`;
  }
}

xdescribe('DraftStore', function draftStore() {
  beforeEach(() => {
    this.fakeThread = new Thread({id: 'fake-thread', clientId: 'fake-thread'});
    this.fakeMessage = new Message({id: 'fake-message', clientId: 'fake-message'});

    spyOn(NylasEnv, 'newWindow').andCallFake(() => {});
    spyOn(DatabaseWriter.prototype, "persistModel").andReturn(Promise.resolve());
    spyOn(DatabaseStore, 'run').andCallFake((query) => {
      if (query._klass === Thread) { return Promise.resolve(this.fakeThread); }
      if (query._klass === Message) { return Promise.resolve(this.fakeMessage); }
      if (query._klass === Contact) { return Promise.resolve(null); }
      return Promise.reject(new Error(`Not Stubbed for class ${query._klass.name}`));
    });

    for (const draftClientId of Object.keys(DraftStore._draftSessions)) {
      const sess = DraftStore._draftSessions[draftClientId];
      if (sess.teardown) {
        DraftStore._doneWithSession(sess);
      }
    }
    DraftStore._draftSessions = {};
  });

  describe("creating and opening drafts", () => {
    beforeEach(() => {
      const draft = new Message({id: "A", subject: "B", clientId: "A", body: "123"});
      this.newDraft = draft;
      spyOn(DraftFactory, "createDraftForReply").andReturn(Promise.resolve(draft));
      spyOn(DraftFactory, "createOrUpdateDraftForReply").andReturn(Promise.resolve(draft));
      spyOn(DraftFactory, "createDraftForForward").andReturn(Promise.resolve(draft));
      spyOn(DraftFactory, "createDraft").andReturn(Promise.resolve(draft));
    });

    it("should always attempt to focus the new draft", () => {
      spyOn(Actions, 'focusDraft')
      DraftStore._onComposeReply({
        threadId: this.fakeThread.id,
        messageId: this.fakeMessage.id,
        type: 'reply',
        behavior: 'prefer-existing',
      });
      advanceClock();
      advanceClock();
      expect(Actions.focusDraft).toHaveBeenCalled();
    });

    describe("context", () => {
      it("can accept IDs for thread and message arguments", () => {
        DraftStore._onComposeReply({
          threadId: this.fakeThread.id,
          messageId: this.fakeMessage.id,
          type: 'reply',
          behavior: 'prefer-existing',
        });
        advanceClock();
        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({
          thread: this.fakeThread,
          message: this.fakeMessage,
          type: 'reply',
          behavior: 'prefer-existing',
        });
      });

      it("can accept models for thread and message arguments", () => {
        DraftStore._onComposeReply({
          thread: this.fakeThread,
          message: this.fakeMessage,
          type: 'reply',
          behavior: 'prefer-existing',
        });
        advanceClock();
        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({
          thread: this.fakeThread,
          message: this.fakeMessage,
          type: 'reply',
          behavior: 'prefer-existing',
        });
      });

      it("can accept only a thread / threadId, and use the last message on the thread", () => {
        DraftStore._onComposeReply({
          thread: this.fakeThread,
          type: 'reply',
          behavior: 'prefer-existing',
        });
        advanceClock();
        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({
          thread: this.fakeThread,
          message: this.fakeMessage,
          type: 'reply',
          behavior: 'prefer-existing',
        });
      });
    });

    describe("popout behavior", () => {
      it("can popout a reply", () => {
        runs(() => {
          DraftStore._onComposeReply({
            threadId: this.fakeThread.id,
            messageId: this.fakeMessage.id,
            type: 'reply',
            popout: true}
          );
        });
        waitsFor(() => {
          return DatabaseWriter.prototype.persistModel.callCount > 0;
        });
        runs(() => {
          expect(NylasEnv.newWindow).toHaveBeenCalledWith({
            title: 'Message',
            hidden: true,
            windowKey: `composer-A`,
            windowType: "composer",
            windowProps: { draftClientId: "A", draftJSON: this.newDraft.toJSON() },
          });
        });
      });

      it("can popout a forward", () => {
        runs(() => {
          DraftStore._onComposeForward({
            threadId: this.fakeThread.id,
            messageId: this.fakeMessage.id,
            popout: true,
          });
        });
        waitsFor(() => {
          return DatabaseWriter.prototype.persistModel.callCount > 0;
        });
        runs(() => {
          expect(NylasEnv.newWindow).toHaveBeenCalledWith({
            title: 'Message',
            hidden: true,
            windowKey: `composer-A`,
            windowType: "composer",
            windowProps: { draftClientId: "A", draftJSON: this.newDraft.toJSON() },
          });
        });
      });
    });
  });

  describe("onDestroyDraft", () => {
    beforeEach(() => {
      this.draftSessionTeardown = jasmine.createSpy('draft teardown');
      this.session =
      {draft() {
        return {pristine: false};
      },
        changes:
        {commit() { return Promise.resolve(); },
          teardown() {},
        },
        teardown: this.draftSessionTeardown,
      };
      DraftStore._draftSessions = {abc: this.session};
      spyOn(Actions, 'queueTask');
    });

    it("should teardown the draft session, ensuring no more saves are made", () => {
      DraftStore._onDestroyDraft('abc');
      expect(this.draftSessionTeardown).toHaveBeenCalled();
    });

    it("should not throw if the draft session is not in the window", () => {
      expect(() => DraftStore._onDestroyDraft('other')).not.toThrow();
    });

    it("should queue a destroy draft task", () => {
      DraftStore._onDestroyDraft('abc');
      expect(Actions.queueTask).toHaveBeenCalled();
      expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true);
    });

    it("should clean up the draft session", () => {
      spyOn(DraftStore, '_doneWithSession');
      DraftStore._onDestroyDraft('abc');
      expect(DraftStore._doneWithSession).toHaveBeenCalledWith(this.session);
    });

    it("should close the window if it's a popout", () => {
      spyOn(NylasEnv, "close");
      spyOn(NylasEnv, "isComposerWindow").andReturn(true);
      DraftStore._onDestroyDraft('abc');
      expect(NylasEnv.close).toHaveBeenCalled();
    });

    it("should NOT close the window if isn't a popout", () => {
      spyOn(NylasEnv, "close");
      spyOn(NylasEnv, "isComposerWindow").andReturn(false);
      DraftStore._onDestroyDraft('abc');
      expect(NylasEnv.close).not.toHaveBeenCalled();
    });
  });

  describe("before unloading", () => {
    it("should destroy pristine drafts", () => {
      DraftStore._draftSessions = {
        abc: {
          changes: {},
          draft() {
            return {pristine: true};
          },
        },
      };

      spyOn(Actions, 'queueTask');
      DraftStore._onBeforeUnload();
      expect(Actions.queueTask).toHaveBeenCalled();
      expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true);
    });

    describe("when drafts return unresolved commit promises", () => {
      beforeEach(() => {
        this.resolve = null;
        DraftStore._draftSessions = {
          abc: {
            changes: {
              commit: () => new Promise((resolve) => { this.resolve = resolve }),
            },
            draft() {
              return {pristine: false};
            },
          },
        };
      });

      it("should return false and call window.close itself", () => {
        const callback = jasmine.createSpy('callback');
        expect(DraftStore._onBeforeUnload(callback)).toBe(false);
        expect(callback).not.toHaveBeenCalled();
        this.resolve();
        advanceClock(1000);
        advanceClock(1000);
        expect(callback).toHaveBeenCalled();
      });
    });

    describe("when drafts return immediately fulfilled commit promises", () => {
      beforeEach(() => {
        DraftStore._draftSessions = {
          abc: {
            changes:
              {commit: () => Promise.resolve()},
            draft() {
              return {pristine: false};
            },
          },
        };
      });

      it("should still wait one tick before firing NylasEnv.close again", () => {
        const callback = jasmine.createSpy('callback');
        expect(DraftStore._onBeforeUnload(callback)).toBe(false);
        expect(callback).not.toHaveBeenCalled();
        advanceClock();
        advanceClock(1000);
        expect(callback).toHaveBeenCalled();
      });
    });

    describe("when there are no drafts", () => {
      beforeEach(() => {
        DraftStore._draftSessions = {};
      });

      it("should return true and allow the window to close", () => {
        expect(DraftStore._onBeforeUnload()).toBe(true);
      });
    });
  });

  describe("sending a draft", () => {
    beforeEach(() => {
      this.draft = new Message({
        clientId: "local-123",
        threadId: "thread-123",
        replyToMessageId: "message-123",
        files: ['stub'],
      });
      DraftStore._draftSessions = {};
      DraftStore._draftsSending = {};
      this.forceCommit = false;
      const session = {
        prepare() {
          return Promise.resolve(session);
        },
        teardown() {},
        draft: () => this.draft,
        changes: {
          commit: ({force} = {}) => {
            this.forceCommit = force;
            return Promise.resolve();
          },
        },
      };

      DraftStore._draftSessions[this.draft.clientId] = session;
      spyOn(DraftStore, "_doneWithSession").andCallThrough();
      spyOn(DraftHelpers, "draftPreparedForSyncback").andReturn(Promise.resolve());
      spyOn(DraftStore, "trigger");
      spyOn(SoundRegistry, "playSound");
      spyOn(Actions, "queueTask");
    });

    it("plays a sound immediately when sending draft", () => {
      spyOn(NylasEnv.config, "get").andReturn(true);
      DraftStore._onSendDraft(this.draft.headerMessageId);
      advanceClock();
      expect(NylasEnv.config.get).toHaveBeenCalledWith("core.sending.sounds");
      expect(SoundRegistry.playSound).toHaveBeenCalledWith("hit-send");
    });

    it("doesn't plays a sound if the setting is off", () => {
      spyOn(NylasEnv.config, "get").andReturn(false);
      DraftStore._onSendDraft(this.draft.headerMessageId);
      advanceClock();
      expect(NylasEnv.config.get).toHaveBeenCalledWith("core.sending.sounds");
      expect(SoundRegistry.playSound).not.toHaveBeenCalled();
    });

    it("sets the sending state when sending", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      DraftStore._onSendDraft(this.draft.headerMessageId);
      advanceClock();
      expect(DraftStore.isSendingDraft(this.draft.headerMessageId)).toBe(true);
    });

    // Since all changes haven't been applied yet, we want to ensure that
    // no view of the draft renders the draft as if its sending, but with
    // the wrong text.
    it("does NOT trigger until the latest changes have been applied", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      runs(() => {
        DraftStore._onSendDraft(this.draft.headerMessageId);
        expect(DraftStore.trigger).not.toHaveBeenCalled();
      });
      waitsFor(() => {
        return Actions.queueTask.calls.length > 0;
      });
      runs(() => {
        // Normally, the session.changes.commit will persist to the
        // Database. Since that's stubbed out, we need to manually invoke
        // to database update event to get the trigger (which we want to
        // test) to fire
        DraftStore._onDataChanged({
          objectClass: "Message",
          objects: [{draft: true}],
        });
        expect(DraftStore.isSendingDraft(this.draft.headerMessageId)).toBe(true);
        expect(DraftStore.trigger).toHaveBeenCalled();
        expect(DraftStore.trigger.calls.length).toBe(1);
      });
    });

    it("returns false if the draft hasn't been seen", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      expect(DraftStore.isSendingDraft(this.draft.headerMessageId)).toBe(false);
    });

    it("closes the window if it's a popout", () => {
      spyOn(NylasEnv, "getWindowType").andReturn("composer");
      spyOn(NylasEnv, "isMainWindow").andReturn(false);
      spyOn(NylasEnv, "close");
      runs(() => {
        return DraftStore._onSendDraft(this.draft.headerMessageId);
      });
      waitsFor("N1 to close", () => NylasEnv.close.calls.length > 0);
    });

    it("doesn't close the window if it's inline", () => {
      spyOn(NylasEnv, "getWindowType").andReturn("other");
      spyOn(NylasEnv, "isMainWindow").andReturn(false);
      spyOn(NylasEnv, "close");
      spyOn(NylasEnv, "isComposerWindow").andCallThrough();
      runs(() => {
        DraftStore._onSendDraft(this.draft.headerMessageId);
      });
      waitsFor(() => NylasEnv.isComposerWindow.calls.length > 0);
      runs(() => {
        expect(NylasEnv.close).not.toHaveBeenCalled();
      });
    });

    it("resets the sending state if there's an error", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(false);
      DraftStore._draftsSending[this.draft.headerMessageId] = true;
      Actions.draftDeliveryFailed({errorMessage: "boohoo", draftheaderMessageId: this.draft.headerMessageId});
      expect(DraftStore.isSendingDraft(this.draft.headerMessageId)).toBe(false);
      expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.headerMessageId);
    });

    it("displays a popup in the main window if there's an error", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      spyOn(FocusedContentStore, "focused").andReturn({id: "t1"});
      spyOn(remote.dialog, "showMessageBox");
      spyOn(Actions, "composePopoutDraft");
      DraftStore._draftsSending[this.draft.headerMessageId] = true;
      Actions.draftDeliveryFailed({threadId: 't1', errorMessage: "boohoo", draftheaderMessageId: this.draft.headerMessageId});
      advanceClock(400);
      expect(DraftStore.isSendingDraft(this.draft.headerMessageId)).toBe(false);
      expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.headerMessageId);
      expect(remote.dialog.showMessageBox).toHaveBeenCalled();
      const dialogArgs = remote.dialog.showMessageBox.mostRecentCall.args[1];
      expect(dialogArgs.detail).toEqual("boohoo");
      expect(Actions.composePopoutDraft).not.toHaveBeenCalled();
    });

    it("re-opens the draft if you're not looking at the thread", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      spyOn(FocusedContentStore, "focused").andReturn({id: "t1"});
      spyOn(Actions, "composePopoutDraft");
      DraftStore._draftsSending[this.draft.headerMessageId] = true;
      Actions.draftDeliveryFailed({threadId: 't2', errorMessage: "boohoo", draftheaderMessageId: this.draft.headerMessageId});
      advanceClock(400);
      expect(Actions.composePopoutDraft).toHaveBeenCalled();
      const call = Actions.composePopoutDraft.calls[0];
      expect(call.args[0]).toBe(this.draft.headerMessageId);
      expect(call.args[1]).toEqual({errorMessage: "boohoo"});
    });

    it("re-opens the draft if there is no thread id", () => {
      spyOn(NylasEnv, "isMainWindow").andReturn(true);
      spyOn(Actions, "composePopoutDraft");
      DraftStore._draftsSending[this.draft.headerMessageId] = true;
      spyOn(FocusedContentStore, "focused").andReturn(null);
      Actions.draftDeliveryFailed({errorMessage: "boohoo", draftheaderMessageId: this.draft.headerMessageId});
      advanceClock(400);
      expect(Actions.composePopoutDraft).toHaveBeenCalled();
      const call = Actions.composePopoutDraft.calls[0];
      expect(call.args[0]).toBe(this.draft.headerMessageId);
      expect(call.args[1]).toEqual({errorMessage: "boohoo"});
    });
  });

  describe("session teardown", () => {
    beforeEach(() => {
      spyOn(NylasEnv, 'isMainWindow').andReturn(true);
      this.draftTeardown = jasmine.createSpy('draft teardown');
      this.session = {
        draftClientId: "abc",
        draft() {
          return {pristine: false};
        },
        changes: {
          commit() { return Promise.resolve(); },
          reset() {},
        },
        teardown: this.draftTeardown,
      };
      DraftStore._draftSessions = {abc: this.session};
      DraftStore._doneWithSession(this.session);
    });

    it("removes from the list of draftSessions", () => {
      expect(DraftStore._draftSessions.abc).toBeUndefined();
    });

    it("Calls teardown on the session", () => {
      expect(this.draftTeardown).toHaveBeenCalled();
    });
  });

  describe("mailto handling", () => {
    beforeEach(() => {
      spyOn(NylasEnv, 'isMainWindow').andReturn(true);
    });

    describe("extensions", () => {
      beforeEach(() => {
        ExtensionRegistry.Composer.register(TestExtension);
      });
      afterEach(() => {
        ExtensionRegistry.Composer.unregister(TestExtension);
      });

      it("should give extensions a chance to customize the draft via ext.prepareNewDraft", () => {
        waitsForPromise(() => {
          return DraftStore._onHandleMailtoLink({}, 'mailto:bengotow@gmail.com').then(() => {
            const received = DatabaseWriter.prototype.persistModel.mostRecentCall.args[0];
            expect(received.body.indexOf("Edited by TestExtension!")).toBe(0);
          });
        });
      });
    });

    it("should call through to DraftFactory and popout a new draft", () => {
      const draft = new Message({clientId: "A", body: '123'});
      spyOn(DraftFactory, 'createDraftForMailto').andReturn(Promise.resolve(draft));
      spyOn(DraftStore, '_onPopoutDraftClientId');
      waitsForPromise(() => {
        return DraftStore._onHandleMailtoLink({}, 'mailto:bengotow@gmail.com').then(() => {
          const received = DatabaseWriter.prototype.persistModel.mostRecentCall.args[0];
          expect(received).toEqual(draft);
          expect(DraftStore._onPopoutDraftClientId).toHaveBeenCalled();
        });
      });
    });
  });

  describe("mailfiles handling", () => {
    it("should popout a new draft", () => {
      const defaultMe = new Contact();
      spyOn(DraftStore, '_onPopoutDraftClientId');
      spyOn(Account.prototype, 'defaultMe').andReturn(defaultMe);
      spyOn(Actions, 'addAttachment').andCallFake(({onCreated}) => onCreated());
      DraftStore._onHandleMailFiles({}, ['/Users/ben/file1.png', '/Users/ben/file2.png']);
      waitsFor(() => DatabaseWriter.prototype.persistModel.callCount > 0);
      runs(() => {
        const {body, subject, from} = DatabaseWriter.prototype.persistModel.calls[0].args[0];
        expect({body, subject, from}).toEqual({body: '', subject: '', from: [defaultMe]});
        expect(DraftStore._onPopoutDraftClientId).toHaveBeenCalled();
      });
    });

    it("should call addAttachment for each provided file path", () => {
      spyOn(Actions, 'addAttachment');
      DraftStore._onHandleMailFiles({}, ['/Users/ben/file1.png', '/Users/ben/file2.png']);
      waitsFor(() => Actions.addAttachment.callCount === 2);
      runs(() => {
        expect(Actions.addAttachment.calls[0].args[0].filePath).toEqual('/Users/ben/file1.png');
        expect(Actions.addAttachment.calls[1].args[0].filePath).toEqual('/Users/ben/file2.png');
      });
    });
  });
});
