/* eslint-disable @typescript-eslint/dot-notation */
// ^^ For testing private methods ^^
// Import dotenv
import * as dotenv from 'dotenv';

// Import Mango
import { initMango, Collection, closeMango } from '../src';

// Import types
import Lock from './types/Lock';
import TestDocument from './types/TestDocument';
import FullTestDocument from './types/FullTestDocument';

// Import constants
import TEST_DOCUMENT from './constants/TEST_DOCUMENT';
import SECOND_TEST_DOCUMENT from './constants/SECOND_TEST_DOCUMENT';
import COLLECTION_NAMES from './constants/COLLECTION_NAMES';
import COLLECTION_OPTS from './constants/COLLECTION_OPTS';

// Import helpers
import cleanupTestCollections from './helpers/cleanupTestCollections';
import getLockCollectionName from './helpers/getLockCollectionName';
import getLockCollectionOpts from './helpers/getLockCollectionOpts';

/*------------------------------------------------------------------------*/
/*                                 Setup                                  */
/*------------------------------------------------------------------------*/

beforeAll(() => {
  dotenv.config();
  if (!process.env.MONGO_URL) {
    throw new Error('Set a MONGO_URL in .env for testing!');
  }

  // Initialize database
  // NOTE: bump schemaVersion if changes are made to test collections
  initMango({ schemaVersion: Date.now() });
});

/*------------------------------------------------------------------------*/
/*                               Teardown                                 */
/*------------------------------------------------------------------------*/

afterAll(async () => {
  // Cleanup
  await cleanupTestCollections();

  // Close connection
  await closeMango();
});

/*------------------------------------------------------------------------*/
/*                                 Tests                                  */
/*------------------------------------------------------------------------*/

test(
  'Basic Mango test: init, insert, find, delete, fail-to-find',
  async () => {
    const collectionName = COLLECTION_NAMES.test;
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Test insert:
    await testCollection.insert(TEST_DOCUMENT);

    // Test find:
    const foundDocuments = await testCollection.find({ id: TEST_DOCUMENT.id });

    const expectedFound: FullTestDocument[] = [{ ...TEST_DOCUMENT, mongoTimestamp: undefined }];

    expect(foundDocuments).toStrictEqual(expectedFound);

    // Test delete:
    await testCollection.delete({ id: TEST_DOCUMENT.id });

    const findNone = await testCollection.find({ id: TEST_DOCUMENT.id });

    expect(findNone).toStrictEqual([]);
  },
);

test(
  'deleteAll works',
  async () => {
    const collectionName = COLLECTION_NAMES.test;
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert initial test documents:
    await testCollection.insert(TEST_DOCUMENT);
    await testCollection.insert(SECOND_TEST_DOCUMENT);

    // Delete all:
    await testCollection.deleteAll({});

    // Find and compare
    const foundDocuments = await testCollection.find({});

    expect(foundDocuments).toStrictEqual([]);
  },
);

test(
  'count works',
  async () => {
    const collectionName = COLLECTION_NAMES.test;
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert initial test documents:
    await testCollection.insert(TEST_DOCUMENT);
    await testCollection.insert(SECOND_TEST_DOCUMENT);

    // Count all:
    const count = await testCollection.count({});
    expect(count).toBe(2);

    // Count only first document:
    const countFirst = await testCollection.count({ id: TEST_DOCUMENT.id });
    expect(countFirst).toBe(1);

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
    await testCollection.delete({ id: SECOND_TEST_DOCUMENT.id });
  },
);

test(
  'findAndExtractProp works',
  async () => {
    const collectionName = COLLECTION_NAMES.test;
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert initial test document:
    await testCollection.insert(TEST_DOCUMENT);
    await testCollection.insert(SECOND_TEST_DOCUMENT);

    // Find and extract message
    const messages = await testCollection.findAndExtractProp(
      {},
      'msg',
    );
    expect(messages).toStrictEqual([
      TEST_DOCUMENT.msg,
      SECOND_TEST_DOCUMENT.msg,
    ]);

    // Find and extract rating (exclude falsy)
    const ratings = await testCollection.findAndExtractProp(
      {},
      'rating',
      true,
    );
    expect(ratings).toStrictEqual(
      [TEST_DOCUMENT.rating, SECOND_TEST_DOCUMENT.rating]
        .filter((rating) => {
          return rating;
        }),
    );

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
    await testCollection.delete({ id: SECOND_TEST_DOCUMENT.id });
  },
);

