const PaneContainer = require('../src/pane-container');

describe('PaneContainer', () => {
  let confirm, params;

  beforeEach(() => {
    confirm = spyOn(atom.applicationDelegate, 'confirm').andCallFake(
      (options, callback) => callback(0)
    );
    params = {
      location: 'center',
      config: atom.config,
      deserializerManager: atom.deserializers,
      applicationDelegate: atom.applicationDelegate,
      viewRegistry: atom.views
    };
  });

  describe('serialization', () => {
    let containerA, pane1A, pane2A, pane3A;

    beforeEach(() => {
      // This is a dummy item to prevent panes from being empty on deserialization
      class Item {
        static deserialize() {
          return new this();
        }
        serialize() {
          return { deserializer: 'Item' };
        }
      }
      atom.deserializers.add(Item);

      containerA = new PaneContainer(params);
      pane1A = containerA.getActivePane();
      pane1A.addItem(new Item());
      pane2A = pane1A.splitRight({ items: [new Item()] });
      pane3A = pane2A.splitDown({ items: [new Item()] });
      pane3A.focus();
    });

    it('preserves the focused pane across serialization', () => {
      expect(pane3A.focused).toBe(true);

      const containerB = new PaneContainer(params);
      containerB.deserialize(containerA.serialize(), atom.deserializers);
      const pane3B = containerB.getPanes()[2];
      expect(pane3B.focused).toBe(true);
    });

    it('preserves the active pane across serialization, independent of focus', () => {
      pane3A.activate();
      expect(containerA.getActivePane()).toBe(pane3A);

      const containerB = new PaneContainer(params);
      containerB.deserialize(containerA.serialize(), atom.deserializers);
      const pane3B = containerB.getPanes()[2];
      expect(containerB.getActivePane()).toBe(pane3B);
    });

    it('makes the first pane active if no pane exists for the activePaneId', () => {
      pane3A.activate();
      const state = containerA.serialize();
      state.activePaneId = -22;
      const containerB = new PaneContainer(params);
      containerB.deserialize(state, atom.deserializers);
      expect(containerB.getActivePane()).toBe(containerB.getPanes()[0]);
    });

    describe('if there are empty panes after deserialization', () => {
      beforeEach(() => {
        pane3A.getItems()[0].serialize = () => ({ deserializer: 'Bogus' });
      });

      describe("if the 'core.destroyEmptyPanes' config option is false (the default)", () =>
        it('leaves the empty panes intact', () => {
          const state = containerA.serialize();
          const containerB = new PaneContainer(params);
          containerB.deserialize(state, atom.deserializers);
          const [leftPane, column] = containerB.getRoot().getChildren();
          const [topPane, bottomPane] = column.getChildren();

          expect(leftPane.getItems().length).toBe(1);
          expect(topPane.getItems().length).toBe(1);
          expect(bottomPane.getItems().length).toBe(0);
        }));

      describe("if the 'core.destroyEmptyPanes' config option is true", () =>
        it('removes empty panes on deserialization', () => {
          atom.config.set('core.destroyEmptyPanes', true);

          const state = containerA.serialize();
          const containerB = new PaneContainer(params);
          containerB.deserialize(state, atom.deserializers);
          const [leftPane, rightPane] = containerB.getRoot().getChildren();

          expect(leftPane.getItems().length).toBe(1);
          expect(rightPane.getItems().length).toBe(1);
        }));
    });
  });

  it('does not allow the root pane to be destroyed', () => {
    const container = new PaneContainer(params);
    container.getRoot().destroy();
    expect(container.getRoot()).toBeDefined();
    expect(container.getRoot().isDestroyed()).toBe(false);
  });

  describe('::getActivePane()', () => {
    let container, pane1, pane2;

    beforeEach(() => {
      container = new PaneContainer(params);
      pane1 = container.getRoot();
    });

    it('returns the first pane if no pane has been made active', () => {
      expect(container.getActivePane()).toBe(pane1);
      expect(pane1.isActive()).toBe(true);
    });

    it('returns the most pane on which ::activate() was most recently called', () => {
      pane2 = pane1.splitRight();
      pane2.activate();
      expect(container.getActivePane()).toBe(pane2);
      expect(pane1.isActive()).toBe(false);
      expect(pane2.isActive()).toBe(true);
      pane1.activate();
      expect(container.getActivePane()).toBe(pane1);
      expect(pane1.isActive()).toBe(true);
      expect(pane2.isActive()).toBe(false);
    });

    it('returns the next pane if the current active pane is destroyed', () => {
      pane2 = pane1.splitRight();
      pane2.activate();
      pane2.destroy();
      expect(container.getActivePane()).toBe(pane1);
      expect(pane1.isActive()).toBe(true);
    });
  });

  describe('::onDidChangeActivePane()', () => {
    let container, pane1, pane2, observed;

    beforeEach(() => {
      container = new PaneContainer(params);
      container.getRoot().addItems([{}, {}]);
      container.getRoot().splitRight({ items: [{}, {}] });
      [pane1, pane2] = container.getPanes();

      observed = [];
      container.onDidChangeActivePane(pane => observed.push(pane));
    });

    it('invokes observers when the active pane changes', () => {
      pane1.activate();
      pane2.activate();
      expect(observed).toEqual([pane1, pane2]);
    });
  });

  describe('::onDidChangeActivePaneItem()', () => {
    let container, pane1, pane2, observed;

    beforeEach(() => {
      container = new PaneContainer(params);
      container.getRoot().addItems([{}, {}]);
      container.getRoot().splitRight({ items: [{}, {}] });
      [pane1, pane2] = container.getPanes();

      observed = [];
      container.onDidChangeActivePaneItem(item => observed.push(item));
    });

    it('invokes observers when the active item of the active pane changes', () => {
      pane2.activateNextItem();
      pane2.activateNextItem();
      expect(observed).toEqual([pane2.itemAtIndex(1), pane2.itemAtIndex(0)]);
    });

    it('invokes observers when the active pane changes', () => {
      pane1.activate();
      pane2.activate();
      expect(observed).toEqual([pane1.itemAtIndex(0), pane2.itemAtIndex(0)]);
    });
  });

  describe('::onDidStopChangingActivePaneItem()', () => {
    let container, pane1, pane2, observed;

    beforeEach(() => {
      container = new PaneContainer(params);
      container.getRoot().addItems([{}, {}]);
      container.getRoot().splitRight({ items: [{}, {}] });
      [pane1, pane2] = container.getPanes();

      observed = [];
      container.onDidStopChangingActivePaneItem(item => observed.push(item));
    });

    it('invokes observers once when the active item of the active pane changes', () => {
      pane2.activateNextItem();
      pane2.activateNextItem();
      expect(observed).toEqual([]);
      advanceClock(100);
      expect(observed).toEqual([pane2.itemAtIndex(0)]);
    });

    it('invokes observers once when the active pane changes', () => {
      pane1.activate();
      pane2.activate();
      expect(observed).toEqual([]);
      advanceClock(100);
      expect(observed).toEqual([pane2.itemAtIndex(0)]);
    });
  });

  describe('::onDidActivatePane', () => {
    it('invokes observers when a pane is activated (even if it was already active)', () => {
      const container = new PaneContainer(params);
      container.getRoot().splitRight();
      const [pane1, pane2] = container.getPanes();

      const activatedPanes = [];
      container.onDidActivatePane(pane => activatedPanes.push(pane));

      pane1.activate();
      pane1.activate();
      pane2.activate();
      pane2.activate();
      expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2]);
    });
  });

  describe('::observePanes()', () => {
    it('invokes observers with all current and future panes', () => {
      const container = new PaneContainer(params);
      container.getRoot().splitRight();
      const [pane1, pane2] = container.getPanes();

      const observed = [];
      container.observePanes(pane => observed.push(pane));

      const pane3 = pane2.splitDown();
      const pane4 = pane2.splitRight();

      expect(observed).toEqual([pane1, pane2, pane3, pane4]);
    });
  });

  describe('::observePaneItems()', () =>
    it('invokes observers with all current and future pane items', () => {
      const container = new PaneContainer(params);
      container.getRoot().addItems([{}, {}]);
      container.getRoot().splitRight({ items: [{}] });
      const pane2 = container.getPanes()[1];
      const observed = [];
      container.observePaneItems(pane => observed.push(pane));

      const pane3 = pane2.splitDown({ items: [{}] });
      pane3.addItems([{}, {}]);

      expect(observed).toEqual(container.getPaneItems());
    }));

  describe('::confirmClose()', () => {
    let container, pane1, pane2;

    beforeEach(() => {
      class TestItem {
        shouldPromptToSave() {
          return true;
        }
        getURI() {
          return 'test';
        }
      }

      container = new PaneContainer(params);
      container.getRoot().splitRight();
      [pane1, pane2] = container.getPanes();
      pane1.addItem(new TestItem());
      pane2.addItem(new TestItem());
    });

    it('returns true if the user saves all modified files when prompted', async () => {
      confirm.andCallFake((options, callback) => callback(0));
      const saved = await container.confirmClose();
      expect(confirm).toHaveBeenCalled();
      expect(saved).toBeTruthy();
    });

    it('returns false if the user cancels saving any modified file', async () => {
      confirm.andCallFake((options, callback) => callback(1));
      const saved = await container.confirmClose();
      expect(confirm).toHaveBeenCalled();
      expect(saved).toBeFalsy();
    });
  });

  describe('::onDidAddPane(callback)', () => {
    it('invokes the given callback when panes are added', () => {
      const container = new PaneContainer(params);
      const events = [];
      container.onDidAddPane(event => {
        expect(container.getPanes().includes(event.pane)).toBe(true);
        events.push(event);
      });

      const pane1 = container.getActivePane();
      const pane2 = pane1.splitRight();
      const pane3 = pane2.splitDown();

      expect(events).toEqual([{ pane: pane2 }, { pane: pane3 }]);
    });
  });

  describe('::onWillDestroyPane(callback)', () => {
    it('invokes the given callback before panes or their items are destroyed', () => {
      class TestItem {
        constructor() {
          this._isDestroyed = false;
        }
        destroy() {
          this._isDestroyed = true;
        }
        isDestroyed() {
          return this._isDestroyed;
        }
      }

      const container = new PaneContainer(params);
      const events = [];
      container.onWillDestroyPane(event => {
        const itemsDestroyed = event.pane
          .getItems()
          .map(item => item.isDestroyed());
        events.push([event, { itemsDestroyed }]);
      });

      const pane1 = container.getActivePane();
      const pane2 = pane1.splitRight();
      pane2.addItem(new TestItem());

      pane2.destroy();

      expect(events).toEqual([[{ pane: pane2 }, { itemsDestroyed: [false] }]]);
    });
  });

  describe('::onDidDestroyPane(callback)', () => {
    it('invokes the given callback when panes are destroyed', () => {
      const container = new PaneContainer(params);
      const events = [];
      container.onDidDestroyPane(event => {
        expect(container.getPanes().includes(event.pane)).toBe(false);
        events.push(event);
      });

      const pane1 = container.getActivePane();
      const pane2 = pane1.splitRight();
      const pane3 = pane2.splitDown();

      pane2.destroy();
      pane3.destroy();

      expect(events).toEqual([{ pane: pane2 }, { pane: pane3 }]);
    });

    it('invokes the given callback when the container is destroyed', () => {
      const container = new PaneContainer(params);
      const events = [];
      container.onDidDestroyPane(event => {
        expect(container.getPanes().includes(event.pane)).toBe(false);
        events.push(event);
      });

      const pane1 = container.getActivePane();
      const pane2 = pane1.splitRight();
      const pane3 = pane2.splitDown();

      container.destroy();

      expect(events).toEqual([
        { pane: pane1 },
        { pane: pane2 },
        { pane: pane3 }
      ]);
    });
  });

  describe('::onWillDestroyPaneItem() and ::onDidDestroyPaneItem', () => {
    it('invokes the given callbacks when an item will be destroyed on any pane', async () => {
      const container = new PaneContainer(params);
      const pane1 = container.getRoot();
      const item1 = {};
      const item2 = {};
      const item3 = {};

      pane1.addItem(item1);
      const events = [];
      container.onWillDestroyPaneItem(event => events.push(['will', event]));
      container.onDidDestroyPaneItem(event => events.push(['did', event]));
      const pane2 = pane1.splitRight({ items: [item2, item3] });

      await pane1.destroyItem(item1);
      await pane2.destroyItem(item3);
      await pane2.destroyItem(item2);

      expect(events).toEqual([
        ['will', { item: item1, pane: pane1, index: 0 }],
        ['did', { item: item1, pane: pane1, index: 0 }],
        ['will', { item: item3, pane: pane2, index: 1 }],
        ['did', { item: item3, pane: pane2, index: 1 }],
        ['will', { item: item2, pane: pane2, index: 0 }],
        ['did', { item: item2, pane: pane2, index: 0 }]
      ]);
    });
  });

  describe('::saveAll()', () =>
    it('saves all modified pane items', async () => {
      const container = new PaneContainer(params);
      const pane1 = container.getRoot();
      pane1.splitRight();

      const item1 = {
        saved: false,
        getURI() {
          return '';
        },
        isModified() {
          return true;
        },
        save() {
          this.saved = true;
        }
      };
      const item2 = {
        saved: false,
        getURI() {
          return '';
        },
        isModified() {
          return false;
        },
        save() {
          this.saved = true;
        }
      };
      const item3 = {
        saved: false,
        getURI() {
          return '';
        },
        isModified() {
          return true;
        },
        save() {
          this.saved = true;
        }
      };

      pane1.addItem(item1);
      pane1.addItem(item2);
      pane1.addItem(item3);

      container.saveAll();

      expect(item1.saved).toBe(true);
      expect(item2.saved).toBe(false);
      expect(item3.saved).toBe(true);
    }));

  describe('::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)', () => {
    let container, pane1, pane2, item1;

    beforeEach(() => {
      class TestItem {
        constructor(id) {
          this.id = id;
        }
        copy() {
          return new TestItem(this.id);
        }
      }

      container = new PaneContainer(params);
      pane1 = container.getRoot();
      item1 = new TestItem('1');
      pane2 = pane1.splitRight({ items: [item1] });
    });

    describe('::::moveActiveItemToPane(destPane)', () =>
      it('moves active item to given pane and focuses it', () => {
        container.moveActiveItemToPane(pane1);
        expect(pane1.getActiveItem()).toBe(item1);
      }));

    describe('::::copyActiveItemToPane(destPane)', () =>
      it('copies active item to given pane and focuses it', () => {
        container.copyActiveItemToPane(pane1);
        expect(container.paneForItem(item1)).toBe(pane2);
        expect(pane1.getActiveItem().id).toBe(item1.id);
      }));
  });
});
