import pickle
import time
from django.test.runner import DiscoverRunner, ParallelTestSuite, RemoteTestResult, RemoteTestRunner


class _Py313SafeRemoteTestResult(RemoteTestResult):
    """Prevents Python 3.13 'cannot pickle traceback' crash in parallel workers.

    In Python 3.13, traceback objects are not picklable. Django's parallel
    runner serialises test errors via multiprocessing (pickle) to send them
    from worker subprocesses back to the main process. When a Selenium test
    crashes the browser, the resulting exception traceback fails to pickle,
    aborting the entire parallel run with a confusing TypeError.

    Fix: strip the traceback (replace with None) before it reaches pickle.
    Error type and message are preserved; only the traceback is lost in transit.

    Note: the fix must be on RemoteTestResult (not RemoteTestRunner) because
    addError/addFailure — where check_picklable is called — live on the result
    object, not the runner. RemoteTestRunner.run() instantiates resultclass()
    and passes it to the test suite; overriding it on the runner has no effect.
    """

    @staticmethod
    def _safe_err(err):
        exc_type, exc_value, exc_tb = err
        try:
            pickle.dumps((exc_type, exc_value, exc_tb))
        except (TypeError, AttributeError):
            return (exc_type, exc_value, None)
        return err

    def addError(self, test, err):
        super().addError(test, self._safe_err(err))

    def addFailure(self, test, err):
        super().addFailure(test, self._safe_err(err))


class _Py313SafeRemoteTestRunner(RemoteTestRunner):
    resultclass = _Py313SafeRemoteTestResult


class _SafeParallelTestSuite(ParallelTestSuite):
    runner_class = _Py313SafeRemoteTestRunner


class RobustCompressorTestRunner(DiscoverRunner):
    parallel_test_suite = _SafeParallelTestSuite

    def setup_test_environment(self, **kwargs):
        super().setup_test_environment(**kwargs)
        from compressor.storage import CompressorFileStorage
        _orig_save = CompressorFileStorage.save
        def _robust_save(self, name, content):
            for _ in range(5):
                try: return _orig_save(self, name, content)
                except PermissionError: time.sleep(0.05)
            raise
        CompressorFileStorage.save = _robust_save

    def teardown_databases(self, old_config, **kwargs):
        """Force-close any lingering DB connections before Django issues
        `DROP DATABASE`. Without this, CI fails post-test w. Postgres'
        `ObjectInUse: database "..." is being accessed by other users`
        even on a fully-green run.

        Root cause: `core.settings.DATABASES['default']['conn_max_age']
        = 600` keeps Postgres connections alive in the per-thread pool
        for 10 min. Channels' `database_sync_to_async` (used in
        `apps.epic.tests.integrated.test_consumers`'s `CursorMoveConsumerTest`
        + `SigHoverConsumerTest`) runs in a global asgiref threadpool;
        each worker thread accumulates its own DB connection that
        outlives the test + sits idle in the pool when teardown fires.
        Postgres refuses the drop while ANY session targets the row.

        Two-step fix — `connections.close_all()` covers the main
        thread (test runner's own connection); `pg_terminate_backend`
        targets any worker-thread session still pinning the test DB.
        Both are safe on a green test run: the DB is about to be
        dropped anyway, and prod's `CONN_MAX_AGE=600` stays untouched
        (the fix lives in the test runner, not in `settings.py`).

        Local dev (SQLite, no DROP step) is unaffected; CI Postgres
        is where the leak was visible. See [[feedback-test-teardown-conn-leak]].
        """
        from django.db import connections
        connections.close_all()
        for connection, _, _ in old_config:
            if connection.vendor == "postgresql":
                test_db_name = connection.settings_dict["NAME"]
                with connection._nodb_cursor() as cursor:
                    cursor.execute(
                        "SELECT pg_terminate_backend(pid) "
                        "FROM pg_stat_activity "
                        "WHERE datname = %s AND pid <> pg_backend_pid()",
                        [test_db_name],
                    )
        super().teardown_databases(old_config, **kwargs)
