import sqlite3
import time
from abc import (
    ABC,
    abstractmethod,
)
from dataclasses import dataclass
from sqlite3 import (
    connect,
    OperationalError,
)
from typing import (
    Sequence,
    Optional,
    Dict,
)

import pytest


@dataclass(frozen=True)
class DatabaseInfo(object):
    conn_string_template = "file:/${DB_NAME}?vfs=memdb"

    name: str
    number_of_instances: Optional[int]

    def __init__(self, name, number_of_instances: Optional[int] = None):
        object.__setattr__(self, "name", name)
        object.__setattr__(self, "number_of_instances", number_of_instances)

    @property
    def conn_string(self):
        return self._format_conn_string()

    @property
    def all_conn_strings(self) -> Sequence[str]:
        if self.number_of_instances is None:
            return [self.conn_string]
        else:
            return [self._format_conn_string(instance_number=i) for i in range(self.number_of_instances + 1)]

    @property
    def all_db_names(self) -> Sequence[str]:
        if self.number_of_instances is None:
            return [self.name]
        else:
            return [self._get_db_name(instance_number=i) for i in range(self.number_of_instances + 1)]

    def get_conn_string_dict(self):
        return {db_name: conn_string for db_name, conn_string in zip(self.all_db_names, self.all_conn_strings)}

    def _format_conn_string(self, instance_number: int = None):
        if self.number_of_instances is None:
            return self.conn_string_template.replace("${DB_NAME}", self.name)
        return self.conn_string_template.replace("${DB_NAME}", self._get_db_name(instance_number=instance_number))

    def _get_db_name(self, instance_number: int = None):
        return f"{self.name}{instance_number}"


class CreatedDatabaseTest(ABC):
    @pytest.fixture(autouse=True)
    def create_temporary_memory_database(
        self
    ):
        sqlite3.connect = self._wrap_connect(sqlite3.connect)

        conns = []
        for db_info in self.get_database_infos():
            for conn_string in db_info.all_conn_strings:
                conns.append(connect(database=conn_string, uri=True))

        yield

        for conn in conns:
            conn.close()

        sqlite3.connect = self._unwrap_connect(sqlite3.connect)

    @abstractmethod
    def get_database_infos(self) -> Sequence[DatabaseInfo]:
        raise NotImplementedError()

    def get_conn_strings(self) -> Dict[str, str]:
        ret = {}
        for db_info in self.get_database_infos():
            ret = {
                **ret,
                **db_info.get_conn_string_dict(),
            }

        return ret

    def _wrap_connect(self, original_connect):
        def wrapped_connect(*args, **kwargs):
            conn = original_connect(*args, **kwargs)
            return WrappedConnection(conn)

        wrapped_connect._original_connect = original_connect

        return wrapped_connect

    def _unwrap_connect(self, wrapped_connect):
        return wrapped_connect._original_connect


class WrappedConnection(object):
    def __init__(self, conn):
        self.conn = conn

    def cursor(self, *args, **kwargs):
        return WrappedCursor(self.conn.cursor(*args, **kwargs))

    def __getattr__(self, name):
        return getattr(self.conn, name)


class WrappedCursor(object):
    def __init__(self, cursor):
        self.cursor = cursor

    def execute(self, *args, **kwargs):
        return self._execute_with_retry(self.cursor.execute, 0, *args, **kwargs)

    def __getattr__(self, name):
        return getattr(self.cursor, name)

    def _execute_with_retry(self, func, retry_count, *args, **kwargs):
        try:
            return func(*args, **kwargs)
        except OperationalError as ex:
            if retry_count > 10:
                raise

            if not hasattr(ex, "args"):
                raise
            ex_args = getattr(ex, "args")
            if not isinstance(ex_args, tuple):
                raise
            if not isinstance(ex_args[0], str):
                raise

            if ex_args[0].startswith("database table is locked"):
                time.sleep(0)
                return self._execute_with_retry(func, retry_count + 1, *args, **kwargs)

            raise
