"""Tests for the fire-and-forget background dispatch helper."""

import threading
import time

import pytest

import apps.core.background as background_module
from apps.core.background import run_in_background


def _settled_baseline(timeout: float = 5.0) -> int:
    """`_pending_count` once it stops moving, or the last value seen.

    The counter is a module-level singleton. Reading it while a job submitted by
    an earlier test is still finishing gives a baseline that then drifts
    downward, so a strict `delta == 2` assertion fails intermittently under
    full-suite load (it passes in isolation, where nothing else is in flight).
    Waiting for two consecutive equal reads removes the race without weakening
    what the test asserts.
    """
    deadline = time.monotonic() + timeout
    previous = background_module._pending_count
    while time.monotonic() < deadline:
        time.sleep(0.05)
        current = background_module._pending_count
        if current == previous:
            return current
        previous = current
    return previous


class TestEagerMode:
    @pytest.fixture(autouse=True)
    def eager(self, settings):
        settings.BACKGROUND_TASKS_EAGER = True

    def test_runs_inline(self):
        calls = []
        run_in_background(calls.append, "ran")
        assert calls == ["ran"]

    def test_swallows_exceptions(self):
        def boom():
            raise RuntimeError("nope")
        run_in_background(boom)  # must not raise

    def test_passes_kwargs(self):
        calls = {}
        run_in_background(calls.update, key="value")
        assert calls == {"key": "value"}


class TestThreadedMode:
    @pytest.mark.django_db
    def test_runs_on_background_thread(self):
        done = threading.Event()
        seen = {}

        def task(value):
            seen["value"] = value
            seen["thread"] = threading.current_thread().name
            done.set()

        run_in_background(task, 42)
        assert done.wait(timeout=5), "background task never ran"
        assert seen["value"] == 42
        assert seen["thread"].startswith("khub-bg")

    @pytest.mark.django_db
    def test_exception_does_not_propagate(self):
        done = threading.Event()

        def boom():
            done.set()
            raise RuntimeError("nope")

        run_in_background(boom)
        assert done.wait(timeout=5)


class TestBoundedQueue:
    """PERF-005: the submission queue must be bounded — a full queue drops
    the task (logged/counted) instead of growing without limit or blocking
    the calling (request) thread.

    `_pending_count` is a module-level singleton shared with any background
    job still in flight from another test in the same session, so these
    tests size BACKGROUND_MAX_QUEUE relative to a captured baseline instead
    of assuming the counter starts at zero."""

    @pytest.mark.django_db
    def test_submission_at_capacity_succeeds(self, settings):
        baseline = _settled_baseline()
        settings.BACKGROUND_MAX_QUEUE = baseline + 2
        release = threading.Event()

        def blocker():
            release.wait(timeout=5)

        run_in_background(blocker)
        run_in_background(blocker)
        # Both submissions accepted (queue capacity allows 2 more than baseline).
        assert background_module._pending_count - baseline == 2
        release.set()

    @pytest.mark.django_db
    def test_submission_above_capacity_is_dropped_not_blocking(self, settings):
        baseline = _settled_baseline()
        settings.BACKGROUND_MAX_QUEUE = baseline + 1
        dropped_before = background_module.background_tasks_dropped_total
        release = threading.Event()

        def blocker():
            release.wait(timeout=5)

        run_in_background(blocker)  # fills the bounded queue (baseline + 1)
        assert background_module._pending_count - baseline == 1

        calls = []
        # This call must return immediately (dropped), never block the caller.
        run_in_background(calls.append, "should be dropped")
        assert calls == []
        assert background_module.background_tasks_dropped_total - dropped_before == 1
        release.set()
