import { DatabaseOption, EncryptionKey, Storable, DatabaseDriver } from '../../src/typings';
import tap from 'tap';
import DB from '../../src/db/db';
import { randomBytes } from 'crypto';

const encryptionKey: EncryptionKey = 'I+mnyTixBoNGu0OtpG0KXJSunoPTiWMb';

const dbObjs: { [key: string]: DatabaseDriver } = {};
const connectionStores: Storable[] = [];
const ttlStores: Storable[] = [];
const ttl = 2;

const record1 = {
  id: '1',
  name: 'Deepak',
  city: 'London',
};

const record2 = {
  id: '2',
  name: 'Sama',
  city: 'London',
};

const record3 = {
  id: '3',
  name: 'Samuel Jackson',
  city: 'Delhi',
};

const records = [record1, record2, record3];

const memDbConfig = <DatabaseOption>{
  engine: 'mem',
  ttl: 1,
  pageLimit: 2,
};

const redisDbConfig = <DatabaseOption>{
  engine: 'redis',
  url: 'redis://localhost:6379',
  pageLimit: 2,
};

const postgresDbConfig = <DatabaseOption>{
  engine: 'sql',
  url: 'postgresql://postgres:postgres@localhost:5432/postgres',
  type: 'postgres',
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
};

const mongoDbConfig = <DatabaseOption>{
  engine: 'mongo',
  url: 'mongodb://localhost:27017/jackson',
  pageLimit: 2,
};

const mysqlDbConfig = <DatabaseOption>{
  engine: 'sql',
  url: 'mysql://root:mysql@localhost:3307/mysql',
  type: 'mysql',
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
};

const planetscaleDbConfig = <DatabaseOption>{
  engine: 'planetscale',
  url: process.env.PLANETSCALE_URL,
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
  // ssl: {
  //   rejectUnauthorized: true,
  // },
};

const mariadbDbConfig = <DatabaseOption>{
  engine: 'sql',
  url: 'mariadb://root@localhost:3306/mysql',
  type: 'mariadb',
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
};

const mssqlDbConfig = <DatabaseOption>{
  engine: 'sql',
  type: 'mssql',
  url: 'sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!',
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
};

const dynamoDbConfig = <DatabaseOption>{
  engine: 'dynamodb',
  url: process.env.DYNAMODB_URL,
  ttl: 1,
  cleanupLimit: 10,
  pageLimit: 2,
  dynamodb: {
    region: 'us-east-1',
    readCapacityUnits: 5,
    writeCapacityUnits: 5,
  },
};

const dbs = [
  {
    ...memDbConfig,
  },
  {
    ...memDbConfig,
    encryptionKey,
  },
  {
    ...redisDbConfig,
  },
  {
    ...redisDbConfig,
    encryptionKey,
  },
  {
    ...postgresDbConfig,
  },
  {
    ...postgresDbConfig,
    encryptionKey,
  },
  {
    ...mongoDbConfig,
  },
  {
    ...mongoDbConfig,
    encryptionKey,
  },
  {
    ...mysqlDbConfig,
  },
  {
    ...mysqlDbConfig,
    encryptionKey,
  },
  {
    ...mariadbDbConfig,
  },
  {
    ...mariadbDbConfig,
    encryptionKey,
  },
  {
    ...mssqlDbConfig,
  },
  {
    ...mssqlDbConfig,
    encryptionKey,
  },
];

if (process.env.PLANETSCALE_URL) {
  dbs.push(
    {
      ...planetscaleDbConfig,
    },
    {
      ...planetscaleDbConfig,
      encryptionKey,
    }
  );
}

if (process.env.DYNAMODB_URL) {
  dbs.push(
    {
      ...dynamoDbConfig,
    },
    {
      ...dynamoDbConfig,
      encryptionKey,
    }
  );
}

tap.before(async () => {
  for (const idx in dbs) {
    const opts = dbs[idx];
    const db = await DB.new(opts, true);
    dbObjs[opts.engine! + (opts.type ? opts.type : '')] = db;

    const randomSession = Date.now();
    connectionStores.push(db.store('saml:config:' + randomSession + randomBytes(4).toString('hex')));
    ttlStores.push(db.store('oauth:session:' + randomSession + randomBytes(4).toString('hex'), ttl));
  }
});

tap.teardown(async () => {
  process.exit(0);
});

