import completer, { BASE_COMPLETIONS } from './';
import { signatures as shellSignatures, Topologies } from '@mongosh/shell-api';

import { expect } from 'chai';

let collections: string[];
let databases: string[];
const standalone440 = {
  topology: () => Topologies.Standalone,
  connectionInfo: () => ({
    is_atlas: false,
    is_data_lake: false,
    server_version: '4.4.0'
  }),
  getCollectionCompletionsForCurrentDb: () => collections,
  getDatabaseCompletions: () => databases
};
const sharded440 = {
  topology: () => Topologies.Sharded,
  connectionInfo: () => ({
    is_atlas: false,
    is_data_lake: false,
    server_version: '4.4.0'
  }),
  getCollectionCompletionsForCurrentDb: () => collections,
  getDatabaseCompletions: () => databases
};

const standalone300 = {
  topology: () => Topologies.Standalone,
  connectionInfo: () => ({
    is_atlas: false,
    is_data_lake: false,
    server_version: '3.0.0'
  }),
  getCollectionCompletionsForCurrentDb: () => collections,
  getDatabaseCompletions: () => databases
};
const datalake440 = {
  topology: () => Topologies.Sharded,
  connectionInfo: () => ({
    is_atlas: true,
    is_data_lake: true,
    server_version: '4.4.0'
  }),
  getCollectionCompletionsForCurrentDb: () => collections,
  getDatabaseCompletions: () => databases
};

const noParams = {
  topology: () => Topologies.Standalone,
  connectionInfo: () => undefined,
  getCollectionCompletionsForCurrentDb: () => collections,
  getDatabaseCompletions: () => databases
};

