import 'dart:async';

import 'package:drift/drift.dart' hide isNull;
import 'package:drift/extensions/json1.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import '../../generated/todos.dart';
import '../../test_utils/test_utils.dart';

final _dataOfTodoEntry = {
  'id': 10,
  'title': 'A todo title',
  'content': 'Content',
  'category': 3
};

const _todoEntry = TodoEntry(
  id: 10,
  title: 'A todo title',
  content: 'Content',
  category: 3,
);

void main() {
  late TodoDb db;
  late MockExecutor executor;

  setUp(() {
    executor = MockExecutor();
    db = TodoDb(executor);
  });

  group('SELECT statements are generated', () {
    test('for simple statements', () async {
      await db.select(db.users, distinct: true).get();
      verify(executor.runSelect(
          'SELECT DISTINCT * FROM "users";', argThat(isEmpty)));
    });

    test('with limit statements', () async {
      await (db.select(db.users)..limit(10, offset: 0)).get();
      verify(executor.runSelect(
          'SELECT * FROM "users" LIMIT 10 OFFSET 0;', argThat(isEmpty)));
    });

    test('with simple limits', () async {
      await (db.select(db.users)..limit(10)).get();

      verify(executor.runSelect(
          'SELECT * FROM "users" LIMIT 10;', argThat(isEmpty)));
    });

    test('with like expressions', () async {
      await (db.select(db.users)..where((u) => u.name.like('Dash%'))).get();
      verify(executor
          .runSelect('SELECT * FROM "users" WHERE "name" LIKE ?;', ['Dash%']));
    });

    test('with order-by clauses', () async {
      await (db.select(db.users)
            ..orderBy([
              (u) => OrderingTerm.desc(u.isAwesome),
              (u) => OrderingTerm.asc(u.id)
            ]))
          .get();

      verify(executor.runSelect(
          'SELECT * FROM "users" ORDER BY '
          '"is_awesome" DESC, "id" ASC;',
          argThat(isEmpty)));
    });

    test('with random order by clause', () async {
      await (db.select(db.users)..orderBy([(u) => OrderingTerm.random()]))
          .get();

      verify(executor.runSelect(
          'SELECT * FROM "users" ORDER BY random() ASC;', argThat(isEmpty)));
    });

    test('with complex predicates', () async {
      await (db.select(db.users)
            ..where((u) =>
                u.name.equals('Dash').not() & u.id.isBiggerThanValue(12)))
          .get();

      verify(executor.runSelect(
          'SELECT * FROM "users" WHERE NOT ("name" = ?) AND "id" > ?;',
          ['Dash', 12]));
    });

    test('with expressions from boolean columns', () async {
      await (db.select(db.users)..where((u) => u.isAwesome)).get();

      verify(executor.runSelect(
          'SELECT * FROM "users" WHERE "is_awesome";', argThat(isEmpty)));
    });

    test('with aliased tables', () async {
      final users = db.alias(db.users, 'u');
      await (db.select(users)
            ..where((u) => u.id.isSmallerThan(const Constant(5))))
          .get();

      verify(
          executor.runSelect('SELECT * FROM "users" "u" WHERE "id" < 5;', []));
    });
  });

  group('SELECT results are parsed', () {
    test('when all fields are non-null', () {
      when(executor.runSelect('SELECT * FROM "todos";', any))
          .thenAnswer((_) => Future.value([_dataOfTodoEntry]));

      expect(db.select(db.todosTable).get(), completion([_todoEntry]));
    });

    test('when some fields are null', () {
      final data = [
        {
          'id': 10,
          'title': null,
          'content': 'Content',
          'category': null,
        }
      ];
      const resolved = TodoEntry(
        id: 10,
        title: null,
        content: 'Content',
        category: null,
      );

      when(executor.runSelect('SELECT * FROM "todos";', any))
          .thenAnswer((_) => Future.value(data));

      expect(db.select(db.todosTable).get(), completion([resolved]));
    });
  });

  group('queries for a single row', () {
    test('get once', () {
      when(executor.runSelect('SELECT * FROM "todos";', any))
          .thenAnswer((_) => Future.value([_dataOfTodoEntry]));
      expect(db.select(db.todosTable).getSingle(), completion(_todoEntry));
    });

    test('get once without rows', () {
      when(executor.runSelect('SELECT * FROM "todos";', any))
          .thenAnswer((_) => Future.value([]));

      expect(db.select(db.todosTable).getSingle(), throwsA(anything));
      expect(db.select(db.todosTable).getSingleOrNull(), completion(isNull));
    });

    test('get multiple times', () async {
      final resultRows = <List<Map<String, dynamic>>>[
        [_dataOfTodoEntry],
        [],
        [_dataOfTodoEntry, _dataOfTodoEntry],
      ];
      var currentRow = 0;

      when(executor.runSelect('SELECT * FROM "todos";', any)).thenAnswer((_) {
        return Future.value(resultRows[currentRow++]);
      });

      expectLater(
          db.select(db.todosTable).watchSingle(),
          emitsInOrder(
              [_todoEntry, emitsError(anything), emitsError(anything)]));
      expectLater(db.select(db.todosTable).watchSingleOrNull(),
          emitsInOrder([_todoEntry, isNull, emitsError(anything)]));

      await pumpEventQueue(); // First select as listeners attach
      db.markTablesUpdated({db.todosTable});
      await pumpEventQueue(); // Second select due to invalidation
      db.markTablesUpdated({db.todosTable});
      // Third select due to invalidation
    });
  });

  test('applies implicit type converter', () async {
    when(executor.runSelect(any, any)).thenAnswer((_) {
      return Future.value([
        {
          'id': 1,
          'desc': 'description',
          'description_in_upper_case': 'DESCRIPTION',
          'priority': 2,
        }
      ]);
    });

    final category = await db.select(db.categories).getSingle();

    expect(
      category,
      const Category(
        id: 1,
        description: 'description',
        descriptionInUpperCase: 'DESCRIPTION',
        priority: CategoryPriority.high,
      ),
    );
  });

  test('watching a view will update when a referenced table updates', () async {
    when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([]));

    final stream = db.select(db.categoryTodoCountView).watch();
    expect(stream, emitsInOrder([isEmpty, isEmpty]));

    await pumpEventQueue();
    db.markTablesUpdated([db.categories]);
  });

  test('select from subquery', () async {
    final data = [
      {
        'id': 10,
        'title': null,
        'content': 'Content',
        'category': null,
      }
    ];
    when(executor.runSelect(any, any)).thenAnswer((_) => Future.value(data));

    final subquery = Subquery(db.todosTable.select(), 's');
    final rows = await db.select(subquery).get();

    expect(rows, [
      TodoEntry(
        id: 10,
        title: null,
        content: 'Content',
        category: null,
      )
    ]);

    verify(executor.runSelect('SELECT * FROM (SELECT * FROM "todos") s;', []));
  });

  test('select from table-valued function', () async {
    final each = db.todosTable.content.jsonEach(db, r'$.foo');

    final query = db
        .select(db.todosTable)
        .join([innerJoin(each, each.atom.isNotNull(), useColumns: false)]);

    await query.get();

    verify(executor.runSelect(
      'SELECT "todos"."id" AS "todos.id", "todos"."title" AS "todos.title", "todos"."content" AS "todos.content", "todos"."target_date" AS "todos.target_date", "todos"."category" AS "todos.category", "todos"."status" AS "todos.status" FROM "todos" INNER JOIN json_each("todos"."content", ?) ON "json_each"."atom" IS NOT NULL;',
      [r'$.foo'],
    ));
  });
}
