/**
 * Test of the UI for the SchemaEdit permission in Granular Access Control.
 */
import { UserAPI } from 'app/common/UserAPI';
import { TableRecordValue } from 'app/common/DocActions';
import { assert, driver } from 'mocha-webdriver';
import { assertChanged, assertSaved, enterRulePart,
         findDefaultRuleSet, findTable, getRules } from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
import pick = require('lodash/pick');

describe("AccessRulesSchemaEdit", function() {
  this.timeout(40000);
  const cleanup = setupTestSuite();
  let docId: string;
  let mainSession: gu.Session;
  let api: UserAPI;
  let editorApi: UserAPI;

  before(async function() {
    const editorSession = await gu.session().teamSite.user('user2').login();
    editorApi = editorSession.createHomeApi();

    // Import a test document we've set up for this.
    mainSession = await gu.session().teamSite.user('user1').login();
    docId = await mainSession.tempNewDoc(cleanup, 'ACL-SchemaEdit', {load: false});

    // Share it with a few users.
    api = mainSession.createHomeApi();
    await api.updateDocPermissions(docId, { users: {
      [gu.translateUser("user2").email]: 'editors',
    } });
    return docId;
  });

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

  it('should allow disabling non-owner schemaEdit via checkbox', async function() {
    const mainDocApi = api.getDocAPI(docId);

    // Open Access Rules page.
    await loadAccessRulesPage(mainSession, docId);

    // Check the schemaEdit checkbox is checked (editors allowed)
    assert.equal(await getSchemaEditCheckbox().isSelected(), true);

    // Check that a warning is present.
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isDisplayed(), true);

    // Check that default rules don't show the schemaEdit bit.
    assert.deepEqual((await getRules(findTable('*')))[0],
      {res: 'All', formula: 'user.Access in [EDITOR, OWNER]', perm: '+R+U+C+D'});

    // Check that an editor CAN make structure changes (default behavior is unchanged).
    await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed1']]));

    // Uncheck the box.
    await getSchemaEditCheckbox().click();
    assert.equal(await getSchemaEditCheckbox().isSelected(), false);

    // Check the warning is now absent.
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
    await assertChanged();

    // Save the changes.
    await saveRules();

    // Check that it works: an editor cannot make structure changes.
    await assert.isRejected(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed1', 'Table1']]),
      /Blocked by table structure access rules/);

    // Check that after reload, the box is unchecked and the warning is gone.
    await reloadAccessRulesPage();
    assert.equal(await getSchemaEditCheckbox().isSelected(), false);
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
    assert.equal(await getSchemaEditRuleSet().isPresent(), false);

    // Check what the rules are on the default resource.
    const rules = await mainDocApi.getRecords('_grist_ACLRules');
    const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
    assert.deepEqual(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")),
      [{resource: defaultResourceRef, aclFormula: 'user.Access != OWNER', permissionsText: '-S'}]);

    // Revert by checking the box again.
    await getSchemaEditCheckbox().click();
    assert.equal(await getSchemaEditCheckbox().isSelected(), true);

    // Check the warning is now present.
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isDisplayed(), true);

    // Save the changes.
    await saveRules();

    // Check that it works: an editor can make structure changes again.
    await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed1', 'Table1']]));

    // Check there are no rules left.
    assert.lengthOf(await mainDocApi.getRecords('_grist_ACLRules'), 0);
  });

  it('should allow dismissing the warning', async function() {
    const mainDocApi = api.getDocAPI(docId);
    await loadAccessRulesPage(mainSession, docId);

    // Check we are in the expected default state: checkbox is checked and a warning is present.
    assert.equal(await getSchemaEditCheckbox().isSelected(), true);
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), true);

    // Click "Dismiss", and save.
    await driver.findContent('.test-rule-schema-edit-warning a', /Dismiss/).click();
    await assertChanged();
    await saveRules();

    // Check that an editor can still make structure changes.
    await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed2']]));

    // Check that after reload, the box is checked and warning is gone.
    await reloadAccessRulesPage();
    assert.equal(await getSchemaEditCheckbox().isSelected(), true);
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
    assert.equal(await getSchemaEditRuleSet().isPresent(), false);

    // Check what the rules are on the default resource.
    const rules = await mainDocApi.getRecords('_grist_ACLRules');
    const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
    assert.deepEqual(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")),
      [{resource: defaultResourceRef, aclFormula: 'user.Access == EDITOR', permissionsText: '+S'}]);

    // Revert the rule change; wait for page to reload.
    await gu.undo();
    await driver.findWait('.test-rule-set', 2000);

    assert.equal(await getSchemaEditCheckbox().isSelected(), true);
    assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), true);
    assert.lengthOf(await mainDocApi.getRecords('_grist_ACLRules'), 0);

    // Let's revert also the table rename, to keep test cases independent.
    await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed2', 'Table1']]));
  });

  it('should handle existing rules that mix schemaEdit and other permissions', async function() {
    const editorEmailAddr = gu.translateUser("user2").email;
    const customAclFormula = `user.Email == "${editorEmailAddr}"`;

    // Use the API to add a mixed rule, with both a 'schemaEdit' and a 'delete' permission.
    const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
    await api.applyUserActions(docId, [
      ['AddRecord', '_grist_ACLRules', null, {
        resource: defaultResourceRef,
        aclFormula: customAclFormula,
        permissionsText: '-DS',
        memo: 'Memo MIXED',
      }],
    ]);

    // Load the Access Rules page.
    await loadAccessRulesPage(mainSession, docId);

    // The new rule should be visible in both the default section, and in SchemaEdit section
    // (which should be expanded).
    assert.deepEqual((await getRules(findTable('*')))[0],
      {formula: customAclFormula, perm: '-D', memo: 'Memo MIXED', res: 'All'});

    assert.equal(await getSchemaEditRuleSet().isDisplayed(), true);
    assert.deepEqual((await getRules(driver.find('.test-rule-special-SchemaEdit')))[0],
      {formula: customAclFormula, perm: '-S', memo: 'Memo MIXED'});

    // Check that the checkbox is disabled (since non-standard state).
    assert.equal(await getSchemaEditCheckbox().getAttribute('disabled'), 'true');

    // Check that the rule works.
    let error = await editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed3']])
      .then(() => null).catch(err => err);
    assert.match(error?.message, /Blocked by table structure access rules/);
    assert.deepInclude(error?.details, {memos: ['Memo MIXED']});

    // Change the memos on both copies of the rule.
    await enterRulePart(findDefaultRuleSet('*'), 1, null, {}, 'Memo DDD');
    await enterRulePart(getSchemaEditRuleSet(), 1, null, {}, 'Memo SSS');

    // Save.
    await saveRules();

    // The rules should look as before, only the memo is different.
    assert.deepEqual((await getRules(findTable('*')))[0],
      {formula: customAclFormula, perm: '-D', memo: 'Memo DDD', res: 'All'});
    assert.deepEqual((await getRules(driver.find('.test-rule-special-SchemaEdit')))[0],
      {formula: customAclFormula, perm: '-S', memo: 'Memo SSS'});

    // Check that the changed rule works.
    error = await editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed3']])
      .then(() => null).catch(err => err);
    assert.match(error?.message, /Blocked by table structure access rules/);
    assert.deepInclude(error?.details, {memos: ['Memo SSS']});

    // Check what the rules are on the default resource.
    const mainDocApi = api.getDocAPI(docId);
    const rules = await mainDocApi.getRecords('_grist_ACLRules');
    assert.sameDeepMembers(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")), [
      {resource: defaultResourceRef, aclFormula: customAclFormula, permissionsText: '-S'},
      {resource: defaultResourceRef, aclFormula: customAclFormula, permissionsText: '-D'}
    ]);
  });
});

function getSchemaEditCheckbox() {
  return driver.find('.test-rule-special-SchemaEdit input[type=checkbox]');
}
function getSchemaEditRuleSet() {
  return driver.find('.test-rule-special-SchemaEdit .test-rule-set');
}
function getSaveButton() {
  return driver.find('.test-rules-save');
}
async function saveRules() {
  await getSaveButton().click();
  await gu.waitForServer();
  await assertSaved();
}
async function loadAccessRulesPage(session: gu.Session, docId: string) {
  await session.loadRelPath(`/doc/${docId}/p/acl`);
  await driver.findWait('.test-rule-set', 5000);
}
async function reloadAccessRulesPage() {
  await driver.navigate().refresh();
  await driver.findWait('.test-rule-set', 5000);
}
async function getDefaultResourceRec(api: UserAPI, docId: string): Promise<TableRecordValue|undefined> {
  const records = await api.getDocAPI(docId).getRecords('_grist_ACLResources', {filters: {tableId: ['*']}});
  return records[0];
}