import logging
import sqlite3
from random import choice

import mysql.connector
import pytest
from mysql.connector import errorcode
from sqlalchemy import inspect
from sqlalchemy.dialects.mysql import __all__ as mysql_column_types

from mysql_to_sqlite3 import MySQLtoSQLite


class TestMySQLtoSQLiteClassmethods:
    def test_translate_type_from_mysql_to_sqlite_invalid_column_type(self, mocker):
        with pytest.raises(ValueError) as excinfo:
            mocker.patch.object(MySQLtoSQLite, "_valid_column_type", return_value=False)
            MySQLtoSQLite._translate_type_from_mysql_to_sqlite("text")
        assert "Invalid column_type!" in str(excinfo.value)

    def test_translate_type_from_mysql_to_sqlite_all_valid_columns(self):
        for column_type in mysql_column_types + (
            "CHAR(2)",
            "NCHAR(7)",
            "NVARCHAR(17)",
            "VARCHAR(123)",
        ):
            if column_type in {"dialect", "insert", "Insert"}:
                continue
            elif column_type == "INT":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "INTEGER"
                )
            elif column_type == "DECIMAL":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "DECIMAL"
                )
            elif column_type == "YEAR":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "YEAR"
                )
            elif column_type == "TIME":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "TIME"
                )
            elif column_type == "TIMESTAMP":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "DATETIME"
                )
            elif column_type in {
                "BINARY",
                "BIT",
                "LONGBLOB",
                "MEDIUMBLOB",
                "TINYBLOB",
                "VARBINARY",
            }:
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "BLOB"
                )
            elif column_type == "CHAR":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "CHARACTER"
                )
            elif column_type == "CHAR(2)":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "CHARACTER(2)"
                )
            elif column_type == "NCHAR(7)":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "NCHAR(7)"
                )
            elif column_type == "NVARCHAR(17)":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "NVARCHAR(17)"
                )
            elif column_type == "VARCHAR(123)":
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "VARCHAR(123)"
                )
            elif column_type in {
                "ENUM",
                "JSON",
                "LONGTEXT",
                "MEDIUMTEXT",
                "SET",
                "TINYTEXT",
            }:
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == "TEXT"
                )
            else:
                assert (
                    MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
                    == column_type
                )


