import 'package:platform_driver_rethinkdb/platform_driver_rethinkdb.dart';
import 'package:test/test.dart';

main() {
  var r = RethinkDb() as dynamic;
  String? databaseName;
  String? tableName;
  String? testDbName;
  bool shouldDropTable = false;
  Connection? connection;

  setUp(() async {
    connection = await r.connect();

    if (testDbName == null) {
      String useDb = await r.uuid().run(connection);
      testDbName = 'unit_test_db${useDb.replaceAll("-", "")}';
      await r.dbCreate(testDbName).run(connection);
    }

    if (databaseName == null) {
      String dbName = await r.uuid().run(connection);
      databaseName = "test_database_${dbName.replaceAll("-", "")}";
    }

    if (tableName == null) {
      String tblName = await r.uuid().run(connection);
      tableName = "test_table_${tblName.replaceAll("-", "")}";
    }
    connection!.use(testDbName!);
  });

  tearDown(() async {
    if (shouldDropTable) {
      shouldDropTable = false;
      await r.tableDrop(tableName).run(connection);
    }
    connection!.close();
  });

  test("r.db throws an error if a bad database name is given", () async {
    try {
      await r.db('fake2834723895').tableList().run(connection);
    } catch (err) {
      expect(err is Exception, equals(true));
      expect(err.toString().split("\n")[2],
          equals('Database `fake2834723895` does not exist.'));
    }
  });

  group("dbCreate command -> ", () {
    test("r.dbCreate will create a new database", () async {
      Map response = await r.dbCreate(databaseName).run(connection);

      expect(response.keys.length, equals(2));
      expect(response.containsKey('config_changes'), equals(true));
      expect(response['dbs_created'], equals(1));

      Map configChanges = response['config_changes'][0];
      expect(configChanges.keys.length, equals(2));
      expect(configChanges['old_val'], equals(null));
      Map newVal = configChanges['new_val'];
      expect(newVal.containsKey('id'), equals(true));
      expect(newVal.containsKey('name'), equals(true));
      expect(newVal['name'], equals(databaseName));
    });

    test("r.dbCreate will throw an error if the database exists", () async {
      try {
        await r.dbCreate(databaseName).run(connection);
      } catch (err) {
        expect(err is Exception, equals(true));
        expect(
            err.toString().split("\n")[2],
            // ignore: unnecessary_brace_in_string_interps
            equals('Database `${databaseName}` already exists.'));
      }
    });
  });

  group("dbDrop command -> ", () {
    test("r.dbDrop should drop a database", () async {
      Map response = await r.dbDrop(databaseName).run(connection);

      expect(response.keys.length, equals(3));
      expect(response.containsKey('config_changes'), equals(true));
      expect(response['dbs_dropped'], equals(1));
      expect(response['tables_dropped'], equals(0));

      Map configChanges = response['config_changes'][0];
      expect(configChanges.keys.length, equals(2));
      expect(configChanges['new_val'], equals(null));
      Map oldVal = configChanges['old_val'];
      expect(oldVal.containsKey('id'), equals(true));
      expect(oldVal.containsKey('name'), equals(true));
      expect(oldVal['name'], equals(databaseName));
    });

    test("r.dbDrop should error if the database does not exist", () async {
      try {
        await r.dbDrop(databaseName).run(connection);
      } catch (err) {
        expect(err.toString().split("\n")[2],
            equals('Database `$databaseName` does not exist.'));
      }
    });
  });

  test("r.dbList should list all databases", () async {
    List response = await r.dbList().run(connection);

    expect(response.indexOf('rethinkdb'), greaterThan(-1));
  });

  group("range command -> ", () {
    test("r.range() with no arguments should return a stream", () async {
      Cursor cur = await r.range().run(connection);

      List item = await cur.take(17).toList();
      expect(item,
          equals([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]));
    });

    test("r.range() should accept a single end arguement", () async {
      Cursor cur = await r.range(10).run(connection);

      List l = await cur.toList();
      expect(l, equals([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]));
    });

    test("r.range() should accept a start and end arguement", () async {
      Cursor cur = await r.range(7, 10).run(connection);
      List l = await cur.toList();
      expect(l, equals([7, 8, 9]));
    });
  });

  group("table command -> ", () {
    test("table should return a cursor containing all records for a table",
        () async {
      Cursor cur = await r.db('rethinkdb').table('stats').run(connection);
      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should allow for `read_mode: single` option", () async {
      Cursor cur = await r
          .db('rethinkdb')
          .table('stats', {'read_mode': 'single'}).run(connection);

      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should allow for `read_mode: majority` option", () async {
      Cursor cur = await r
          .db('rethinkdb')
          .table('stats', {'read_mode': 'majority'}).run(connection);

      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should allow for `read_mode: outdated` option", () async {
      Cursor cur = await r
          .db('rethinkdb')
          .table('stats', {'read_mode': 'outdated'}).run(connection);

      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should catch invalid read_mode option", () async {
      try {
        await r
            .db('rethinkdb')
            .table('stats', {'read_mode': 'badReadMode'}).run(connection);
      } catch (err) {
        expect(
            err.toString().split("\n")[2],
            equals(
                'Read mode `badReadMode` unrecognized (options are "majority", "single", and "outdated").'));
      }
    });

    test("table should allow for `identifier_format: name` option", () async {
      Cursor cur = await r
          .db('rethinkdb')
          .table('stats', {'identifier_format': 'name'}).run(connection);

      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should allow for `identifier_format: uuid` option", () async {
      Cursor cur = await r
          .db('rethinkdb')
          .table('stats', {'identifier_format': 'uuid'}).run(connection);

      await for (Map item in cur) {
        expect(item.containsKey('id'), equals(true));
        expect(item.containsKey('query_engine'), equals(true));
      }
    });

    test("table should catch invalid identifier_format option", () async {
      try {
        await r
            .db('rethinkdb')
            .table('stats', {'identifier_format': 'badFormat'}).run(connection);
      } catch (err) {
        expect(
            err.toString().split("\n")[2],
            equals(
                'Identifier format `badFormat` unrecognized (options are "name" and "uuid").'));
      }
    });

    test("table should catch bad options", () async {
      try {
        await r
            .db('rethinkdb')
            .table('stats', {'fake_option': 'bad_value'}).run(connection);
      } catch (err) {
        expect(err.toString().split("\n")[2],
            equals('Unrecognized optional argument `fake_option`.'));
      }
    });
  });

  group("time command -> ", () {
    test(
        "should return a time object if given a year, month, day, and timezone",
        () async {
      DateTime obj = await r.time(2010, 12, 29, timezone: 'Z').run(connection);

      expect(obj.runtimeType, equals(DateTime));
      expect(obj.isBefore(DateTime.now()), equals(true));
      expect(obj.minute, equals(0));
      expect(obj.second, equals(0));
    });

    test(
        "should return a time object if given a year, month, day, hour, minute, second, and timezone",
        () async {
      DateTime obj = await r
          .time(2010, 12, 29, hour: 7, minute: 33, second: 45, timezone: 'Z')
          .run(connection);

      expect(obj.runtimeType, equals(DateTime));
      expect(obj.isBefore(DateTime.now()), equals(true));
      expect(obj.minute, equals(33));
      expect(obj.second, equals(45));
    });
  });

  test(
      "nativeTime command -> should turn a native dart DateTime to a reql time",
      () async {
    DateTime dt = DateTime.now();
    DateTime rqlDt = await r.nativeTime(dt).run(connection);

    expect(dt.year, equals(rqlDt.year));
    expect(dt.month, equals(rqlDt.month));
    expect(dt.day, equals(rqlDt.day));
    expect(dt.hour, equals(rqlDt.hour));
    expect(dt.minute, equals(rqlDt.minute));
    expect(dt.second, equals(rqlDt.second));
  });

  group("ISO8601 command -> ", () {
    test("should take an ISO8601 string and convert it to a DateTime object",
        () async {
      DateTime dt =
          await r.ISO8601('1986-11-03T08:30:00-07:00').run(connection);

      expect(dt.year, equals(1986));
      expect(dt.month, equals(11));
      expect(dt.day, equals(3));
      expect(dt.minute, equals(30));
    });

    test("should accept a timezone argument as well", () async {
      DateTime dt =
          await r.ISO8601('1986-11-03T08:30:00-07:00', 'MST').run(connection);

      expect(dt.year, equals(1986));
      expect(dt.month, equals(11));
      expect(dt.day, equals(3));
      expect(dt.minute, equals(30));
    });
  });

  test("epochTime command -> should take a timestamp and return a time object",
      () async {
    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(531360000000);

    DateTime dt = await r.epochTime(531360000).run(connection);

    expect(dt.month, equals(dateTime.month));
    expect(dt.day, equals(dateTime.day));
    expect(dt.hour, equals(dateTime.hour));
    expect(dt.minute, equals(dateTime.minute));
    expect(dt.second, equals(dateTime.second));
  });

  test("now command -> should return current DateTime object", () async {
    DateTime dt = await r.now().run(connection);

    await Future.delayed(Duration(milliseconds: 1));

    //expect(dt is DateTime, equals(true));
    expect(DateTime.now().difference(dt).inSeconds == 0, equals(true));
  });

  group("rqlDo command -> ", () {
    test("should accept a single argument and function", () async {
      bool i = await r.rqlDo(3, (item) => item > 4).run(connection);

      expect(i, equals(false));
    });

    test("should accept a many arguments and a function", () async {
      num i = await r
          .rqlDo(
              3,
              4,
              5,
              6,
              7,
              (item1, item2, item3, item4, item5) =>
                  item1 + item2 + item3 + item4 + item5)
          .run(connection);

      expect(i, equals(25));
    });

    test("should accept many args and an expression", () async {
      Cursor cur = await r.rqlDo(3, 7, r.range).run(connection);

      List list = await cur.toList();
      expect(list, equals([3, 4, 5, 6]));
    });
  });

  group("branch command -> ", () {
    test("should accept a true test and return the true branch value",
        () async {
      String val = await r.branch(3 < 4, 'isTrue', 'isFalse').run(connection);

      expect(val, equals('isTrue'));
    });

    test("should accept a false test and return the false branch value",
        () async {
      String val = await r.branch(3 > 4, 'isTrue', 'isFalse').run(connection);

      expect(val, equals('isFalse'));
    });

    test("should accept multiple tests and actions", () async {
      String val = await r
          .branch(1 > 4, 'isTrue', 0 < 1, 'elseTrue', 'isFalse')
          .run(connection);

      expect(val, equals('elseTrue'));
    });
  });

  test("error command -> should create a custom error", () async {
    try {
      await r.error('This is my Error').run(connection);
    } catch (err) {
      expect(err.runtimeType, equals(ReqlUserError));
      expect(err.toString().split("\n")[2], equals('This is my Error'));
    }
  });

  group("js command -> ", () {
    test("should run custom javascript", () async {
      String jsString = """
        function concatStrs(){
          return 'firstHalf' + '_' + 'secondHalf';
        }
        concatStrs();
        """;

      String str = await r.js(jsString).run(connection);

      expect(str, equals('firstHalf_secondHalf'));
    });

    //TODO: fix test

    // test("should accept a timeout option", () async {
    //   String jsString = """
    //     function concatStrs(){
    //       return 'firstHalf' + '_' + 'secondHalf';
    //     }
    //     while(true){
    //       concatStrs();
    //     }
    //     """;
    //   int timeout = 3;
    //   try {
    //     await r.js(jsString, {'timeout': timeout}).run(connection);
    //   } catch (err) {
    //     expect(
    //         err.toString(),
    //         equals(
    //             'JavaScript query `$jsString` timed out after $timeout.000 seconds.'));
    //   }
    // });
  });

  group("json command -> ", () {
    test("should parse a json string", () async {
      String jsonString = "[1,2,3,4]";
      List obj = await r.json(jsonString).run(connection);

      expect([1, 2, 3, 4], equals(obj));
    });

    test("should throw error if jsonString is invalid", () async {
      String jsonString = "1,2,3,4]";
      try {
        await r.json(jsonString).run(connection);
      } catch (err) {
        expect(
            err.toString().split("\n")[2],
            equals(
                'Failed to parse "$jsonString" as JSON: The document root must not follow by other values.'));
      }
    });
  });

  group("object command -> ", () {
    test("should create an object from an array of values", () async {
      Map obj = await r
          .object('key', 'val', 'listKey', [1, 2, 3, 4], 'objKey', {'a': 'b'})
          .run(connection);

      //expect(obj is Map, equals(true));
      expect(obj['key'], equals('val'));
      expect(obj['listKey'], equals([1, 2, 3, 4]));
      expect(obj['objKey']['a'], equals('b'));
    });

    test("should throw an error if params cannot be parsed into a map",
        () async {
      try {
        await r
            .object('key', 'val', 'listKey', [1, 2, 3, 4], 'objKey', {'a': 'b'},
                'odd')
            .run(connection);
      } catch (err) {
        expect(
            err.toString().split("\n")[2],
            equals(
                'OBJECT expects an even number of arguments (but found 7).'));
      }
    });
  });

  test("args command -> should accept an array", () async {
    List l = await r.args([1, 2]).run(connection);

    expect(l, equals([1, 2]));
  });

  group("random command -> ", () {
    test("should generate a random number if no parameters are provided",
        () async {
      double number = await r.random().run(connection);

      expect(number, lessThanOrEqualTo(1));
      expect(number, greaterThanOrEqualTo(0));
    });

    test(
        "should generate a positive random int no greater than the single argument",
        () async {
      int number = await r.random(50).run(connection);

      expect(number, lessThanOrEqualTo(50));
      expect(number, greaterThanOrEqualTo(0));
    });

    test("should generate a random int between the two arguments", () async {
      int number = await r.random(50, 55).run(connection);

      expect(number, lessThanOrEqualTo(55));
      expect(number, greaterThanOrEqualTo(50));
    });

    test("should generate a random float between the two arguments", () async {
      double number = await r.random(50, 55, {'float': true}).run(connection);
      expect(number, lessThanOrEqualTo(55));
      expect(number, greaterThanOrEqualTo(50));
    });
  });

  group("not command -> ", () {
    test("should return false if given no arguements", () async {
      bool val = await r.not().run(connection);

      expect(val, equals(false));
    });

    test("should return the inverse of the argument provided", () async {
      bool val = await r.not(false).run(connection);

      expect(val, equals(true));
    });
  });

  group("map command -> ", () {
    test("should map over an array", () async {
      List arr =
          await r.map([1, 2, 3, 4, 5], (item) => item * 2).run(connection);
      expect(arr, equals([2, 4, 6, 8, 10]));
    });

    test("should map over multiple arrays", () async {
      List arr = await r.map([1, 2, 3, 4, 5], [10, 9, 8, 7],
          (item, item2) => item + item2).run(connection);

      //notice that the first array is longer but we
      //only map the length of the shortest array
      expect(arr, equals([11, 11, 11, 11]));
    });

    test("should map a sequence", () async {
      List arr = await r
          .map(
              r.expr({
                'key': [1, 2, 3, 4, 5]
              }).getField('key'),
              (item) => item + 1)
          .run(connection);

      expect(arr, equals([2, 3, 4, 5, 6]));
    });

    test("should map over multiple sequences", () async {
      List arr = await r
          .map(
              r.expr({
                'key': [1, 2, 3, 4, 5]
              }).getField('key'),
              r.expr({
                'key': [1, 2, 3, 4, 5]
              }).getField('key'),
              (item, item2) => item + item2)
          .run(connection);

      expect(arr, equals([2, 4, 6, 8, 10]));
    });
  });

  group("and command -> ", () {
    test("should and two values together", () async {
      bool val = await r.and(true, true).run(connection);

      expect(val, equals(true));
    });
    test("should and more than two values together", () async {
      bool val = await r.and(true, true, false).run(connection);

      expect(val, equals(false));
    });
  });

  group("or command -> ", () {
    test("should or two values together", () async {
      bool val = await r.or(true, false).run(connection);

      expect(val, equals(true));
    });

    test("should and more than two values together", () async {
      bool val = await r.or(false, false, false).run(connection);

      expect(val, equals(false));
    });
  });

  group("binary command -> ", () {
    test("should convert string to binary", () async {
      List data = await r.binary('billysometimes').run(connection);

      expect(
          data,
          equals([
            98,
            105,
            108,
            108,
            121,
            115,
            111,
            109,
            101,
            116,
            105,
            109,
            101,
            115
          ]));
    });
  });

  group("uuid command -> ", () {
    test("should create a unique uuid", () async {
      String val = await r.uuid().run(connection);

      expect(val, isNotNull);
    });

    test("should create a uuid based on a string key", () async {
      String key = "billysometimes";
      String val = await r.uuid(key).run(connection);

      expect(val, equals('b3f5029e-f777-572f-a85d-5529b74fd99b'));
    });
  });

  group("expr command -> ", () {
    test("expr should convert native string to rql string", () async {
      String str = await r.expr('string').run(connection);
      expect(str, equals('string'));
    });

    test("expr should convert native int to rql int", () async {
      int str = await r.expr(3).run(connection);
      expect(str, equals(3));
    });
    test("expr should convert native double to rql float", () async {
      double str = await r.expr(3.14).run(connection);
      expect(str, equals(3.14));
    });
    test("expr should convert native bool to rql bool", () async {
      bool str = await r.expr(true).run(connection);
      expect(str, equals(true));
    });
    test("expr should convert native list to rql array", () async {
      List str = await r.expr([1, 2, 3]).run(connection);
      expect(str, equals([1, 2, 3]));
    });
    test("expr should convert native object to rql object", () async {
      Map str = await r.expr({'a': 'b'}).run(connection);
      expect(str, equals({'a': 'b'}));
    });
  });

  test("remove the test database", () async {
    Map response = await r.dbDrop(testDbName).run(connection);

    expect(response.containsKey('config_changes'), equals(true));
    expect(response['dbs_dropped'], equals(1));
    expect(response['tables_dropped'], equals(0));
  });

  /// TO TEST:
  /// test with orderby: r.asc(attr)
  /// test with orderby: r.desc(attr)
  /// r.http(url)
  ///
  /// test with filter or something: r.row;
  /// test with time: r.monday ... r.sunday;
  ///    test with time: r.january .. r.december;
}