test(
  'Testing the increment method',
  async () => {
    const collectionName = COLLECTION_NAMES.test;
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert initial test document:
    await testCollection.insert({ ...TEST_DOCUMENT, rating: 1 });

    // Increment the rating
    await testCollection.increment(TEST_DOCUMENT.id, 'rating');

    // Find and compare
    const incrementedDocument = (await testCollection.find({ id: TEST_DOCUMENT.id }))[0];

    expect(incrementedDocument.rating).toBe(2);

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
  },
);

test(
  'Testing updatePropValues',
  async () => {
    const collectionName = COLLECTION_NAMES.objTest;
    const options = COLLECTION_OPTS[collectionName];
    const testDocId = 777;

    const testObjDoc = {
      id: testDocId,
      messageMap: {
        hello: 'world',
        outer: 'space',
        mother: 'earth',
      },
    };

    const testCollection = new Collection<typeof testObjDoc>(collectionName, options);

    // Insert initial test document:
    await testCollection.insert(testObjDoc);

    // Update hello to 'moon'
    await testCollection.updatePropValues({ id: testDocId }, { 'messageMap.hello': 'moon' });

    // Find and compare
    const updatedDocument = (await testCollection.find({ id: testDocId }))[0];

    expect(updatedDocument.messageMap.hello).toBe('moon');

    // Cleanup
    await testCollection.delete({ id: testDocId });
  },
);

test(
  'Collection methods for array documents',
  async () => {
    const collectionName = COLLECTION_NAMES.arrayTest;
    const options = COLLECTION_OPTS[collectionName];
    const testDocId = 888;

    const testArrDoc = {
      id: testDocId,
      messages: [
        'First',
        'Second',
        'Third',
      ],
    };

    const testCollection = new Collection<typeof testArrDoc>(collectionName, options);

    // Insert initial test document:
    await testCollection.insert(testArrDoc);

    /* --------- Test Pushing Element --------- */

    // Push new array member
    await testCollection.push(testDocId, 'messages', 'Fourth');

    // Find and compare
    const pushedDocument = await testCollection.find({ id: testDocId }).then(docs => docs.shift());

    expect(pushedDocument.messages).toStrictEqual(['First', 'Second', 'Third', 'Fourth']);

    /* ---------- Test Filtering Out ---------- */

    // Filter out 'goodbye'
    await testCollection.filterOut({
      id: testDocId,
      arrayProp: 'messages',
      compareValue: 'Fourth',
    });

    // Compare to original
    const filteredDocument = await testCollection.find({ id: testDocId }).then(docs => docs.shift());

    expect(filteredDocument).toStrictEqual({ ...testArrDoc, mongoTimestamp: undefined });

    // Cleanup
    await testCollection.delete({ id: testDocId });
  },
);

test(
  'Test deep filtering an array of objects.',
  async () => {
    const collectionName = COLLECTION_NAMES.arrayTest;
    const options = COLLECTION_OPTS[collectionName];
    const testDocId = 999;

    const testArrDoc = {
      id: testDocId,
      messages: [
        { msg: 'Hello' },
        { msg: 'World' },
        { msg: '!!!' },
      ],
    };

    const testCollection = new Collection<typeof testArrDoc>(collectionName, options);

    // Insert initial test document:
    await testCollection.insert(testArrDoc);

    // Filter out '!!!'
    await testCollection.filterOut({
      id: testDocId,
      arrayProp: 'messages',
      compareProp: 'msg',
      compareValue: '!!!',
    });

    // Compare to original
    const filteredDocument = await testCollection.find({ id: testDocId }).then(docs => docs.shift());

    testArrDoc.messages.pop();
    expect(filteredDocument).toStrictEqual({ ...testArrDoc, mongoTimestamp: undefined });

    // Cleanup
    await testCollection.delete({ id: testDocId });
  },
);

