import * as gu from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
import {getCollapsedSection, openCollapsedSectionMenu} from 'test/nbrowser/ViewLayoutUtils';
import {assert, driver, Key, WebElement, WebElementPromise} from 'mocha-webdriver';
import {arrayRepeat} from 'app/plugin/gutil';
import {addStatic, serveSomething} from 'test/server/customUtil';
import {AccessLevel} from 'app/common/CustomWidget';

const GAP = 16;     // Distance between buttons representing collapsed widgets.

describe("ViewLayoutCollapse", function() {
  this.timeout('50s');
  const cleanup = setupTestSuite();
  gu.bigScreen();
  let session: gu.Session;

  before(async () => {
    session = await gu.session().login();
    await session.tempNewDoc(cleanup);
  });

  it('fix:copies collapsed sections properly', async function() {
    // When one of 2 widget was collapsed, the resulting widget can become a root section. Then,
    // when a page was duplicated, the layout was duplicated incorrectly (with wrong collapsed
    // section). This resulted in a bug, when the root section was deleted, as it was the last
    // section in the saved layout, but not the last section on the visible layout.

    // Add new page with new table.
    await gu.addNewPage('Table', 'New Table', {
      tableName: 'Broken'
    });

    await gu.renameActiveSection('Collapsed');

    // Add section here (with the same table).
    await gu.addNewSection('Table', 'Broken');

    // Rename it so that it is easier to find.
    await gu.renameActiveSection('NotCollapsed');

    // Now store the layout, by amending it (so move the collapsed widget below).
    const {height} = await gu.getSection('NotCollapsed').getRect();
    await dragMain('Collapsed');
    await move(gu.getSection('NotCollapsed'), { x: 50, y: height / 2 });
    await driver.sleep(300);
    await move(gu.getSection('NotCollapsed'), { x: 100, y: height / 2 });
    await driver.sleep(300);
    await driver.withActions(actions => actions.release());
    // Wait for the debounced save.
    await driver.sleep(1500);
    await gu.waitForServer();

    // Now collapse it.
    await collapseByMenu('Collapsed');

    // Now duplicate the page.
    await gu.duplicatePage('Broken', 'Broken2');

    // Now on this page we saw two uncollapsed sections (make sure this is not the case).
    assert.deepEqual(await gu.getSectionTitles(), ['NotCollapsed']);
  });

  it('fix:can delete root section', async function() {
    // But even if the layout spec was corrupted, we still should be able to delete the root section
    // when replacing it with new one.

    // Break the spec.
    const specJson: string = await driver.executeScript(
      'return gristDocPageModel.gristDoc.get().docModel.views.rowModels[3].layoutSpec()'
    );

    // To break the spec, we will replace id of the collapsed section, then viewLayout will try to fix it,
    // by rendering the missing section without patching the layout spec (which is good, because this could
    // happen on readonly doc or a snapshot).
    const spec = JSON.parse(specJson);
    spec.collapsed[0].leaf = -10;

    await driver.executeScript(
      `gristDocPageModel.gristDoc.get().docModel.views.rowModels[3].layoutSpec.setAndSave('${JSON.stringify(spec)}')`
    );

    await gu.waitForServer();

    // We now should see two sections.
    assert.deepEqual(await gu.getSectionTitles(), ['NotCollapsed', 'Collapsed']);

    // And we should be able to delete the top one (NotCollapsed).
    await gu.openSectionMenu('viewLayout', 'NotCollapsed');
    await driver.findContent('.test-cmd-name', 'Delete widget').click();
    await gu.waitForServer();

    await gu.checkForErrors();
  });

  it('fix: custom widget should restart when added back after collapsing', async function() {
    await session.tempDoc(cleanup, 'Investment Research.grist');
    await gu.openPage("Overview");

    const revert = await gu.begin();

    // Add custom section.
    await gu.addNewPage('Table', 'Companies');
    await gu.addNewSection('Custom', 'Companies', { selectBy: 'COMPANIES'});

    // Serve custom widget.
    const widgetServer = await serveSomething(app => {
      addStatic(app);
    });
    cleanup.addAfterAll(widgetServer.shutdown);
    await gu.openWidgetPanel();
    await gu.setWidgetUrl(widgetServer.url + '/probe/index.html');
    await gu.widgetAccess(AccessLevel.full);

    // Collapse it.
    await collapseByMenu('COMPANIES Custom');

    // Now restore its position.
    await addToMainByMenu('COMPANIES Custom');

    // Collapsed widget used to lost connection with Grist as it was disposed to early.
    // Make sure that this widget can call the API.
    await gu.doInIframe(async () => {
      await gu.waitToPass(async () => {
        assert.equal(await driver.find('#output').getText(),
          `["Companies","Investments","Companies_summary_category_code","Investments_summary_funded_year",` +
          `"Investments_summary_Company_category_code_funded_year","Investments_summary_Company_category_code"]`
        );
      });
    });


    // Make sure we don't have an error.
    await gu.checkForErrors();
    await revert();
  });

  it('fix: custom widget should not throw errors when collapsed', async function() {
    const revert = await gu.begin();

    // Add custom section.
    await gu.addNewPage('Table', 'Companies');
    await gu.addNewSection('Custom', 'Companies', { selectBy: 'COMPANIES'});

    // Serve custom widget.
    const widgetServer = await serveSomething(app => {
      addStatic(app);
    });
    cleanup.addAfterAll(widgetServer.shutdown);
    await gu.openWidgetPanel();
    await gu.setWidgetUrl(widgetServer.url + '/probe/index.html');
    await gu.widgetAccess(AccessLevel.full);

    // Collapse it.
    await collapseByMenu('COMPANIES Custom');

    // Change cursor position in the active section.
    await gu.getCell(2, 4).click();

    // Put custom section in popup.
    await openCollapsedSection('COMPANIES Custom');

    // Close it by pressing escape.
    await gu.sendKeys(Key.ESCAPE);

    // Change cursor once again.
    await gu.getCell(2, 5).click();

    // Make sure we don't have an error (there was a bug here).
    await gu.checkForErrors();

    await revert();
  });

  it('fix: should resize other sections correctly when maximized and linked', async function() {
    const revert = await gu.begin();
    // If there are two sections linked, but one is collapsed, and user is changing the row
    // in the popup of the maximized section, the other section should resize correctly.

    // Add two linked tables.
    await gu.addNewTable('Table1');
    await gu.addNewSection('Table', 'New Table');

    await gu.toggleSidePanel('right', 'open');

    // Set A in Table2 to be linked in Table1, by ref column
    await gu.openColumnMenu('A', 'Column Options');

    // Change it to the Ref column of TABLE1
    await gu.setType('Reference');
    await gu.setRefTable('Table1');
    await gu.setRefShowColumn('A');

    // Select it by Table1.
    await gu.selectBy('TABLE1');

    // Now add 2 records with 'White' and 'Black' in Table1
    await gu.sendActions([
      ['BulkAddRecord', 'Table1', arrayRepeat(2, null), { A: ['White', 'Black'] }],
      // And 30 records in Table2 that are connected to White.
      ['BulkAddRecord', 'Table2', arrayRepeat(30, null), { A: arrayRepeat(30, 1) }],
      // And 30 records in Table2 that are connected to Black.
      ['BulkAddRecord', 'Table2', arrayRepeat(30, null), { A: arrayRepeat(30, 2) }],
    ]);

    // Now select White in Table1.
    await gu.getCell('A', 1, 'Table1').click();

    // Now expand Table1.
    await gu.expandSection();

    // Change to black.
    await gu.getCell('A', 2, 'Table1').click();

    // Close popup by sending ESCAPE
    await gu.sendKeys(Key.ESCAPE);

    // Make sure we see 30 records in Table2.
    const count = await driver.executeScript(`
      const section = Array.from(document.querySelectorAll('.test-widget-title-text'))
                           .find(e => e.textContent === 'TABLE2')
                           .closest('.viewsection_content');
      return Array.from(section.querySelectorAll('.gridview_data_row_num')).length;
    `);

    assert.equal(count, 30 + 1);

    await revert();
  });


  it('fix: should support searching', async function() {
    // Collapse Companies section (a first one).
    await collapseByMenu(COMPANIES);

    // Clear any saved position state.
    await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();');

    // Refresh.
    await driver.navigate().refresh();
    await gu.waitForDocToLoad();

    // Here we had a bug that the hidden section was active, and the search was not working as it was
    // starting with the hidden section.

    // Now search (for something in the INVESTMENTS section)
    await gu.search('2006');
    await gu.closeSearch();

    // Make sure we don't have an error.
    await gu.checkForErrors();

    assert.equal(await gu.getActiveSectionTitle(), INVESTMENTS);
    // Make sure we are in 1column 9th row.
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 9, col: 0});

    // Hide companies chart, and search for mobile (should show no results).
    await collapseByMenu(COMPANIES_CHART);
    await gu.search('mobile');
    await gu.hasNoResult();
    await gu.closeSearch();

    // Open companies in the popup.
    await openCollapsedSection(COMPANIES);
    // Search for 2006, there will be no results.
    await gu.search('2006');
    await gu.hasNoResult();
    // Now search for web.
    await gu.closeSearch();
    await gu.search('web');
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 5, col: 0});

    // Recreate document (can't undo).
    await session.tempDoc(cleanup, 'Investment Research.grist');
  });


  it('fix: should not dispose the instance when drag is cancelled', async function() {
    const revert = await gu.begin();

    // Collapse a section.
    await collapseByMenu(INVESTMENTS);

    // Drag it and then cancel.
    await dragCollapsed(INVESTMENTS);
    const logo = driver.find('.test-dm-logo');
    await move(logo, {y:  0});
    await move(logo, {y:  -1});
    // Drop it here.
    await driver.withActions(actions => actions.release());

    // Now open it in the full view.
    await openCollapsedSection(INVESTMENTS);

    // And make sure we can move cursor.
    await gu.getCell(1, 1).click();
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 1});
    await gu.getCell(1, 2).click();
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 1});

    // Change its type, and check that it works.
    await gu.changeWidget('Card List');
    // Undo it.
    await gu.undo();
    await gu.getCell(1, 3).click();
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 3, col: 1});

    await gu.sendKeys(Key.ESCAPE);

    // Move it back
    await dragCollapsed(INVESTMENTS);

    // Move back and drop.
    await gu.getSection(COMPANIES_CHART).getRect();
    await move(getDragElement(COMPANIES_CHART), {x : 50});
    await driver.sleep(100);
    await move(getDragElement(COMPANIES_CHART), {x : 100});
    await driver.sleep(100);
    await move(getDragElement(COMPANIES_CHART), {x : 200});
    await gu.waitToPass(async () => {
      assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 1);
    }, 1000);

    await driver.withActions(actions => actions.release());
    await driver.sleep(600);

    // And make sure we can move cursor.
    await gu.getCell(1, 1).click();
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 1});
    await gu.getCell(1, 2).click();
    assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 1});

    await waitForSave();
    await revert();
  });


  it('fix: should work when the page is refreshed', async function() {
    const revert = await gu.begin();

    await gu.openPage("Companies");
    await gu.selectSectionByTitle("Companies");
    // Go to second row.
    await gu.getCell(0, 2).click();

    // Make sure we see correct company card.
    assert.equal(await gu.getCardCell('name', 'COMPANIES Card').getText(), '#NAME?');

    // Hide first section.
    await collapseByMenu("Companies");
    await waitForSave();

    // Refresh the page.
    await driver.navigate().refresh();
    await gu.waitForDocToLoad();

    // Make sure card is still at the correct row.
    await gu.waitToPass(async () => {
      assert.equal(await gu.getCardCell('name', 'COMPANIES Card').getText(), '#NAME?');
    });

    await addToMainByMenu("Companies");
    await revert();
  });

  it('fix: should support anchor links', async function() {
    const revert = await gu.begin();

    // Open 42Floors in Companies section.
    assert.equal(await gu.getActiveSectionTitle(), "COMPANIES");
    await gu.getCell('Link', 11).click();
    assert.equal(await gu.getActiveCell().getText(), '42Floors');

    // Open 12 row (Alex Bresler, angel).
    await gu.getCell('funding_round_type', 12, 'Investments').click();
    assert.equal(await gu.getActiveCell().getText(), 'angel');

    // Copy anchor link.
    const link = await gu.getAnchor();

    // Collapse first section.
    await collapseByMenu("COMPANIES");

    // Clear any saved position state.
    await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();');

    // Navigate to the home screen.
    await gu.loadDocMenu('/o/docs');

    // Now go to the anchor.
    await driver.get(link);
    await gu.waitForAnchor();

    const cursor = await gu.getCursorPosition();
    assert.equal(cursor.rowNum, 12);
    assert.equal(cursor.col, 1);
    assert.equal(await gu.getActiveCell().getText(), 'angel');
    assert.equal(await gu.getActiveSectionTitle(), 'INVESTMENTS');
    assert.match(await driver.getCurrentUrl(), /\/o\/docs\/[^/]+\/Investment-Research\/p\/1$/);
    await revert();
  });

  it("should not autoexpand the tray on a page with a single widget", async () => {
    await gu.openPage("Investments");
    assert.equal((await driver.findAll(".viewsection_content")).length, 1);

    // Start drag the main section.
    await dragMain("INVESTMENTS");

    // Move it over the logo, so that the tray thinks that it should expand.
    const logo = driver.find('.test-dm-logo');
    await move(logo, {y:  0});
    await move(logo, {y:  -1});
    await driver.sleep(100);

    // Make sure the tray was not tricked into expanding itself.
    assert.isFalse(await layoutTray().isDisplayed());
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); // No empty box

    // Drop it on the button, it should go back to where it was.
    await driver.withActions(actions => actions.release());
  });

  it("should autoexpand the tray", async () => {
    await gu.openPage("Overview");

    // Get one of the sections and start dragging it.
    await dragMain(COMPANIES_CHART);

    // The tray should not be expanded.
    assert.isFalse(await layoutTray().isDisplayed());

    const logo = driver.find('.test-dm-logo');
    // Now move it to the top, so that tray should be expanded.
    await move(logo, {y:  0});
    await driver.sleep(100);

    // Now the tray is visible
    assert.isTrue(await layoutTray().isDisplayed());
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 1); // One empty box
    assert.isTrue(await layoutEditor().matches('[class*=-is-active]')); // Is active
    assert.isFalse(await layoutEditor().matches('[class*=-is-target]')); // Is not a target

    // Drop it on the button, it should go back to where it was.
    await driver.withActions(actions => actions.release());

    // The tray should not be expanded.
    assert.isFalse(await layoutTray().isDisplayed());

    await gu.checkForErrors();
  });

  it("should drag onto main area", async () => {
    const revert = await gu.begin();
    await collapseByMenu(COMPANIES);
    await collapseByMenu(INVESTMENTS);

    await dragCollapsed(COMPANIES);
    const chartCords = await gu.getSection(COMPANIES_CHART).getRect();
    await move(getDragElement(COMPANIES_CHART));
    await driver.sleep(100);
    await move(getDragElement(COMPANIES_CHART), {x : 10});
    await driver.sleep(300);

    // We should have a drop target.
    const dropTarget = await driver.find(".layout_editor_drop_target.layout_hover");
    const dCords = await dropTarget.getRect();
    // It should be more or less on the left of the chart.
    assertDistance(dCords.x, chartCords.x, 20);
    assertDistance(dCords.y, chartCords.y, 20);

    // Move away from the drop target.
    const addButton = driver.find('.test-dp-add-new');
    await move(addButton);
    await driver.sleep(300);

    // Drop target should be gone.
    assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 0);

    // Move back and drop.
    await move(getDragElement(COMPANIES_CHART));
    await driver.sleep(100);
    // Split the movement into two parts, to make sure layout sees the mouse move.
    await move(getDragElement(COMPANIES_CHART), {x : 10});
    await driver.sleep(200);
    assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 1);
    await driver.withActions(actions => actions.release());
    await driver.sleep(600); // This animation can be longer.

    // Make sure it was dropped.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 1); // Only one collapsed box.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); // No empty box.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0); // No target box.
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS]); // Only investments is collapsed.
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, COMPANIES_CHART, INVESTMENTS_CHART]);
    // Check that it was dropped on the left top side.
    const companiesCords = await gu.getSection(COMPANIES).getRect();
    assertDistance(companiesCords.x, chartCords.x, 20);
    assertDistance(companiesCords.y, chartCords.y, 20);
    // It should be half as tall as the main layout.
    const root = await driver.find(".layout_root").getRect();
    assertDistance(companiesCords.height, root.height / 2, 30);
    // And almost half as wide.
    assertDistance(companiesCords.width, root.width / 2, 30);

    // Now move it back to the tray. But first collapse another section (so we can test inbetween target).
    await collapseByMenu(COMPANIES_CHART);
    await dragMain(COMPANIES);

    // Try to move it as the first element.
    const firstLeafSize = await firstLeaf().getRect();
    await move(firstLeaf(), { x: -firstLeafSize.width / 2 });
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1);
    // Make sure that the target is in right place.
    let target = await layoutTray().find(".test-layoutTray-target-box").getRect();
    assertDistance(target.x, firstLeafSize.x, 10);

    // Now as the second element.
    await move(firstLeaf(), { x: firstLeafSize.width / 2 + GAP });
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1);
    target = await layoutTray().find(".test-layoutTray-target-box").getRect();
    assertDistance(target.x, firstLeafSize.x + firstLeafSize.width + GAP, 10);

    // Move away to make sure the target is gone.
    await move(addButton);
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0);

    // Move back and drop.
    await move(firstLeaf(), { x: firstLeafSize.width / 2 + GAP });
    await driver.sleep(300);
    await driver.withActions(actions => actions.release());
    await driver.sleep(600);

    // Make sure it was dropped.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 3);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0);

    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES, COMPANIES_CHART]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS_CHART]);

    await waitForSave(); // Layout save is debounced 1s.

    // Test couple of undo steps.
    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES_CHART]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, COMPANIES_CHART, INVESTMENTS_CHART]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, INVESTMENTS]);

    await gu.redo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]);

    await revert();
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, COMPANIES, INVESTMENTS_CHART, INVESTMENTS]);
    await gu.checkForErrors();
  });

  it("should reorder collapsed sections", async () => {
    const revert = await gu.begin();
    await collapseByMenu(COMPANIES);
    await collapseByMenu(INVESTMENTS);
    await collapseByMenu(COMPANIES_CHART);

    await dragCollapsed(COMPANIES);

    // We should see the empty box in the layout.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 1);
    // The section is actually removed from the layout tray.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 2);
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES_CHART]);

    // Layout should be active and accepting.
    assert.isTrue(await layoutEditor().matches('[class*=-is-active]'));
    assert.isTrue(await layoutEditor().matches('[class*=-is-target]'));

    // Move mouse somewhere else, layout should not by highlighted.
    const addButton = driver.find('.test-dp-add-new');
    await move(addButton);
    assert.isTrue(await layoutEditor().matches('[class*=-is-active]'));
    assert.isFalse(await layoutEditor().matches('[class*=-is-target]'));

    // Move to the first leaf, and wait for the target to show up.
    const first = await firstLeaf().getRect();
    await move(firstLeaf(), {x : -first.width / 2});
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1);
    // Make sure that the target is in right place.
    let target = await layoutTray().find(".test-layoutTray-target-box").getRect();
    assert.isBelow(Math.abs(target.x - first.x), 10);
    assert.isBelow(Math.abs(target.y - first.y), 10);
    assert.isBelow(Math.abs(target.height - first.height), 10);

    // Move away and make sure the target is gone.
    await move(addButton);
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0);

    // Move between first and second leaf.
    await move(firstLeaf(), {x : first.width / 2 + GAP});
    await driver.sleep(300);
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1);
    target = await layoutTray().find(".test-layoutTray-target-box").getRect();
    assert.isBelow(Math.abs(target.height - first.height), 2);
    assert.isBelow(Math.abs(target.y - first.y), 2);
    // Should be between first and second leaf.
    assert.isBelow(Math.abs(target.x - (first.x + first.width + GAP)), 10);

    // Drop here.
    await driver.withActions(actions => actions.release());
    await waitForSave(); // Wait for layout to be saved.
    // Target is gone.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0);
    // And we have 3 sections in the layout.
    assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 3);
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES, COMPANIES_CHART]);

    // Undo.
    await gu.undo();
    // Order should be restored.
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS, COMPANIES_CHART]);

    await revert();
    await gu.checkForErrors();
  });

  it("should collapse sections and expand using menu", async () => {
    await collapseByMenu(COMPANIES_CHART);
    await gu.checkForErrors();

    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]);
    // Make sure that other sections are not collapsed.
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]);

    await collapseByMenu(INVESTMENTS_CHART);
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]);

    await collapseByMenu(COMPANIES);
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    // The last section is INVESTMENTS, which can't be collapsed.
    await gu.openSectionMenu('viewLayout', INVESTMENTS);
    assert.equal(await driver.find('.test-section-collapse').matches('[class*=disabled]'), true);
    await driver.sendKeys(Key.ESCAPE);

    // Now expand them one by one and test.
    await addToMainByMenu(COMPANIES_CHART);
    await gu.checkForErrors();

    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART]);

    await addToMainByMenu(INVESTMENTS_CHART);
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART]);
    await gu.checkForErrors();

    await addToMainByMenu(COMPANIES);
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]);
    await gu.checkForErrors();

    // Now revert everything using undo but test each step.
    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART]);
    await gu.checkForErrors();

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART]);
    await gu.checkForErrors();

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);
    await gu.checkForErrors();

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]);
    await gu.checkForErrors();

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]);
    await gu.checkForErrors();

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, COMPANIES, INVESTMENTS_CHART, INVESTMENTS]);
    await gu.checkForErrors();
  });

  it("should remove sections from collapsed tray", async () => {
    const revert = await gu.begin();
    // Collapse everything we can.
    await collapseByMenu(COMPANIES_CHART);
    await collapseByMenu(INVESTMENTS_CHART);
    await collapseByMenu(COMPANIES);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    // Now remove them using menu.
    await removeMiniSection(COMPANIES_CHART);
    await gu.checkForErrors();

    // Check that the section is removed from the collapsed tray.
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]);
    // Make sure it is stays removed when we move to the other page.
    await gu.openPage("Investments");
    // Go back.
    await gu.openPage("Overview");
    await gu.checkForErrors();

    // Test if we see everything as it was.
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]);
    // Make sure that visible sections are not affected.
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    // Remove the other sections.
    await removeMiniSection(INVESTMENTS_CHART);
    await removeMiniSection(COMPANIES);
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    // Now revert everything using undo but test each step.
    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]);
    assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]);

    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]);
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]);

    // Ok, we are good, revert back to the original state.
    await revert();
    await gu.checkForErrors();
  });

  it('should prompt when last section is removed from tray', async () => {
    const revert = await gu.begin();

    // Add brand new table and collapse it.
    await gu.addNewSection('Table', 'New Table', {tableName: 'ToCollapse'});
    await collapseByMenu('ToCollapse');

    // Now try to remove it, we should see prompt.
    await openCollapsedSectionMenu('ToCollapse');
    await driver.find('.test-section-delete').click();
    assert.match(
      await driver.find('.test-modal-title').getText(),
      /Table ToCollapse will no longer be visible/
    );

    // Select first option, to delete both table and widget.
    await driver.find('.test-option-deleteDataAndWidget').click();
    await driver.find('.test-modal-confirm').click();
    await gu.waitForServer();

    // Make sure it is removed.
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments']);
    await gu.sendKeys(Key.ESCAPE);

    // Single undo should add it back.
    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), ['TOCOLLAPSE']);
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments', 'ToCollapse']);

    // Now do the same but, keep data.
    await openCollapsedSectionMenu('ToCollapse');
    await driver.find('.test-section-delete').click();
    await driver.findWait('.test-modal-dialog', 100);
    await driver.find('.test-option-deleteOnlyWidget').click();
    await driver.find('.test-modal-confirm').click();
    await gu.waitForServer();

    // Make sure it is removed.
    assert.deepEqual(await collapsedSectionTitles(), []);
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments', 'ToCollapse']);

    // Test single undo.
    await gu.undo();
    assert.deepEqual(await collapsedSectionTitles(), ['TOCOLLAPSE']);
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments', 'ToCollapse']);

    // Uncollapse it, and do the same with normal section.
    await addToMainByMenu('ToCollapse');

    // Now try to remove it, we should see prompt.
    assert.include(
      await driver.findAll('.test-viewsection-title', e => e.getText()), 'TOCOLLAPSE');

    await gu.openSectionMenu('viewLayout', 'ToCollapse');
    await driver.find('.test-section-delete').click();
    await driver.findWait('.test-modal-dialog', 100);
    await driver.find('.test-option-deleteOnlyWidget').click();
    await driver.find('.test-modal-confirm').click();
    await gu.waitForServer();
    assert.notInclude(
      await driver.findAll('.test-viewsection-title', e => e.getText()), 'TOCOLLAPSE');
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments', 'ToCollapse']);
    // Test undo.
    await gu.undo();
    assert.include(
      await driver.findAll('.test-viewsection-title', e => e.getText()), 'TOCOLLAPSE');

    // Do the same but delete data and widget.
    await gu.openSectionMenu('viewLayout', 'ToCollapse');
    await driver.find('.test-section-delete').click();
    await driver.findWait('.test-modal-dialog', 100);
    await driver.find('.test-option-deleteDataAndWidget').click();
    await driver.find('.test-modal-confirm').click();
    await gu.waitForServer();

    // Make sure it is removed.
    assert.notInclude(
      await driver.findAll('.test-viewsection-title', e => e.getText()), 'TOCOLLAPSE');
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments']);

    // Test undo.
    await gu.undo();
    assert.include(
      await driver.findAll('.test-viewsection-title', e => e.getText()), 'TOCOLLAPSE');
    assert.deepEqual(await visibleTables(), ['Companies', 'Investments', 'ToCollapse']);

    await revert();
  });

  it("should switch active section when collapsed", async () => {
    const revert = await gu.begin();
    await gu.selectSectionByTitle(gu.exactMatch(COMPANIES));
    // Make sure we are active.
    assert.equal(await gu.getActiveSectionTitle(), COMPANIES);
    // Collapse it.
    await collapseByMenu(COMPANIES);
    // Make sure it is collapsed.
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]);
    // Make sure that now COMPANIES_CHART is active. (first one).
    assert.equal(await gu.getActiveSectionTitle(), COMPANIES_CHART);
    // Expand COMPANIES.
    await addToMainByMenu(COMPANIES);
    // Make sure that now it is active.
    assert.equal(await gu.getActiveSectionTitle(), COMPANIES);
    await revert();
    await gu.checkForErrors();
  });

  it("should show section on popup when clicked", async () => {
    const revert = await gu.begin();
    await collapseByMenu(COMPANIES);
    await openCollapsedSection(COMPANIES);
    // Make sure it is expanded.
    assert.isTrue(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]"));
    assert.equal(await gu.getActiveSectionTitle(), COMPANIES);
    // Make sure that the panel shows it.
    await gu.toggleSidePanel('right', 'open');
    await driver.find('.test-config-widget').click();
    assert.equal(await driver.find('.test-right-widget-title').value(), COMPANIES);
    // Make sure we see proper items in the menu.
    await gu.openSectionMenu('viewLayout', COMPANIES);
    // Collapse widget menu item is disabled.
    assert.equal(await driver.find('.test-section-collapse').matches('[class*=disabled]'), true);
    // Delete widget is enabled.
    assert.equal(await driver.find('.test-section-delete').matches('[class*=disabled]'), false);
    await driver.sendKeys(Key.ESCAPE);
    // Expand button is not visible
    assert.lengthOf(await driver.findAll(".active_section .test-section-menu-expandSection"), 0);
    // We can rename a section using the popup.
    await gu.renameActiveSection("New name");
    assert.equal(await gu.getActiveSectionTitle(), "New name");
    // Make sure the name is reflected in the collapsed tray.
    await gu.sendKeys(Key.ESCAPE);
    assert.deepEqual(await collapsedSectionTitles(), ["New name"]);
    // Open it back.
    await openCollapsedSection("New name");
    // Rename it back using undo.
    await gu.undo();
    assert.equal(await gu.getActiveSectionTitle(), COMPANIES);
    // Now remove it.
    await gu.openSectionMenu('viewLayout', COMPANIES);
    await driver.find('.test-section-delete').click();
    await gu.waitForServer();
    // Make sure it is closed.
    assert.isFalse(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]"));
    // Make sure it is removed.
    assert.deepEqual(await collapsedSectionTitles(), []);
    // Make sure it didn't reappear on the main area.
    assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, INVESTMENTS]);

    await revert();
    await gu.checkForErrors();
  });

  it("should collapse and expand charts without an error", async () => {
    const revert = await gu.begin();
    await collapseByMenu(INVESTMENTS);
    await dragMain(COMPANIES_CHART);
    const firstRect = await firstLeaf().getRect();
    await move(firstLeaf(), { x: firstRect.width / 2 + GAP });
    await driver.sleep(300);
    await driver.withActions(actions => actions.release());
    await waitForSave(); // Resize is delayed.
    await gu.checkForErrors();
    await revert();
  });

  it("should drop on the empty space", async () => {
    const revert = await gu.begin();
    // Get one of the sections and start dragging it.
    await dragMain(COMPANIES_CHART);
    // Move it over the logo to show the tray.
    const logo = driver.find('.test-dm-logo');
    await move(logo, {y:  0});
    await move(logo, {y:  -20});
    await driver.sleep(100);
    // Now the tray is visible
    assert.isTrue(await layoutTray().isDisplayed());
    // Move it on the empty space just after the empty box
    const emptyBox = await layoutTray().find(".test-layoutTray-empty-box");
    const emptyBoxCords = await emptyBox.getRect();
    await move(emptyBox, {x: emptyBoxCords.width + 100 });
    // Make sure that the empty box is not active.
    assert.isFalse(await emptyBox.matches('[class*=-is-active]'));
    // Drop it here
    await driver.withActions(actions => actions.release());
    await driver.sleep(600); // Wait for animation to finish.
    await waitForSave();
    // The tray should stay expanded.
    assert.isTrue(await layoutTray().isDisplayed());

    // Check that the section was collapsed.
    assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]);
    // And other sections are still there.
    assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]);
    await gu.checkForErrors();
    await revert();
    await gu.checkForErrors();
  });

  it("should clear layout when dropped section is removed", async () => {
    await session.tempNewDoc(cleanup, 'CollapsedBug.grist');
    // Add a new section based on current table.
    await gu.addNewSection('Table', 'Table1');
    // It will have id 3 (1 is raw, 2 is visible).
    // Collapse it.
    await gu.renameActiveSection('ToDelete');
    await collapseByMenu('ToDelete');
    // Remove it from the tray.
    await openCollapsedSectionMenu('ToDelete');
    await driver.find('.test-section-delete').click();
    await gu.waitForServer();
    await waitForSave();
    // Now add another one, it will have the same id (3) and it used to be collapsed when added
    // as the layout was not cleared.
    await gu.addNewSection('Table', 'Table1');
    // Make sure it is expanded.
    assert.deepEqual(await mainSectionTitles(), ['TABLE1', 'TABLE1']);
    assert.deepEqual(await collapsedSectionTitles(), []);
  });
});