tap.test('dbs', async () => {
  for (const idx in connectionStores) {
    const connectionStore = connectionStores[idx];
    const ttlStore = ttlStores[idx];
    const dbEngine = dbs[idx].engine!;
    let dbType = dbEngine;
    if (dbs[idx].type) {
      dbType += ': ' + dbs[idx].type;
    }

    tap.test('put(): ' + dbType, async () => {
      await connectionStore.put(
        record1.id,
        record1,
        {
          // secondary index on city
          name: 'city',
          value: record1.city,
        },
        {
          // secondary index on name
          name: 'name',
          value: record1.name,
        }
      );

      // wait 100ms to ensure that the record is written with a different timestamp
      await new Promise((resolve) => setTimeout(resolve, 100));

      await connectionStore.put(
        record2.id,
        record2,
        {
          // secondary index on city
          name: 'city',
          value: record2.city,
        },
        {
          // secondary index on name
          name: 'name',
          value: record2.name,
        }
      );

      // wait 100ms to ensure that the record is written with a different timestamp
      await new Promise((resolve) => setTimeout(resolve, 100));

      await connectionStore.put(
        record3.id,
        record3,
        {
          // secondary index on city
          name: 'city',
          value: record3.city,
        },
        {
          // secondary index on name
          name: 'name',
          value: record3.name,
        }
      );
    });

    tap.test('get(): ' + dbType, async (t) => {
      const ret1 = await connectionStore.get(record1.id);
      const ret2 = await connectionStore.get(record2.id);

      t.same(ret1, record1, 'unable to get record1');
      t.same(ret2, record2, 'unable to get record2');
    });

    tap.test('getAll(): ' + dbType, async (t) => {
      const testMessage =
        dbType === 'dynamodb' // dynamodb doesn't support sort order
          ? 'should return all the records upto options.pageLimit'
          : 'should return all the records upto options.pageLimit in DESC order by creation time';
      const wanted = dbType === 'dynamodb' ? records.slice(1, 3) : [...records].reverse().slice(0, 2);
      // getAll without pagination params
      t.same((await connectionStore.getAll()).data, wanted, `without pagination params ` + testMessage);
      // getAll with pagination params
      t.same((await connectionStore.getAll(0, 2)).data, wanted, `with pagination params ` + testMessage);
      // getAll with pageLimit set to 0
      t.same((await connectionStore.getAll(0, 0)).data, wanted, `with pageLimit set to 0 ` + testMessage);
      // getAll with pageLimit > options.pageLimit
      t.same(
        (await connectionStore.getAll(0, 3)).data,
        wanted,
        `with pageLimit > options.pageLimit ` + testMessage
      );

      const oneRecordWithPagination = await connectionStore.getAll(0, 1);
      t.same(
        oneRecordWithPagination.data.length,
        1,
        "getAll pagination should get only 1 record, order doesn't matter"
      );

      const secondRecordWithPagination = await connectionStore.getAll(
        1,
        1,
        oneRecordWithPagination.pageToken
      );
      t.same(
        secondRecordWithPagination.data.length,
        1,
        "getAll pagination should get only 1 record, order doesn't matter"
      );

      if (!dbEngine.startsWith('dynamodb')) {
        const { data: sortedRecordsAsc } = await connectionStore.getAll(0, 2, undefined, 'ASC');
        t.match(sortedRecordsAsc, [record1, record2], 'records are sorted in ASC order');

        const { data: sortedRecordsDesc } = await connectionStore.getAll(0, 2, undefined, 'DESC');
        t.match(sortedRecordsDesc, [record3, record2], 'records are sorted in DESC order');
      }
    });

    tap.test('getByIndex(): ' + dbType, async (t) => {
      const ret1 = await connectionStore.getByIndex({
        name: 'name',
        value: record1.name,
      });

      const ret2 = await connectionStore.getByIndex({
        name: 'city',
        value: record1.city,
      });

      t.same(ret1.data, [record1], 'unable to get index "name"');
      t.same(
        ret2.data.sort((a, b) => a.id.localeCompare(b.id)),
        [record1, record2].sort((a, b) => a.id.localeCompare(b.id)),
        'unable to get index "city"'
      );

      const ret3 = await connectionStore.getByIndex(
        {
          name: 'city',
          value: record1.city,
        },
        0,
        1
      );
      const numRec = dbEngine === 'dynamodb' ? 2 : 1;
      t.same(
        ret3.data.length,
        numRec,
        `getByIndex pagination should get only ${numRec} record, order doesn't matter`
      );

      const ret4 = await connectionStore.getByIndex(
        {
          name: 'city',
          value: record1.city,
        },
        1,
        1,
        ret3.pageToken
      );
      t.same(
        ret4.data.length,
        numRec,
        `getByIndex pagination should get only ${numRec} record, order doesn't matter`
      );

      t.same(
        ret2.data.sort((a, b) => a.id.localeCompare(b.id)),
        dbEngine === 'dynamodb'
          ? ret3.data.sort((a, b) => a.id.localeCompare(b.id))
          : [ret3.data[0], ret4.data[0]].sort((a, b) => a.id.localeCompare(b.id)),
        'getByIndex pagination for index "city" failed'
      );

      // sort order tests
      if (!dbEngine.startsWith('dynamodb') && !dbEngine.startsWith('redis')) {
        const { data: sortedRecordsAsc } = await connectionStore.getByIndex(
          {
            name: 'city',
            value: record1.city,
          },
          undefined,
          undefined,
          undefined,
          'ASC'
        );
        t.match(sortedRecordsAsc, [record1, record2], 'records are sorted in ASC order');

        const { data: sortedRecordsDesc } = await connectionStore.getByIndex(
          {
            name: 'city',
            value: record2.city,
          },
          undefined,
          undefined,
          undefined,
          'DESC'
        );
        t.match(sortedRecordsDesc, [record2, record1], 'records are sorted in DESC order');
      }
    });

    tap.test('getCount(): ' + dbType, async (t) => {
      if (dbEngine !== 'sql' && dbEngine !== 'mongo' && dbEngine !== 'planetscale') {
        console.log(`skipping getCount test for ${dbEngine}`);
        return;
      }
      const count = await connectionStore.getCount();
      t.equal(count, records.length);
      const countByIndex = await connectionStore.getCount({ name: 'name', value: record1.name });
      t.equal(countByIndex, 1);
    });

    tap.test('delete(): ' + dbType, async (t) => {
      await connectionStore.delete(record1.id);

      const ret0 = await connectionStore.getByIndex({
        name: 'city',
        value: record1.city,
      });

      t.same(ret0.data, [record2], 'unable to get index "city" after delete');

      await connectionStore.delete(record2.id);

      const ret1 = await connectionStore.get(record1.id);
      const ret2 = await connectionStore.get(record2.id);

      const ret3 = await connectionStore.getByIndex({
        name: 'name',
        value: record1.name,
      });
      const ret4 = await connectionStore.getByIndex({
        name: 'city',
        value: record1.city,
      });

      t.same(ret1, null, 'delete for record1 failed');
      t.same(ret2, null, 'delete for record2 failed');

      t.same(ret3.data, [], 'delete for record1 failed');
      t.same(ret4.data, [], 'delete for record2 failed');
    });

    tap.test('ttl put(): ' + dbType, async () => {
      await ttlStore.put(record1.id, record1);

      await ttlStore.put(record2.id, record2);
    });

    tap.test('ttl get(): ' + dbType, async (t) => {
      const ret1 = await ttlStore.get(record1.id);
      const ret2 = await ttlStore.get(record2.id);

      t.same(ret1, record1, 'unable to get record1');
      t.same(ret2, record2, 'unable to get record2');
    });

    tap.test('ttl expiry: ' + dbType, async (t) => {
      // mongo runs ttl task every 60 seconds
      if (dbEngine.startsWith('mongo')) {
        return;
      }

      await new Promise((resolve) => setTimeout(resolve, (2 * ttl + 0.5) * 1000));

      const ret1 = await ttlStore.get(record1.id);
      const ret2 = await ttlStore.get(record2.id);

      t.same(ret1, null, 'ttl for record1 failed');
      t.same(ret2, null, 'ttl for record2 failed');
    });

    tap.test('deleteMany(): ' + dbType, async (t) => {
      await connectionStore.put(
        record1.id,
        record1,
        {
          name: 'city',
          value: record1.city,
        },
        {
          name: 'name',
          value: record1.name,
        }
      );

      await connectionStore.put(
        record2.id,
        record2,
        {
          name: 'city',
          value: record2.city,
        },
        {
          name: 'name',
          value: record2.name,
        }
      );

      await connectionStore.deleteMany([record1.id, record2.id]);

      const ret1 = await connectionStore.get(record1.id);
      const ret2 = await connectionStore.get(record2.id);

      t.same(ret1, null);
      t.same(ret2, null);

      const ret3 = await connectionStore.getByIndex({
        name: 'name',
        value: record1.name,
      });

      const ret4 = await connectionStore.getByIndex({
        name: 'city',
        value: record1.city,
      });

      t.same(ret3.data, []);
      t.same(ret4.data, []);
    });
  }

  tap.test('close():', async () => {
    for (const [, value] of Object.entries(dbObjs)) {
      await value.close();
    }
  });
});
