import * as gu from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
import {assert, driver, Key} from 'mocha-webdriver';

describe('LinkingBidirectional', function() {
  this.timeout('60s');

  const cleanup = setupTestSuite({team: true});
  let session: gu.Session;

  afterEach(() => gu.checkForErrors());

  before(async function() {
    session = await gu.session().login();
    await session.tempDoc(cleanup, 'Class Enrollment.grist');
  });

  it('should update cursor when sections cursor-linked together', async function() {
    // Setup new page with "Classes1"
    await gu.addNewPage('Table', 'Classes', {});
    await gu.renameActiveSection('Classes1');

    // Set its cursor to row 2
    await gu.getCell('Semester', 2, 'Classes1').click();

    // Add Classes2 & 3
    await gu.addNewSection('Table', 'Classes', { selectBy: 'Classes1' });
    await gu.renameActiveSection('Classes2');
    await gu.addNewSection('Table', 'Classes', { selectBy: 'Classes2' });
    await gu.renameActiveSection('Classes3');

    // Make sure that linking put them all on row 2
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2,    cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,    cursor: {rowNum: 2, col: 0}});
  });

  it('should propagate cursor-links downstream', async function() {
    // Cursor link from upstream should drive a given section, but we can still move the downstream cursor
    // To other positions. The downstream linking should use whichever of its ancestors was most recently clicked

    // Set cursors: Sec1 on row 1, Sec2 on row 2
    await gu.getCell('Semester', 1, 'Classes1').click();
    await gu.getCell('Semester', 2, 'Classes2').click();

    // Sec1 should be on row 1. Sec2 should be on 2 even though link is telling it to be on 1. Sec3 should be on 2
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1,    cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,    cursor: {rowNum: 2, col: 0}});

    // click on Sec3 row 3
    await gu.getCell('Semester', 3, 'Classes3').click();

    // Cursors should be on 1, 2, 3, with arrows lagging 1 step behind
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1,    cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,    cursor: {rowNum: 3, col: 0}});

    // When clicking on oldest ancestor, it should now take priority over all downstream cursors again
    // Click on S1 row 4
    await gu.getCell('Semester', 4, 'Classes1').click();
    // assert that all are on 4
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 4, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 4,    cursor: {rowNum: 4, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 4,    cursor: {rowNum: 4, col: 0}});
  });

  it('should work correctly when linked into a cycle', async function() {
    // Link them all into a cycle.
    await gu.selectBy('Classes3');

    // Now, any section should drive all the other sections

    // Click on row 1 in Sec1
    await gu.getCell('Semester', 1, 'Classes1').click();
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});

    // Click on row 2 in Sec2
    await gu.getCell('Semester', 2, 'Classes2').click();
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});

    // Click on row 3 in Sec3
    await gu.getCell('Semester', 3, 'Classes3').click();
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,   cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 3,   cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,   cursor: {rowNum: 3, col: 0}});
  });

  it('should keep position after refresh', async function() {
    // Nothing should change after a refresh
    await gu.reloadDoc();

    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
  });

  it('should update linking when filters change', async function() {
    // Let's filter out the current row ("Spring 2019") from Classes2
    await gu.selectSectionByTitle('Classes2');
    await gu.sortAndFilter()
      .then(x => x.addColumn())
      .then(x => x.clickColumn('Semester'))
      .then(x => x.close())
      .then(x => x.click());

    // Open the pinned filter, and filter out the date.
    await gu.openPinnedFilter('Semester')
      .then(x => x.toggleValue('Spring 2019'))
      .then(x => x.close());

    // Classes2 got its cursor moved when we filtered out its current row, so all sections should now
    // have moved to row 1
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
  });

  it('should propagate cursor linking even through a filtered-out section', async function() {
    // Row 3 (from Classes1 and Classes3) is no longer present in Classes2
    // Make sure it can still get the value through the link from Classes1 -> 2 -> 3

    // Click on row 3 in Sec1
    await gu.getCell('Semester', 3, 'Classes1').click();

    // Classes1 and 3 should be on row3
    // Classes2 should still be on row 1 (from the last test case), and should have no arrow (since it's desynced)
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
  });

  it('should keep position after refresh when section is filtered out', async function() {
    // Save the filter in Classes2
    await gu.selectSectionByTitle('Classes2');
    await gu.sortAndFilter()
      .then(x => x.save());

    // Go to Classes1, and refresh (its curser will be restored).
    await gu.selectSectionByTitle('Classes1');
    await gu.reloadDoc();

    // Nothing should change after a refresh
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
  });

  it('should navigate to correct cells and sections', async function() {
    await gu.getCell('Semester', 3, 'Classes1').click();
    const anchor = await gu.getAnchor();
    await gu.wipeToasts();

    await gu.onNewTab(async () => {
      await driver.get(anchor);
      await gu.waitForDocToLoad();

      assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
      assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
      assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    });

    await gu.getCell('Semester', 3, 'Classes3').click();
    const anchor2 = await gu.getAnchor();
    await gu.wipeToasts();

    await gu.onNewTab(async () => {
      await driver.get(anchor2);
      await gu.waitForDocToLoad();

      assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
      assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
      assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3,    cursor: {rowNum: 3, col: 0}});
    });
  });

  it("should update cursor-linking when clicking on a cell, even if the position doesn't change", async function() {
    // Classes2 should still be on row 1, but the other 2 sections have their cursors on row 3
    // If we click on Classes2, even if the cursor position doesn't change, we should still record
    // Classes2 as now being the most recently-clicked section and have it drive the linking of the other 2 sections
    // (i.e. they should change to match it)

    // Click on row 1 in Classes2 (it's got filtered-out-rows, and so is desynced from the other 2)
    await gu.getCell('Semester', 1, 'Classes2').click();

    // All sections should now jump to join it
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1,   cursor: {rowNum: 1, col: 0}});
  });

  it('should update cursor linking correctly when the selected row is deleted', async function() {
    // When deleting a row, the current section moves down 1 row (preserving the cursorpos), but other
    // sections with the same table jump to row 1

    // If we're cursor-linked, make sure that doesn't cause a desync.

    // Click on row 2 in Classes1 and delete it
    await gu.getCell('Semester', 2, 'Classes1').click();
    await gu.removeRow(2);

    // Classes1 and Classes3 should both have moved to the next row (previously rowNum3, but now is rowNum 2)
    // Classes2 has this row filtered out, so it should have jumped to row1 instead (desynced from the others)
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2,    cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,    cursor: {rowNum: 2, col: 0}});

    // Undo the row-deletion
    await gu.undo();

    // The first 2 sections will still be on row2, but verify that Classes2 also joins them
    assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});
    assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2,   cursor: {rowNum: 2, col: 0}});
  });

  it('should support custom filters', async function() {
    // Add a new custom section with a widget.
    await gu.addNewPage('Table', 'Classes', {});

    // Rename this section as Data.
    await gu.renameActiveSection('Data');

    // Add new custom section with a widget.
    await gu.addNewSection('Custom', 'Classes', { selectBy: 'Data' });

    // Rename this section as Custom.
    await gu.renameActiveSection('Custom');

    // Make sure it can be used as a filter.
    await gu.changeWidgetAccess('read table');
    // Tell grist that we can be linked to.
    await gu.customCode(grist => grist.sectionApi.configure({allowSelectBy: true}));

    // Now link them together.
    await gu.selectSectionByTitle('Data');
    await gu.selectBy('Custom');

    // And now lets filter some rows in the Data by using custom widget API.
    await gu.selectSectionByTitle('Custom');
    await gu.customCode(grist => grist.setSelectedRows([1, 2, 3, 4]).catch(() => {}));

    // Check for the error. It was easy to create an infinite loop with the call above, and checking
    // for errors here was catching it.
    await gu.checkForErrors();

    // This link was not valid, so custom section wasn't filtered, but it managed to filter Data.
    // But in the process, the configuration was cleared.

    // Switch section, so that UI is refreshed.
    await gu.selectSectionByTitle('Data');
    await gu.selectSectionByTitle('Custom');

    // Make sure we can't select Data by Custom anymore.
    await gu.openSelectByForSection('Custom');

    // Make sure it is empty.
    assert.deepEqual(await driver.findAll('.test-select-menu .test-select-row', e => e.getText()), ['Select Widget']);
    await gu.sendKeys(Key.ESCAPE);
  });
});

interface CursorArrowInfo {
  arrow: null | number;
  cursor: {rowNum: number, col: number};
}

// NOTE: this differs from getCursorSelectorInfo in LinkingSelector.ts
// That function only gives the cursorPos if that section is actively selected (false otherwise), whereas this
// function returns it always
async function getCursorAndArrow(sectionName: string): Promise<CursorArrowInfo> {
  return {
    arrow: await gu.getArrowPosition(sectionName),
    cursor: await gu.getCursorPosition(sectionName),
  };
}