async function addToMainByMenu(section: string) {
  await openCollapsedSectionMenu(section);
  await driver.find('.test-section-expand').click();
  await gu.waitForServer();
  await gu.checkForErrors();
}

async function dragCollapsed(section: string) {
  const handle = getCollapsedSection(section).find('.draggable-handle');
  await driver.withActions((actions) => actions
    .move({origin: handle})
    .press());
  await move(handle, {x : 10, y: 10});
  return handle;
}

async function dragMain(section: string) {
  const handle = gu.getSection(section).find('.viewsection_drag_indicator');
  await driver.withActions((actions) => actions
    .move({origin: handle}));
  await driver.withActions((actions) => actions
    .move({origin: handle, x : 1}) // This is needed to show the drag element.
    .press());
  await move(handle, {x : 10, y: 10});
  return handle;
}

async function openCollapsedSection(section: string) {
  await getCollapsedSection(section).find('.draggable-handle').click();
}

async function removeMiniSection(section: string) {
  await openCollapsedSectionMenu(section);
  await driver.find('.test-section-delete').click();
  await gu.waitForServer();
  await gu.checkForErrors();
}

async function collapseByMenu(section: string) {
  await gu.openSectionMenu('viewLayout', section);
  await driver.find('.test-section-collapse').click();
  await gu.waitForServer();
  await gu.checkForErrors();
}

