import threading

import pytest
from sqlalchemy import text

from app.core.data_source.errors import DataSourceNotFound
from app.core.data_source.models import (
    DataSourceCredential,
    DataSourceDefinition,
)
from app.core.data_source.pool_registry import PoolRegistry


DATA_SOURCE_UID = "01900000-0000-7000-8000-000000000010"


class FakeDefinitions:
    def __init__(self, definition):
        self.definition = definition
        self.requested_uid = None

    def get(self, uid):
        self.requested_uid = uid
        return self.definition


class FakeCredentials:
    def __init__(self):
        self.requested_uid = None
        self.requested_version = None

    def get_active(self, session, uid, version):
        del session
        self.requested_uid = uid
        self.requested_version = version
        return DataSourceCredential("reader", "secret")


class FakeTransaction:
    def __init__(self):
        self.committed = False
        self.rolled_back = False
        self.commit_error = None

    def commit(self):
        self.committed = True
        if self.commit_error:
            raise self.commit_error

    def rollback(self):
        self.rolled_back = True


class FakeConnection:
    def __init__(self):
        self.transactions = []
        self.executed = []

    def begin(self):
        transaction = FakeTransaction()
        self.transactions.append(transaction)
        return transaction

    def execute(self, statement, *args, **kwargs):
        self.executed.append(str(statement))
        return "result"

    def __enter__(self):
        return self

    def __exit__(self, *_args):
        return False


class FakeEngine:
    def __init__(self):
        self.connection = FakeConnection()
        self.disposed = False

    def connect(self):
        return self.connection

    def dispose(self):
        self.disposed = True


class FakeAdapter:
    database_type = "postgresql"

    def __init__(self, engine):
        self.engine = engine
        self.purposes = []
        self.settings = None

    def create_pooled_engine(self, definition, credential, settings):
        del definition, credential
        self.settings = dict(settings)
        return self.engine

    def configure_transaction(self, connection, purpose):
        self.purposes.append(purpose)
        if purpose != "dataflow_write":
            connection.execute(text("SET TRANSACTION READ ONLY"))


class FakeAdapters:
    def __init__(self, adapter):
        self.adapter = adapter
        self.requested_type = None

    def __call__(self, database_type):
        self.requested_type = database_type
        return self.adapter


def definition():
    return DataSourceDefinition(
        uid=DATA_SOURCE_UID,
        name_en="warehouse",
        database_type="postgresql",
        host="db",
        port=5432,
        database="analytics",
        credential_ref=DATA_SOURCE_UID,
        credential_version=4,
    )


def make_manager(source_definition=None):
    from app.core.data_source.manager import DataSourceConnectionManager

    engine = FakeEngine()
    adapter = FakeAdapter(engine)
    definitions = FakeDefinitions(
        definition() if source_definition is None else source_definition
    )
    credentials = FakeCredentials()
    adapters = FakeAdapters(adapter)
    registry = PoolRegistry({"idle_ttl": 900, "max_idle_pools": 20})
    manager = DataSourceConnectionManager(
        definitions=definitions,
        credentials=credentials,
        platform_session=lambda: object(),
        adapter_resolver=adapters,
        registry=registry,
        settings_resolver=lambda _overrides: {
            "pool_size": 2,
            "max_overflow": 3,
            "pool_timeout": 10,
            "pool_recycle": 1800,
            "query_timeout": 30,
        },
    )
    return manager, engine, definitions, credentials, adapters, registry


def test_manager_resolves_definition_credential_and_adapter():
    manager, engine, definitions, credentials, adapters, registry = (
        make_manager()
    )

    with manager.connect(
        DATA_SOURCE_UID,
        purpose="metadata_preview",
    ) as connection:
        assert connection.execute(text("SELECT 1")) == "result"

    assert definitions.requested_uid == DATA_SOURCE_UID
    assert credentials.requested_version == 4
    assert adapters.requested_type == "postgresql"
    status = registry.snapshot()[0]
    assert status.credential_version == 4
    assert status.query_total == 1
    assert engine.connection.executed == [
        "SET TRANSACTION READ ONLY",
        "SELECT 1",
    ]


def test_read_purpose_rolls_back_and_write_purpose_commits():
    manager, engine, *_ = make_manager()

    with manager.connect(DATA_SOURCE_UID, purpose="metadata_preview"):
        pass
    read_transaction = engine.connection.transactions[-1]
    assert read_transaction.rolled_back is True
    assert read_transaction.committed is False

    with manager.connect(DATA_SOURCE_UID, purpose="dataflow_write"):
        pass
    write_transaction = engine.connection.transactions[-1]
    assert write_transaction.committed is True
    assert write_transaction.rolled_back is False


def test_write_commit_connection_loss_is_reported_as_unknown():
    from sqlalchemy.exc import OperationalError

    from app.core.data_source.errors import DataSourceWriteOutcomeUnknown

    manager, engine, *_ = make_manager()

    with pytest.raises(DataSourceWriteOutcomeUnknown):
        with manager.connect(DATA_SOURCE_UID, purpose="dataflow_write"):
            transaction = engine.connection.transactions[-1]
            transaction.commit_error = OperationalError(
                "COMMIT",
                {},
                RuntimeError("connection lost"),
            )


def test_manager_reports_missing_definition_without_creating_pool():
    manager, _engine, _definitions, _credentials, _adapters, registry = (
        make_manager(source_definition=False)
    )

    with pytest.raises(DataSourceNotFound):
        with manager.connect(DATA_SOURCE_UID, purpose="metadata_preview"):
            pass

    assert registry.snapshot() == []


def test_runtime_constructs_one_manager_and_closes_it():
    from app.core.data_source import runtime

    class ClosableManager:
        def __init__(self):
            self.closed = False

        def close(self):
            self.closed = True

    created = []

    def factory():
        manager = ClosableManager()
        created.append(manager)
        return manager

    runtime.configure_data_source_runtime(factory)
    results = []
    threads = [
        threading.Thread(
            target=lambda: results.append(runtime.get_data_source_manager())
        )
        for _ in range(20)
    ]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join(timeout=2)

    assert len(created) == 1
    assert all(item is created[0] for item in results)

    runtime.close_data_source_runtime()
    assert created[0].closed is True
    runtime.configure_data_source_runtime(None)


def test_gunicorn_worker_exit_closes_datasource_runtime():
    source = open("gunicorn_config.py", encoding="utf-8").read()

    assert "def worker_exit" in source
    assert "close_data_source_runtime" in source