describe('completer.completer', () => {
  beforeEach(() => {
    collections = [];
  });

  context('when context is top level shell api', () => {
    it('matches shell completions', async() => {
      const i = 'u';
      expect(await completer(standalone440, i)).to.deep.equal([['use'], i]);
    });

    it('does not have a match', async() => {
      const i = 'ad';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('is an exact match to one of shell completions', async() => {
      const i = 'use';
      expect(await completer(standalone440, i)).to.deep.equal([[], i, 'exclusive']);
    });
  });

  context('when no version is passed to completer', () => {
    it('matches all db completions', async() => {
      const i = 'db.';
      const c = await completer(noParams, i);
      expect(c.length).to.equal(2);
      expect(c[1]).to.equal(i);
      expect(c[0]).to.include.members([
        'db.getMongo',
        'db.getName',
        'db.getCollectionNames',
        'db.getCollectionInfos',
        'db.runCommand',
        'db.adminCommand',
        'db.aggregate',
        'db.getSiblingDB',
        'db.getCollection',
        'db.dropDatabase',
        'db.createUser',
        'db.updateUser',
        'db.changeUserPassword',
        'db.logout',
        'db.dropUser',
        'db.dropAllUsers',
        'db.auth',
        'db.grantRolesToUser',
        'db.revokeRolesFromUser',
        'db.getUser',
        'db.getUsers',
        'db.createCollection',
        'db.createView',
        'db.createRole',
        'db.updateRole',
        'db.dropRole',
        'db.dropAllRoles',
        'db.grantRolesToRole',
        'db.revokeRolesFromRole',
        'db.grantPrivilegesToRole',
        'db.revokePrivilegesFromRole',
        'db.getRole',
        'db.getRoles'
      ]);
    });

    it('does not have a match', async() => {
      const i = 'db.shipwrecks.aggregate([ { $so';
      expect(await completer(noParams, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([ { $sort',
          'db.shipwrecks.aggregate([ { $sortByCount'], i]);
    });

    it('is an exact match to one of shell completions', async() => {
      const i = 'db.bios.find({ field: { $exis';
      expect(await completer(noParams, i))
        .to.deep.equal([['db.bios.find({ field: { $exists'], i]);
    });
  });

  context('datalake features', () => {
    let origBaseCompletions: any[];
    beforeEach(() => {
      // Undo https://github.com/mongodb-js/ace-autocompleter/pull/65 for testing
      // because it's the only DataLake-only feature.
      origBaseCompletions = [...BASE_COMPLETIONS];
      BASE_COMPLETIONS.push({
        name: '$sql',
        value: '$sql',
        label: '$sql',
        score: 1,
        env: [ 'adl' ],
        meta: 'stage',
        version: '4.0.0'
      });
    });
    afterEach(() => {
      BASE_COMPLETIONS.splice(0, BASE_COMPLETIONS.length, ...origBaseCompletions);
    });

    it('includes them when not connected', async() => {
      const i = 'db.shipwrecks.aggregate([ { $sq';
      expect(await completer(noParams, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([ { $sqrt',
          'db.shipwrecks.aggregate([ { $sql'], i]);
    });

    it('includes them when connected to DataLake', async() => {
      const i = 'db.shipwrecks.aggregate([ { $sq';
      expect(await completer(datalake440, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([ { $sqrt',
          'db.shipwrecks.aggregate([ { $sql'], i]);
    });

    it('does not include them when connected to a standalone node', async() => {
      const i = 'db.shipwrecks.aggregate([ { $sq';
      expect(await completer(standalone440, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([ { $sqrt'], i]);
    });
  });

  context('when context is top level db', () => {
    // this should eventually encompass tests for DATABASE commands and
    // COLLECTION names.
    // for now, this will only return the current input.
    it('matches a database command', async() => {
      const i = 'db.agg';
      expect(await completer(standalone440, i)).to.deep.equal([['db.aggregate'], i]);
    });

    it('returns all suggestions', async() => {
      const i = 'db.';
      const attr = shellSignatures.Database.attributes as any;
      const dbComplete = Object.keys(attr);
      const adjusted = dbComplete
        .filter(c => !attr[c].deprecated)
        .map(c => `${i}${c}`);
      expect(await completer(noParams, i)).to.deep.equal([adjusted, i]);
    });

    it('matches several suggestions', async() => {
      const i = 'db.get';
      expect((await completer(standalone440, i))[0]).to.include.members(
        [
          'db.getCollectionNames',
          'db.getCollection',
          'db.getCollectionInfos',
          'db.getSiblingDB'
        ]);
    });

    it('returns current input and no suggestions', async() => {
      const i = 'db.shipw';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('includes collection names', async() => {
      collections = ['shipwrecks'];
      const i = 'db.shipw';
      expect(await completer(standalone440, i)).to.deep.equal([['db.shipwrecks'], i]);
    });
  });

  context('when context is collections', () => {
    it('matches a collection command', async() => {
      const i = 'db.shipwrecks.findAnd';
      expect(await completer(standalone440, i)).to.deep.equal([['db.shipwrecks.findAndModify'], i]);
    });

    it('matches a collection command if part of an expression', async() => {
      const i = 'var result = db.shipwrecks.findAnd';
      expect(await completer(standalone440, i)).to.deep.equal([['var result = db.shipwrecks.findAndModify'], i]);
    });

    it('returns all suggestions', async() => {
      const i = 'db.shipwrecks.';
      const collComplete = Object.keys(shellSignatures.Collection.attributes as any);
      const adjusted = collComplete.filter(c => !['count', 'update', 'remove', 'insert', 'save'].includes(c)).map(c => `${i}${c}`);

      expect(await completer(sharded440, i)).to.deep.equal([adjusted, i]);
    });

    it('matches several collection commands', async() => {
      const i = 'db.shipwrecks.find';
      expect(await completer(standalone440, i)).to.deep.equal([
        [
          'db.shipwrecks.find', 'db.shipwrecks.findAndModify',
          'db.shipwrecks.findOne', 'db.shipwrecks.findOneAndDelete',
          'db.shipwrecks.findOneAndReplace', 'db.shipwrecks.findOneAndUpdate'
        ], i]);
    });

    it('does not have a match', async() => {
      const i = 'db.shipwrecks.pr';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('does not provide anything if there is a function call instead of a collection name', async() => {
      const i = 'db.getMongo().find';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('provides results if the function call is getCollection', async() => {
      const i = 'db.getCollection("foo").find';
      expect((await completer(standalone440, i))[0].length).to.be.greaterThan(1);
    });
  });

  context('when context is collections and aggregation cursor', () => {
    it('matches an aggregation cursor command', async() => {
      const i = 'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).has';
      expect(await completer(standalone440, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).hasNext'], i]);
    });

    it('returns all suggestions', async() => {
      const i = 'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).';
      const aggCursorComplete = Object.keys(shellSignatures.AggregationCursor.attributes as any);
      const adjusted = aggCursorComplete.map(c => `${i}${c}`);

      expect(await completer(standalone440, i)).to.deep.equal([adjusted, i]);
    });

    it('does not have a match', async() => {
      const i = 'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).w';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('has several matches', async() => {
      const i = 'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).i';
      expect(await completer(standalone440, i)).to.deep.equal([
        [
          'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).isClosed',
          'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).isExhausted',
          'db.shipwrecks.aggregate([{$sort: {feature_type: 1}}]).itcount'
        ], i]);
    });
  });

  context('when context is aggregation query', () => {
    it('has several matches', async() => {
      const i = 'db.shipwrecks.aggregate([ { $so';
      expect(await completer(standalone440, i)).to.deep.equal([
        ['db.shipwrecks.aggregate([ { $sort',
          'db.shipwrecks.aggregate([ { $sortByCount'], i]);
    });

    it('does not have a match', async() => {
      const i = 'db.shipwrecks.aggregate([ { $cat';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('matches an aggregation stage', async() => {
      const i = 'db.shipwrecks.aggregate([ { $proj';
      expect(await completer(standalone440, i)).to.deep.equal([
        [ 'db.shipwrecks.aggregate([ { $project' ], i]);
    });
  });

  context('when context is a collection query', () => {
    it('returns all suggestions', async() => {
      const i = 'db.shipwrecks.find({ ';
      expect((await completer(standalone440, i))[0]).to.include.members(
        [ 'db.shipwrecks.find({ $all',
          'db.shipwrecks.find({ $and',
          'db.shipwrecks.find({ $bitsAllClear',
          'db.shipwrecks.find({ $bitsAllSet',
          'db.shipwrecks.find({ $bitsAnyClear',
          'db.shipwrecks.find({ $bitsAnySet',
          'db.shipwrecks.find({ $comment',
          'db.shipwrecks.find({ $elemMatch',
          'db.shipwrecks.find({ $eq',
          'db.shipwrecks.find({ $exists',
          'db.shipwrecks.find({ $expr',
          'db.shipwrecks.find({ $geoIntersects',
          'db.shipwrecks.find({ $geoWithin',
          'db.shipwrecks.find({ $gt',
          'db.shipwrecks.find({ $gte',
          'db.shipwrecks.find({ $in',
          'db.shipwrecks.find({ $jsonSchema',
          'db.shipwrecks.find({ $lt',
          'db.shipwrecks.find({ $lte',
          'db.shipwrecks.find({ $mod',
          'db.shipwrecks.find({ $ne',
          'db.shipwrecks.find({ $near',
          'db.shipwrecks.find({ $nearSphere',
          'db.shipwrecks.find({ $nin',
          'db.shipwrecks.find({ $not',
          'db.shipwrecks.find({ $nor',
          'db.shipwrecks.find({ $or',
          'db.shipwrecks.find({ $regex',
          'db.shipwrecks.find({ $size',
          'db.shipwrecks.find({ $slice',
          'db.shipwrecks.find({ $text',
          'db.shipwrecks.find({ $type',
          'db.shipwrecks.find({ $where',
          'db.shipwrecks.find({ Code',
          'db.shipwrecks.find({ ObjectId',
          'db.shipwrecks.find({ Binary',
          'db.shipwrecks.find({ DBRef',
          'db.shipwrecks.find({ Timestamp',
          'db.shipwrecks.find({ NumberInt',
          'db.shipwrecks.find({ NumberLong',
          'db.shipwrecks.find({ NumberDecimal',
          'db.shipwrecks.find({ MaxKey',
          'db.shipwrecks.find({ MinKey',
          'db.shipwrecks.find({ ISODate',
          'db.shipwrecks.find({ RegExp' ]);
    });

    it('has several matches', async() => {
      const i = 'db.bios.find({ birth: { $g';
      expect(await completer(standalone440, i)).to.deep.equal([
        [
          'db.bios.find({ birth: { $geoIntersects',
          'db.bios.find({ birth: { $geoWithin',
          'db.bios.find({ birth: { $gt',
          'db.bios.find({ birth: { $gte',
        ], i]);
    });

    it('does not have a match', async() => {
      const i = 'db.bios.find({ field: { $cat';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('matches an aggregation stage', async() => {
      const i = 'db.bios.find({ field: { $exis';
      expect(await completer(standalone440, i)).to.deep.equal([
        [ 'db.bios.find({ field: { $exists' ], i]);
    });
  });

  context('when context is collections and collection cursor', () => {
    it('matches a collection cursor command', async() => {
      const i = 'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).for';
      expect(await completer(standalone440, i)).to.deep.equal([
        ['db.shipwrecks.find({feature_type: "Wrecks - Visible"}).forEach'], i]);
    });

    it('returns all suggestions running on 4.4.0 version', async() => {
      const i = 'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).';

      const result = [
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).allowPartialResults',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).batchSize',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).close',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).collation',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).comment',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).explain',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).forEach',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).hasNext',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).hint',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).isClosed',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).isExhausted',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).itcount',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).limit',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).map',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).max',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).maxTimeMS',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).maxAwaitTimeMS',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).min',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).next',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).noCursorTimeout',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).oplogReplay',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).projection',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).readPref',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).returnKey',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).size',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).skip',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).sort',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).tailable',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).toArray',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).pretty',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).showRecordId',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).objsLeftInBatch',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).readConcern',
      ];

      expect((await completer(standalone440, i))[0]).to.include.members(result);
    });

    it('returns all suggestions matching 3.0.0 version', async() => {
      const i = 'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).';

      const result = [
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).addOption',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).allowPartialResults',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).batchSize',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).close',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).count',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).explain',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).forEach',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).hasNext',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).hint',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).isClosed',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).isExhausted',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).itcount',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).limit',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).map',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).max',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).maxTimeMS',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).min',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).next',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).noCursorTimeout',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).oplogReplay',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).projection',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).readPref',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).size',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).skip',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).sort',
        'db.shipwrecks.find({feature_type: \"Wrecks - Visible\"}).toArray',
      ];

      expect((await completer(standalone300, i))[0]).to.include.members(result);
    });

    it('does not have a match', async() => {
      const i = 'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).gre';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });

    it('has several matches', async() => {
      const i = 'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).cl';
      expect(await completer(standalone440, i)).to.deep.equal([
        [
          'db.shipwrecks.find({feature_type: "Wrecks - Visible"}).close'
        ], i]);
    });

    it('does not match if it is not .find or .aggregate', async() => {
      const i = 'db.shipwrecks.moo({feature_type: "Wrecks - Visible"}).';
      expect(await completer(standalone440, i)).to.deep.equal([[], i]);
    });
  });

  context('for shell commands', () => {
    it('completes partial commands', async() => {
      const i = 'sho';
      expect(await completer(noParams, i))
        .to.deep.equal([['show'], i]);
    });

    it('completes partial commands', async() => {
      const i = 'show';
      const result = await completer(noParams, i);
      expect(result[0]).to.contain('show databases');
    });

    it('completes show databases', async() => {
      const i = 'show d';
      expect(await completer(noParams, i))
        .to.deep.equal([['show databases'], i, 'exclusive']);
    });

    it('completes show profile', async() => {
      const i = 'show pr';
      expect(await completer(noParams, i))
        .to.deep.equal([['show profile'], i, 'exclusive']);
    });

    it('completes use db', async() => {
      databases = ['db1', 'db2'];
      const i = 'use';
      expect(await completer(noParams, i))
        .to.deep.equal([['use db1', 'use db2'], i, 'exclusive']);
    });

    it('does not try to complete over-long commands', async() => {
      databases = ['db1', 'db2'];
      const i = 'use db1 d';
      expect(await completer(noParams, i))
        .to.deep.equal([[], i, 'exclusive']);
    });

    it('completes commands like exit', async() => {
      const i = 'exi';
      expect(await completer(noParams, i))
        .to.deep.equal([['exit'], i]);
    });
  });
});
