import os
import time
from threading import Thread
from unittest import TestCase

from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound

from openmodule.config import database_folder
from openmodule.database.database import Database, active_databases, database_name
from openmodule.utils.db_helper import update_query, delete_query
from openmodule_test.database import SQLiteTestMixin
from openmodule_test.database_models import DatabaseTestModel


class DatabaseTest(SQLiteTestMixin, TestCase):
    alembic_path = "../tests/test_database_data"

    def test_filter(self):
        data = [DatabaseTestModel(value1=x, value2=x % 3) for x in range(5)]
        with self.database as db:
            db.add_all(data)
            self.assertEqual(1, db.query(DatabaseTestModel).filter_by(value1=0).count())
            self.assertEqual(2, db.query(DatabaseTestModel).filter_by(value2=0).count())

        with self.database as db:
            query = db.query(DatabaseTestModel).filter_by(value2=0)
            res = query.filter_by(value1=0)
            self.assertEqual(1, len(list(res)))
            self.assertEqual(0, res[0].value1)
            self.assertEqual(0, res[0].value2)

    def test_update_filter(self):
        data = [DatabaseTestModel(value1=x, value2=x % 3) for x in range(5)]
        with self.database as db:
            db.add_all(data)

            rows_changed = update_query(db, db.query(DatabaseTestModel).filter_by(value2=2), dict(value1=7))
            self.assertEqual(rows_changed, 1)
            results = db.query(DatabaseTestModel).filter_by(value2=2)
            self.assertEqual(2, results[0].value2)

            rows_changed = update_query(db, db.query(DatabaseTestModel).filter_by(value2=1), dict(value1=7))
            self.assertEqual(rows_changed, 2)
            results = db.query(DatabaseTestModel).filter_by(value2=1)
            self.assertEqual(2, results.count())
            self.assertEqual(3, db.query(DatabaseTestModel).filter_by(value1=7).count())

    def test_delete(self):
        data = [DatabaseTestModel(value1=x % 5, value2=x % 3) for x in range(10)]
        with self.database as db:
            db.add_all(data)

            delete_query(db, db.query(DatabaseTestModel).filter_by(value2=0, value1=17))
            self.assertEqual(10, len(db.query(DatabaseTestModel).all()))

            delete_query(db, db.query(DatabaseTestModel).filter_by(value2=0))
            self.assertEqual(6, len(db.query(DatabaseTestModel).all()))

            delete_query(db, db.query(DatabaseTestModel).filter_by(value2=1, value1=1))
            self.assertEqual(5, len(db.query(DatabaseTestModel).all()))

            tmp = db.query(DatabaseTestModel).filter_by(value1=4)

            self.assertTrue(1, tmp.count())
            db.delete(tmp[0])
            self.assertFalse(any(x.value1 == 4 for x in db.query(DatabaseTestModel).all()))

    def test_rollback(self):
        a = DatabaseTestModel(value1=5, value2=0)
        with self.database as db:
            db.add(a)
            db.flush()
            tmp = a.id
        with self.assertRaises(Exception):
            with self.database as db:
                a.value1 = 6
                db.add(a)
                raise Exception("asdf")

        with self.database as db:
            current = db.query(DatabaseTestModel).get(tmp)
            self.assertEqual(5, current.value1)
            self.assertEqual(1, len(db.query(DatabaseTestModel).all()))

    def test_database_transaction_is_single_threaded(self):
        """
        this test starts a second thread, which waits for 0.5s, after that it tries to aquire the database
        and read the value of an object. The main thread in the meantimes blocks for 2 seconds, and after that
        writes something to the database. Both threads check that the other is at the expected state.
        The test shows that the transaction is single threaded, and all threads are blocked while
        a thread has aquired the db
        """
        main_thread_position = ""
        second_thread_position = ""
        second_thread_value = None

        # initial setup model(1) with value = initial
        with self.database as db:
            model = DatabaseTestModel(id="1", string="initial")
            db.add(model)

        def second_thread():
            nonlocal second_thread_position, second_thread_value
            time.sleep(0.5)
            second_thread_position = "acquiring"
            with self.database as db:
                self.assertEqual(main_thread_position, "end")
                second_thread_model: DatabaseTestModel = db.query(DatabaseTestModel).get("1")
                second_thread_value = second_thread_model.string

        thread = Thread(target=second_thread)
        thread.start()

        with self.database as db:
            time.sleep(2)
            self.assertEqual(second_thread_position, "acquiring")
            model = db.query(DatabaseTestModel).get("1")
            model.string = "changed"
            db.add(model)
            main_thread_position = "end"

        thread.join()
        self.assertEqual(second_thread_value, "changed")

    def test_multiple_databases(self):
        db1 = Database(self.database_folder, name="asdf", alembic_path=self.alembic_path)
        with db1 as db:
            db.add(DatabaseTestModel(value1=0, value2=4))
        with self.assertRaises(Exception) as e:
            db2 = Database(self.database_folder, name="asdf", alembic_path=self.alembic_path)
        self.assertIn("already exists", str(e.exception))
        self.assertTrue(os.path.exists(os.path.join(database_folder(), "asdf.sqlite3")))

    def test_get(self):
        with self.database as db:
            base = db.query(DatabaseTestModel)
            res = base.get("asdf")
            self.assertIsNone(res)

            with self.assertRaises(Exception) as e:
                base.one()
            self.assertEqual(NoResultFound, type(e.exception))

            res = base.one_or_none()
            self.assertIsNone(res)

            res = base.first()
            self.assertIsNone(res)

            model1 = DatabaseTestModel()
            model2 = DatabaseTestModel()
            db.add(model1)
            db.add(model2)

            res = base.first()
            self.assertIsNotNone(res)

            with self.assertRaises(Exception) as e:
                base.one_or_none()
            self.assertEqual(MultipleResultsFound, type(e.exception))

class ShutdownTestCase(TestCase):
    alembic_path = "../tests/test_database_data"

    def get_database(self):
        return Database(database_folder(), name="shutdown", alembic_path=self.alembic_path)

    def check_db_present(self, value):
        tmp = database_name(database_folder(), "shutdown")
        if value:
            self.assertIn(tmp, active_databases.keys())
        else:
            self.assertNotIn(tmp, active_databases.keys())

    def test_shutdown(self):
        self.check_db_present(False)
        database = self.get_database()
        self.check_db_present(True)
        database.shutdown()
        self.check_db_present(False)

        database1 = self.get_database()
        self.check_db_present(True)
        del database1
        self.check_db_present(False)

        def scope():
            database2 = self.get_database()
            self.check_db_present(True)

        scope()
        self.check_db_present(False)

    def test_shutdown_while_using(self):
        database = self.get_database()

        def hold_db():
            with database as db:
                time.sleep(3)

        thread = Thread(target=hold_db)
        thread.start()
        self.assertEqual(True, thread.is_alive())
        self.check_db_present(True)
        database.shutdown()
        self.assertEqual(False, thread.is_alive())
        self.check_db_present(False)

    def test_double_shutdown(self):
        database = self.get_database()
        self.check_db_present(True)

        database.shutdown()

        with self.assertRaises(AssertionError) as e:
            database.shutdown()
        self.assertIn("already closed", str(e.exception))
        self.check_db_present(False)
