import {DocCreationInfo} from 'app/common/DocListAPI';
import {DocAPI} from 'app/common/UserAPI';
import {assert, driver, Key} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import {EnvironmentSnapshot} from 'test/server/testUtils';

describe('WebhookPage', function () {
  this.timeout(60000);
  const cleanup = setupTestSuite();
  const clipboard = gu.getLockableClipboard();

  let session: gu.Session;
  let oldEnv: EnvironmentSnapshot;
  let docApi: DocAPI;
  let doc: DocCreationInfo;
  let host: string;

  before(async function () {
    oldEnv = new EnvironmentSnapshot();
    host = new URL(server.getHost()).host;
    process.env.ALLOWED_WEBHOOK_DOMAINS = '*';
    await server.restart();
    session = await gu.session().teamSite.login();
    const api = session.createHomeApi();
    doc = await session.tempDoc(cleanup, 'Hello.grist');
    docApi = api.getDocAPI(doc.id);
    await api.applyUserActions(doc.id, [
      ['AddTable', 'Table2', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]],
    ]);
    await api.applyUserActions(doc.id, [
      ['AddTable', 'Table3', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]],
    ]);
    await api.updateDocPermissions(doc.id, {
      users: {
        // for convenience, we'll be sending payloads to the document itself.
        'anon@getgrist.com': 'editors',
        // check another owner's perspective.
        [gu.session().user('user2').email]: 'owners',
      }
    });
  });

  after(async function () {
    oldEnv.restore();
  });

  it('starts with an empty card', async function () {
    await openWebhookPage();
    assert.equal(await gu.getCardListCount(), 1);  // includes empty card
    assert.sameDeepMembers(await gu.getCardFieldLabels(), [
      'Name',
      'Memo',
      'Event Types',
      'Table',
      'Filter for changes in these columns (semicolon-separated ids)',
      'Ready Column',
      'URL',
      'Header Authorization',
      'Webhook Id',
      'Enabled',
      'Status',
    ]);
  });

  it('can create a persistent webhook', async function () {
    // Set up a webhook for Table1, and send it to Table2 (for ease of testing).
    await openWebhookPage();
    await setField(1, 'Event Types', 'add\nupdate\n');
    await setField(1, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table2/records?flat=1`);
    await setField(1, 'Table', 'Table1');
    // Once event types, URL, and table are set, the webhook is created.
    // Up until that point, nothing we've entered is actually persisted,
    // there is no back end for it.
    await gu.waitToPass(async () => {
      assert.include(await getField(1, 'Webhook Id'), '-');
    });
    const id = await getField(1, 'Webhook Id');
    // Reload and make sure the webhook id is still there.
    await driver.navigate().refresh();
    await waitForWebhookPage();
    await gu.waitToPass(async () => {
      assert.equal(await getField(1, 'Webhook Id'), id);
    });
    // Now other fields like name, memo, watchColIds, and Header Auth are persisted.
    await setField(1, 'Name', 'Test Webhook');
    await setField(1, 'Memo', 'Test Memo');
    await setField(1, 'Filter for changes in these columns (semicolon-separated ids)', 'A; B');
    await gu.waitForServer();
    await driver.navigate().refresh();
    await waitForWebhookPage();
    await gu.waitToPass(async () => {
      assert.equal(await getField(1, 'Name'), 'Test Webhook');
      assert.equal(await getField(1, 'Memo'), 'Test Memo');
      assert.equal(await getField(1, 'Filter for changes in these columns (semicolon-separated ids)'), 'A;B');
    });
    // Make sure the webhook is actually working.
    await docApi.addRows('Table1', {A: ['zig'], B: ['zag']});
    // Make sure the data gets delivered, and that the webhook status is updated.
    await gu.waitToPass(async () => {
      assert.lengthOf((await docApi.getRows('Table2')).A, 1);
      assert.equal((await docApi.getRows('Table2')).A[0], 'zig');
      assert.match(await getField(1, 'Status'), /status...success/);
    });
    // Remove the webhook and make sure it is no longer listed.
    assert.equal(await gu.getCardListCount(), 2);
    await gu.getDetailCell({col: 'Name', rowNum: 1}).click();
    await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE));
    await gu.confirm(true, true);
    await gu.waitForServer();
    assert.equal(await gu.getCardListCount(), 1);
    await driver.navigate().refresh();
    await waitForWebhookPage();
    assert.equal(await gu.getCardListCount(), 1);
    await docApi.removeRows('Table2', [1]);
    assert.lengthOf((await docApi.getRows('Table2')).A, 0);
  });

  it('can create webhook with persistant header authorization', async function () {
    // The webhook won't work because the header auth doesn't match the api key of the current test user.
    await openWebhookPage();
    await setField(1, 'Event Types', 'add\nupdate\n');
    await setField(1, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table2/records?flat=1`);
    await setField(1, 'Table', 'Table1');
    await gu.waitForServer();
    await driver.navigate().refresh();
    await waitForWebhookPage();
    await setField(1, 'Header Authorization', 'Bearer 1234');
    await gu.waitForServer();
    await driver.navigate().refresh();
    await waitForWebhookPage();
    await gu.waitToPass(async () => {
      assert.equal(await getField(1, 'Header Authorization'), 'Bearer 1234');
    });
    await gu.getDetailCell({col:'Header Authorization', rowNum: 1}).click();
    await gu.enterCell(Key.DELETE, Key.ENTER);
    await gu.waitForServer();
  });

  it('can create two webhooks', async function () {
    await openWebhookPage();
    await setField(1, 'Event Types', 'add\nupdate\n');
    await setField(1, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table2/records?flat=1`);
    await setField(1, 'Table', 'Table1');
    await gu.waitForServer();
    await setField(2, 'Event Types', 'add\n');
    await setField(2, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table3/records?flat=1`);
    await setField(2, 'Table', 'Table1');
    await gu.waitForServer();
    await docApi.addRows('Table1', {A: ['zig2'], B: ['zag2']});
    await gu.waitToPass(async () => {
      assert.lengthOf((await docApi.getRows('Table2')).A, 1);
      assert.lengthOf((await docApi.getRows('Table3')).A, 1);
      assert.match(await getField(1, 'Status'), /status...success/);
      assert.match(await getField(2, 'Status'), /status...success/);
    });
    await docApi.updateRows('Table1', {id: [1], A: ['zig3'], B: ['zag3']});
    await gu.waitToPass(async () => {
      assert.lengthOf((await docApi.getRows('Table2')).A, 2);
      assert.lengthOf((await docApi.getRows('Table3')).A, 1);
      assert.match(await getField(1, 'Status'), /status...success/);
    });
    await driver.sleep(100);
    // confirm that nothing shows up to Table3.
    assert.lengthOf((await docApi.getRows('Table3')).A, 1);
    // Break everything down.
    await gu.getDetailCell({col: 'Name', rowNum: 1}).click();
    await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE));
    await gu.confirm(true, true);
    await gu.waitForServer();
    await gu.getDetailCell({col: 'Memo', rowNum: 1}).click();
    await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE));
    await gu.waitForServer();
    assert.equal(await gu.getCardListCount(), 1);
    await driver.navigate().refresh();
    await waitForWebhookPage();
    assert.equal(await gu.getCardListCount(), 1);
    await docApi.removeRows('Table2', [1, 2]);
    await docApi.removeRows('Table3', [1]);
    assert.lengthOf((await docApi.getRows('Table2')).A, 0);
    assert.lengthOf((await docApi.getRows('Table3')).A, 0);
  });

  it('can create and repair a dud webhook', async function () {
    await openWebhookPage();
    await setField(1, 'Event Types', 'add\nupdate\n');
    await setField(1, 'URL', `http://${host}/notathing`);
    await setField(1, 'Table', 'Table1');
    await gu.waitForServer();
    await docApi.addRows('Table1', {A: ['dud1']});
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'Status'), /status...failure/);
      assert.match(await getField(1, 'Status'), /numWaiting..1/);
    });
    await setField(1, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table2/records?flat=1`);
    await driver.findContent('button', /Clear Queue/).click();
    await gu.waitForServer();
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'Status'), /numWaiting..0/);
    });
    assert.lengthOf((await docApi.getRows('Table2')).A, 0);
    await docApi.addRows('Table1', {A: ['dud2']});
    await gu.waitToPass(async () => {
      assert.lengthOf((await docApi.getRows('Table2')).A, 1);
      assert.match(await getField(1, 'Status'), /status...success/);
    });

    // Break everything down.
    await gu.getDetailCell({col: 'Name', rowNum: 1}).click();
    await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE));
    await gu.confirm(true, true);
    await gu.waitForServer();
    await docApi.removeRows('Table2', [1]);
    assert.lengthOf((await docApi.getRows('Table2')).A, 0);
  });

  it('can keep multiple sessions in sync', async function () {
    await openWebhookPage();

    // Open another tab.
    await driver.executeScript("window.open('about:blank', '_blank')");
    const [ownerTab, owner2Tab] = await driver.getAllWindowHandles();

    await driver.switchTo().window(owner2Tab);
    const otherSession = await gu.session().teamSite.user('user2').login();
    await otherSession.loadDoc(`/doc/${doc.id}`);
    await openWebhookPage();
    await setField(1, 'Event Types', 'add\nupdate\n');
    await setField(1, 'URL', `http://${host}/multiple`);
    await setField(1, 'Table', 'Table1');
    await gu.waitForServer();
    await driver.switchTo().window(ownerTab);
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'URL'), /multiple/);
    });
    assert.equal(await gu.getCardListCount(), 2);
    await setField(1, 'Memo', 'multiple memo');
    await driver.switchTo().window(owner2Tab);
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'Memo'), /multiple memo/);
    });

    // Basic undo support.
    await driver.switchTo().window(ownerTab);
    await gu.undo();
    await gu.waitToPass(async () => {
      assert.equal(await getField(1, 'Memo'), '');
    });
    await driver.switchTo().window(owner2Tab);
    await gu.waitToPass(async () => {
      assert.equal(await getField(1, 'Memo'), '');
    });

    // Basic redo support.
    await driver.switchTo().window(ownerTab);
    await gu.redo();
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'Memo'), /multiple memo/);
    });
    await driver.switchTo().window(owner2Tab);
    await gu.waitToPass(async () => {
      assert.match(await getField(1, 'Memo'), /multiple memo/);
    });

    await gu.getDetailCell({col: 'Name', rowNum: 1}).click();
    await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE));
    await gu.confirm(true, true);
    await driver.switchTo().window(ownerTab);
    await gu.waitToPass(async () => {
      assert.equal(await gu.getCardListCount(), 1);
    });
    await driver.switchTo().window(owner2Tab);
    await driver.close();
    await driver.switchTo().window(ownerTab);
  });

  /**
   * Checks that a particular route to modifying cells in a virtual table
   * is in place (previously it was not).
   */
  it('can paste into a cell without clicking into it', async function() {
    await openWebhookPage();
    await setField(1, 'Name', '1234');
    await gu.waitForServer();
    await clipboard.lockAndPerform(async (cb) => {
      await cb.copy();
      await gu.getDetailCell({col: 'Memo', rowNum: 1}).click();
      await cb.paste();
    });
    await gu.waitForServer();
    assert.equal(await getField(1, 'Memo'), '1234');
  });
});

async function setField(rowNum: number, col: string, text: string) {
  await gu.getDetailCell({col, rowNum}).click();
  await gu.enterCell(text);
}

async function getField(rowNum: number, col: string) {
  const cell = await gu.getDetailCell({col, rowNum});
  return cell.getText();
}

async function openWebhookPage() {
  await gu.wipeToasts();
  await gu.openDocumentSettings();
  await gu.scrollIntoView(driver.findContentWait('a', /Manage Webhooks/i, 3000)).click();
  await waitForWebhookPage();
}

async function waitForWebhookPage() {
  await driver.findContentWait('button', /Clear Queue/, 3000);
  // No section, so no easy utility for setting focus. Click on a random cell.
  await gu.getDetailCell({col: 'Webhook Id', rowNum: 1}).click();
}