test(
  'Basic Coconut lock/unlock test',
  async () => {
    const collectionName = COLLECTION_NAMES.coconut;

    // Get collection
    const options = COLLECTION_OPTS[collectionName];

    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert test document
    await testCollection.insert(TEST_DOCUMENT);

    // Get lock collection
    const lockCollectionName = getLockCollectionName(collectionName);
    const lockOpts = getLockCollectionOpts();
    const locks = new Collection<Lock>(lockCollectionName, lockOpts);

    // Test lock
    await testCollection['lock'](TEST_DOCUMENT.id);
    const foundLocks = await locks.find({ id: TEST_DOCUMENT.id });
    expect(foundLocks[0].id).toBe(TEST_DOCUMENT.id);

    // Test unlock
    await testCollection['unlock'](TEST_DOCUMENT.id);
    const noLocks = await locks.find({ id: TEST_DOCUMENT.id });
    expect(noLocks).toStrictEqual([]);

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
  },
);

test(
  'Coconut: testing that lock actually blocks',
  async () => {
    const collectionName = COLLECTION_NAMES.coconut;
    // Get collection
    const options = COLLECTION_OPTS[collectionName];
    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert test document
    await testCollection.insert(TEST_DOCUMENT);

    // Get lock collection
    const lockCollectionName = getLockCollectionName(collectionName);
    const lockOpts = getLockCollectionOpts();
    const locks = new Collection<Lock>(lockCollectionName, lockOpts);

    // Test lock
    await testCollection['lock'](TEST_DOCUMENT.id);
    const foundLocks = await locks.find({ id: TEST_DOCUMENT.id });
    expect(foundLocks[0].id).toBe(TEST_DOCUMENT.id);

    // Test that other lock will not overlap
    const lockStatus = { isLocked: true };
    const otherCollection = new Collection<TestDocument>('test_coconut_collection', options);
    const lockPromise = otherCollection['lock'](TEST_DOCUMENT.id)
      .then(() => {
        return lockStatus.isLocked;
      });

    // Unlock from the old collection
    await testCollection['unlock'](TEST_DOCUMENT.id);
    lockStatus.isLocked = false;

    // Checking the lock from before
    const resolvedLockPromise = await lockPromise;
    expect(resolvedLockPromise).toBe(false);
    await otherCollection['unlock'](TEST_DOCUMENT.id);
    const noLocks = await locks.find({ id: TEST_DOCUMENT.id });
    expect(noLocks).toStrictEqual([]);

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
  },
);

test(
  'Coconut: testing runAtomicProcedure',
  async () => {
    const collectionName = COLLECTION_NAMES.coconut;
    // Get collection
    const options = COLLECTION_OPTS[collectionName];
    const testCollection = new Collection<TestDocument>(collectionName, options);

    // Insert test document
    await testCollection.insert(TEST_DOCUMENT);

    // Get lock collection
    const lockCollectionName = getLockCollectionName(collectionName);
    const lockOpts = getLockCollectionOpts();
    const locks = new Collection<Lock>(lockCollectionName, lockOpts);

    const idToLock = TEST_DOCUMENT.id;

    await testCollection.runAtomicProcedure({
      idOrIdsToLock: idToLock,
      procedure: async (collection: Collection<TestDocument>) => {
        // Check that runAtomicProcedure properly locks the list of items
        const allLocks = await locks.find({});
        expect(allLocks.length).toBe(1);
        allLocks.forEach((lock) => {
          expect(lock.id).toBe(TEST_DOCUMENT.id);
        });

        // Try some operations
        await collection.updatePropValues(
          { id: TEST_DOCUMENT.id },
          { msg: 'Goodbye World' },
        );
      },
    });

    // Check that the changes have been pushed to the testCollection
    const changedTestDocs = await testCollection.find({ id: TEST_DOCUMENT.id });
    expect(changedTestDocs[0].msg).toBe('Goodbye World');

    // Test that items are no longer locked
    const noLocks = await locks.find({ id: TEST_DOCUMENT.id });
    expect(noLocks).toStrictEqual([]);

    // Cleanup
    await testCollection.delete({ id: TEST_DOCUMENT.id });
  },
);