@pytest.mark.exceptions
@pytest.mark.usefixtures("mysql_instance")
class TestMySQLtoSQLiteSQLExceptions:
    def test_create_table_server_lost_connection_error(
        self, sqlite_database, mysql_database, mysql_credentials, mocker, caplog
    ):
        proc = MySQLtoSQLite(
            sqlite_file=sqlite_database,
            mysql_user=mysql_credentials.user,
            mysql_password=mysql_credentials.password,
            mysql_database=mysql_credentials.database,
            mysql_host=mysql_credentials.host,
            mysql_port=mysql_credentials.port,
        )

        class FakeSQLiteCursor:
            def executescript(self, *args, **kwargs):
                raise mysql.connector.Error(
                    msg="Error Code: 2013. Lost connection to MySQL server during query",
                    errno=errorcode.CR_SERVER_LOST,
                )

        class FakeSQLiteConnector:
            def commit(self, *args, **kwargs):
                return True

        mysql_inspect = inspect(mysql_database.engine)
        mysql_tables = mysql_inspect.get_table_names()

        mocker.patch.object(proc, "_sqlite_cur", FakeSQLiteCursor())
        mocker.patch.object(proc._mysql, "reconnect", return_value=True)
        mocker.patch.object(proc, "_sqlite", FakeSQLiteConnector())
        caplog.set_level(logging.DEBUG)
        with pytest.raises(mysql.connector.Error):
            proc._create_table(choice(mysql_tables))

    def test_create_table_unknown_mysql_connector_error(
        self, sqlite_database, mysql_database, mysql_credentials, mocker, caplog
    ):
        proc = MySQLtoSQLite(
            sqlite_file=sqlite_database,
            mysql_user=mysql_credentials.user,
            mysql_password=mysql_credentials.password,
            mysql_database=mysql_credentials.database,
            mysql_host=mysql_credentials.host,
            mysql_port=mysql_credentials.port,
        )

        class FakeSQLiteCursor:
            def executescript(self, statement):
                raise mysql.connector.Error(
                    msg="Error Code: 2000. Unknown MySQL error",
                    errno=errorcode.CR_UNKNOWN_ERROR,
                )

        mysql_inspect = inspect(mysql_database.engine)
        mysql_tables = mysql_inspect.get_table_names()
        mocker.patch.object(proc, "_sqlite_cur", FakeSQLiteCursor())
        caplog.set_level(logging.DEBUG)
        with pytest.raises(mysql.connector.Error):
            proc._create_table(choice(mysql_tables))

    def test_create_table_sqlite3_error(
        self, sqlite_database, mysql_database, mysql_credentials, mocker, caplog
    ):
        proc = MySQLtoSQLite(
            sqlite_file=sqlite_database,
            mysql_user=mysql_credentials.user,
            mysql_password=mysql_credentials.password,
            mysql_database=mysql_credentials.database,
            mysql_host=mysql_credentials.host,
            mysql_port=mysql_credentials.port,
        )

        class FakeSQLiteCursor:
            def executescript(self, *args, **kwargs):
                raise sqlite3.Error("Unknown SQLite error")

        mysql_inspect = inspect(mysql_database.engine)
        mysql_tables = mysql_inspect.get_table_names()
        mocker.patch.object(proc, "_sqlite_cur", FakeSQLiteCursor())
        caplog.set_level(logging.DEBUG)
        with pytest.raises(sqlite3.Error):
            proc._create_table(choice(mysql_tables))

    @pytest.mark.parametrize(
        "exception",
        [
            pytest.param(
                mysql.connector.Error(
                    msg="Error Code: 2013. Lost connection to MySQL server during query",
                    errno=errorcode.CR_SERVER_LOST,
                ),
                id="errorcode.CR_SERVER_LOST",
            ),
            pytest.param(
                mysql.connector.Error(
                    msg="Error Code: 2000. Unknown MySQL error",
                    errno=errorcode.CR_UNKNOWN_ERROR,
                ),
                id="errorcode.CR_UNKNOWN_ERROR",
            ),
            pytest.param(sqlite3.Error("Unknown SQLite error"), id="sqlite3.Error"),
        ],
    )
    def test_transfer_table_data_exceptions(
        self,
        sqlite_database,
        mysql_database,
        mysql_credentials,
        mocker,
        caplog,
        exception,
    ):
        proc = MySQLtoSQLite(
            sqlite_file=sqlite_database,
            mysql_user=mysql_credentials.user,
            mysql_password=mysql_credentials.password,
            mysql_database=mysql_credentials.database,
            mysql_host=mysql_credentials.host,
            mysql_port=mysql_credentials.port,
        )

        class FakeMySQLCursor:
            def fetchall(self):
                raise exception

            def fetchmany(self, size=1):
                raise exception

        mysql_inspect = inspect(mysql_database.engine)
        mysql_tables = mysql_inspect.get_table_names()

        table_name = choice(mysql_tables)
        columns = [column["name"] for column in mysql_inspect.get_columns(table_name)]

        sql = 'INSERT OR IGNORE INTO "{table}" ({fields}) VALUES ({placeholders})'.format(
            table=table_name,
            fields=('"{}", ' * len(columns)).rstrip(" ,").format(*columns),
            placeholders=("?, " * len(columns)).rstrip(" ,"),
        )

        mocker.patch.object(proc, "_mysql_cur", FakeMySQLCursor())

        with pytest.raises((mysql.connector.Error, sqlite3.Error)):
            proc._transfer_table_data(table_name, sql)