// Returns the titles of all collapsed sections.
async function collapsedSectionTitles() {
  return await layoutTray().findAll('.test-layoutTray-leaf-box .test-collapsed-section-title', e => e.getText());
}

// Returns titles of all sections in the view layout.
async function mainSectionTitles() {
  return await driver.findAll('.layout_root .test-viewsection-title', e => e.getText());
}

async function move(element: WebElementPromise|WebElement, offset: {x?: number, y?: number} = {x: 0, y: 0}) {
  // With current version of webdriver, a fractional values will get ignored, so round to nearest.
  if (offset.x) { offset.x = Math.round(offset.x); }
  if (offset.y) { offset.y = Math.round(offset.y); }
  await driver.withActions(actions => actions.move({origin: element, ...offset}));
}


function getDragElement(section: string) {
  return gu.getSection(section).find('.viewsection_drag_indicator');
}

function layoutTray() {
  return driver.find(".test-layoutTray-layout");
}

function firstLeaf() {
  return layoutTray().find(".test-layoutTray-leaf-box");
}

function layoutEditor() {
  return driver.find(".test-layoutTray-editor");
}

const COMPANIES_CHART = 'COMPANIES [by category_code] Chart';
const INVESTMENTS_CHART = 'INVESTMENTS [by funded_year] Chart';
const COMPANIES = 'COMPANIES [by category_code]';
const INVESTMENTS = 'INVESTMENTS [by funded_year]';

function assertDistance(left: number, right: number, max: number) {
  return assert.isBelow(Math.abs(left - right), max);
}

async function waitForSave() {
  await gu.waitToPass(async () => {
    const pending = await driver.findAll(".test-viewLayout-save-pending");
    assert.isTrue(pending.length === 0);
    await gu.waitForServer();
  }, 3000);
}

async function visibleTables() {
  await driver.findWait('.test-dp-add-new', 2000).doClick();
  await driver.find('.test-dp-add-new-page').doClick();
  const titles = await driver.findAll('.test-wselect-table', e => e.getText());
  await gu.sendKeys(Key.ESCAPE);
  return titles.map(x => x.trim()).filter(Boolean).filter(x => x !== 'New Table');
}
