"""Tests for sluice.init() — the only public API for V0."""

from __future__ import annotations

import logging
import threading

import pytest

import sluice
from sluice import SluiceConfigError

# Shared test connection ID (valid UUID format).
_TEST_CONN_ID = "550e8400-e29b-41d4-a716-446655440000"


class TestInit:
    """Tests for the init() entry point."""

    def test_init_with_valid_api_key_and_connection_id(self) -> None:
        """init() succeeds with a valid API key and connection ID."""
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)

    def test_init_sets_initialized_flag(self) -> None:
        """init() marks the SDK as initialized."""
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert sluice._init._initialized is True

    def test_init_stores_api_key(self) -> None:
        """init() stores the stripped API key."""
        sluice.init(api_key="  sk_test_abc123  ", connection_id=_TEST_CONN_ID)
        assert sluice._init._api_key == "sk_test_abc123"

    def test_init_stores_connection_id(self) -> None:
        """init() stores the stripped connection ID."""
        sluice.init(
            api_key="sk_test_abc123",
            connection_id=f"  {_TEST_CONN_ID}  ",
        )
        assert sluice._init._connection_id == _TEST_CONN_ID

    def test_init_empty_api_key_raises(self) -> None:
        """init() raises SluiceConfigError when no key is provided anywhere."""
        with pytest.raises(SluiceConfigError, match="API key is empty"):
            sluice.init(api_key="", connection_id=_TEST_CONN_ID)

    def test_init_whitespace_api_key_raises(self) -> None:
        """init() raises SluiceConfigError for whitespace-only api_key."""
        with pytest.raises(SluiceConfigError, match="API key is empty"):
            sluice.init(api_key="   ", connection_id=_TEST_CONN_ID)

    def test_init_none_api_key_no_env_raises(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """init() raises when api_key is None and env var is unset."""
        monkeypatch.delenv("SLUICE_API_KEY", raising=False)
        monkeypatch.delenv("SLUICE_CONNECTION_ID", raising=False)
        with pytest.raises(SluiceConfigError, match="API key is empty"):
            sluice.init()

    def test_init_error_message_includes_troubleshooting_url(self) -> None:
        """Error messages follow Sluice format with troubleshooting URL."""
        with pytest.raises(
            SluiceConfigError,
            match=r"https://docs\.sluice\.sh/troubleshooting/api-key",
        ):
            sluice.init(api_key="", connection_id=_TEST_CONN_ID)

    def test_init_error_message_mentions_env_var(self) -> None:
        """Error message tells user about the SLUICE_API_KEY env var."""
        with pytest.raises(SluiceConfigError, match="SLUICE_API_KEY"):
            sluice.init(api_key="", connection_id=_TEST_CONN_ID)

    def test_init_double_call_warns(self, caplog: pytest.LogCaptureFixture) -> None:
        """Calling init() twice logs a warning and is a no-op."""
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        with caplog.at_level(logging.WARNING, logger="sluice"):
            sluice.init(api_key="sk_test_different_key", connection_id=_TEST_CONN_ID)
        assert "called multiple times" in caplog.text

    def test_init_logs_success(self, caplog: pytest.LogCaptureFixture) -> None:
        """init() logs a success message on first call."""
        with caplog.at_level(logging.INFO, logger="sluice"):
            sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert "initialized successfully" in caplog.text

    def test_init_connects_celeryd_init_signal(self) -> None:
        """init() should connect the celeryd_init signal handler with dispatch_uid."""
        from celery.signals import celeryd_init

        # Before init, no sluice receivers should exist (_reset clears them).
        sluice_receivers_before = [
            r for r in celeryd_init.receivers if r[0][0] == "sluice_celeryd_init"
        ]
        assert len(sluice_receivers_before) == 0

        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)

        # After init, exactly one sluice receiver should exist.
        sluice_receivers_after = [
            r for r in celeryd_init.receivers if r[0][0] == "sluice_celeryd_init"
        ]
        assert len(sluice_receivers_after) == 1

    def test_init_detects_standalone_celery(
        self,
        monkeypatch: pytest.MonkeyPatch,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """init() should detect standalone Celery (no Django)."""
        monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
        with caplog.at_level(logging.INFO, logger="sluice"):
            sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert "Standalone Celery detected" in caplog.text

    def test_init_detects_django(
        self,
        monkeypatch: pytest.MonkeyPatch,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """init() should detect Django when DJANGO_SETTINGS_MODULE is set."""
        monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "myproject.settings")
        with caplog.at_level(logging.INFO, logger="sluice"):
            sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert "Django project detected" in caplog.text

    def test_init_never_crashes_on_integration_failure(
        self,
        monkeypatch: pytest.MonkeyPatch,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """init() should log but never raise if integration setup fails."""

        # Monkey-patch _setup_celery_integration to raise.
        def _broken_setup(api_key: str, connection_id: str) -> None:
            raise RuntimeError("simulated integration failure")

        monkeypatch.setattr(sluice._init, "_setup_celery_integration", _broken_setup)

        with caplog.at_level(logging.ERROR, logger="sluice"):
            # Should NOT raise.
            sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)

        assert "Failed to set up Celery integration" in caplog.text
        # SDK should NOT be marked as initialized — allows retry.
        assert sluice._init._initialized is False

    def test_init_retryable_after_integration_failure(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """init() can be called again after an integration failure."""
        call_count = 0

        def _broken_then_ok(api_key: str, connection_id: str) -> None:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("first call fails")

        monkeypatch.setattr(sluice._init, "_setup_celery_integration", _broken_then_ok)

        # First call fails but doesn't raise.
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert sluice._init._initialized is False

        # Second call succeeds.
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert sluice._init._initialized is True


class TestConnectionIdValidation:
    """Tests for connection_id parameter validation."""

    def test_empty_connection_id_raises(self) -> None:
        """init() raises SluiceConfigError when connection_id is empty."""
        with pytest.raises(SluiceConfigError, match="Connection ID is empty"):
            sluice.init(api_key="sk_test_abc123", connection_id="")

    def test_whitespace_connection_id_raises(self) -> None:
        """init() raises for whitespace-only connection_id."""
        with pytest.raises(SluiceConfigError, match="Connection ID is empty"):
            sluice.init(api_key="sk_test_abc123", connection_id="   ")

    def test_none_connection_id_no_env_raises(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """init() raises when connection_id is None and env var is unset."""
        monkeypatch.delenv("SLUICE_CONNECTION_ID", raising=False)
        with pytest.raises(SluiceConfigError, match="Connection ID is empty"):
            sluice.init(api_key="sk_test_abc123")

    def test_connection_id_error_includes_troubleshooting_url(self) -> None:
        """Error messages include connection-id troubleshooting URL."""
        with pytest.raises(
            SluiceConfigError,
            match=r"https://docs\.sluice\.sh/troubleshooting/connection-id",
        ):
            sluice.init(api_key="sk_test_abc123", connection_id="")

    def test_connection_id_error_mentions_env_var(self) -> None:
        """Error message tells user about the SLUICE_CONNECTION_ID env var."""
        with pytest.raises(SluiceConfigError, match="SLUICE_CONNECTION_ID"):
            sluice.init(api_key="sk_test_abc123", connection_id="")

    def test_non_uuid_connection_id_raises(self) -> None:
        """Non-UUID connection_id should raise at config time."""
        with pytest.raises(SluiceConfigError, match="not a valid UUID"):
            sluice.init(
                api_key="sk_test_abc123",
                connection_id="my-project",
            )

    def test_uuid_connection_id_accepted(self) -> None:
        """Valid UUID connection_id should be accepted."""
        sluice.init(
            api_key="sk_test_abc123",
            connection_id="550e8400-e29b-41d4-a716-446655440000",
        )
        assert sluice._init._initialized is True


class TestEnvVarFallback:
    """Tests for SLUICE_API_KEY and SLUICE_CONNECTION_ID env var fallback."""

    def test_init_reads_api_key_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """init() uses SLUICE_API_KEY env var when no api_key is passed."""
        monkeypatch.setenv("SLUICE_API_KEY", "sk_test_from_env")
        sluice.init(connection_id=_TEST_CONN_ID)
        assert sluice._init._initialized is True

    def test_init_reads_connection_id_env_var(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """init() uses SLUICE_CONNECTION_ID env var when no arg is passed."""
        monkeypatch.setenv("SLUICE_CONNECTION_ID", _TEST_CONN_ID)
        sluice.init(api_key="sk_test_abc123")
        assert sluice._init._connection_id == _TEST_CONN_ID

    def test_init_reads_both_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """init() reads both env vars when no args are passed."""
        monkeypatch.setenv("SLUICE_API_KEY", "sk_test_from_env")
        monkeypatch.setenv("SLUICE_CONNECTION_ID", _TEST_CONN_ID)
        sluice.init()
        assert sluice._init._initialized is True
        assert sluice._init._connection_id == _TEST_CONN_ID

    def test_init_explicit_key_overrides_env(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Explicit api_key= takes precedence over env var."""
        monkeypatch.setenv("SLUICE_API_KEY", "sk_test_from_env")
        sluice.init(api_key="sk_test_explicit", connection_id=_TEST_CONN_ID)
        assert sluice._init._api_key == "sk_test_explicit"

    def test_init_explicit_connection_id_overrides_env(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Explicit connection_id= takes precedence over env var."""
        env_id = "00000000-0000-0000-0000-000000000000"
        monkeypatch.setenv("SLUICE_CONNECTION_ID", env_id)
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert sluice._init._connection_id == _TEST_CONN_ID

    def test_init_empty_env_var_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """init() raises if env var is set but empty."""
        monkeypatch.setenv("SLUICE_API_KEY", "")
        with pytest.raises(SluiceConfigError, match="API key is empty"):
            sluice.init(connection_id=_TEST_CONN_ID)


class TestPublicAPI:
    """Verify the public API surface is exactly what we intend."""

    def test_init_is_exported(self) -> None:
        """init() is accessible from the top-level package."""
        assert hasattr(sluice, "init")
        assert callable(sluice.init)

    def test_config_error_is_exported(self) -> None:
        """SluiceConfigError is accessible from the top-level package."""
        assert hasattr(sluice, "SluiceConfigError")
        assert issubclass(sluice.SluiceConfigError, Exception)

    def test_all_exports_are_intentional(self) -> None:
        """__all__ contains exactly the intended public API."""
        assert set(sluice.__all__) == {"init", "SluiceConfigError"}


class TestInstallViaDjango:
    """Tests for the _install_via_django() internal function."""

    def test_install_via_django_exception_is_not_fatal(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """_install_via_django() should log and continue if installation fails."""
        from sluice._init import _install_via_django

        # Make current_app._get_current_object() raise.
        def _broken_get_current(*args: object, **kwargs: object) -> None:
            raise RuntimeError("simulated Django failure")

        import celery

        monkeypatch.setattr(
            celery.current_app, "_get_current_object", _broken_get_current
        )

        # Should NOT raise — just logs debug message.
        _install_via_django("sk_test_123", _TEST_CONN_ID)

    def test_install_via_django_installs_bootstep(self) -> None:
        """_install_via_django() should install bootstep when app is available."""
        from sluice._bootstep import SluiceBootstep
        from sluice._init import _install_via_django

        _install_via_django("sk_test_django", _TEST_CONN_ID)

        # The bootstep should have been installed on the current app.
        from celery import current_app

        app = current_app._get_current_object()
        assert SluiceBootstep in app.steps["consumer"]


class TestCelerydInitSignalHandler:
    """Tests for the _on_celeryd_init signal handler wired by init()."""

    def test_signal_handler_installs_bootstep_on_sender(self) -> None:
        """The celeryd_init signal handler should install bootstep on the app."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_signal", connection_id=_TEST_CONN_ID)

        # Manually fire the signal with a sender to exercise the handler body.
        # The handler calls install_bootstep on the current_app proxy.
        # No exception means it ran successfully.
        celeryd_init.send(sender="test_worker@host")

    def test_signal_handler_logs_worker_configured(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """The celeryd_init handler should log that the worker is configured."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_signal", connection_id=_TEST_CONN_ID)

        with caplog.at_level(logging.INFO, logger="sluice"):
            celeryd_init.send(sender="test_worker@host")

        assert "configured for monitoring" in caplog.text

    def test_signal_handler_ignores_none_sender(self) -> None:
        """The celeryd_init handler should return early when sender is None."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_signal", connection_id=_TEST_CONN_ID)

        # Should not raise — returns early on None sender.
        celeryd_init.send(sender=None)

    def test_signal_handler_exception_never_crashes(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """The signal handler must never crash, even if install_bootstep fails."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_signal", connection_id=_TEST_CONN_ID)

        # Make current_app._get_current_object raise to trigger the exception path.
        import celery

        def _broken(*args: object, **kwargs: object) -> None:
            raise RuntimeError("simulated signal handler failure")

        monkeypatch.setattr(celery.current_app, "_get_current_object", _broken)

        # Should NOT raise — exception is caught and logged.
        celeryd_init.send(sender="test_worker@host")

    def test_signal_handler_none_app_returns_early(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """The celeryd_init handler should return early when app is None."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_signal", connection_id=_TEST_CONN_ID)

        import celery

        monkeypatch.setattr(celery.current_app, "_get_current_object", lambda: None)

        # Should not raise — returns early on None app.
        celeryd_init.send(sender="test_worker@host")


class TestConcurrentInit:
    """Tests for thread-safety of init()."""

    def test_concurrent_init_only_initializes_once(self) -> None:
        """init() called from multiple threads should only set up once."""
        errors: list[Exception] = []
        success_count = 0
        count_lock = threading.Lock()

        def call_init() -> None:
            nonlocal success_count
            try:
                sluice.init(
                    api_key="sk_test_concurrent",
                    connection_id=_TEST_CONN_ID,
                )
                with count_lock:
                    success_count += 1
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=call_init) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        assert sluice._init._initialized is True

        # Only one signal handler should be connected (dispatch_uid ensures this).
        from celery.signals import celeryd_init

        sluice_handlers = [
            r for r in celeryd_init.receivers if r[0][0] == "sluice_celeryd_init"
        ]
        assert len(sluice_handlers) == 1


class TestReset:
    """Tests for the _reset() helper used in testing."""

    def test_reset_clears_state(self) -> None:
        """_reset() should clear _initialized, _api_key, and _connection_id."""
        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)
        assert sluice._init._initialized is True
        assert sluice._init._api_key == "sk_test_abc123"
        assert sluice._init._connection_id == _TEST_CONN_ID

        sluice._init._reset()

        assert sluice._init._initialized is False
        assert sluice._init._api_key == ""
        assert sluice._init._connection_id == ""

    def test_reset_disconnects_signal_handler(self) -> None:
        """_reset() should disconnect the celeryd_init signal handler."""
        from celery.signals import celeryd_init

        sluice.init(api_key="sk_test_abc123", connection_id=_TEST_CONN_ID)

        # Signal should be connected.
        sluice_receivers = [
            r for r in celeryd_init.receivers if r[0][0] == "sluice_celeryd_init"
        ]
        assert len(sluice_receivers) == 1

        sluice._init._reset()

        # Signal should be disconnected.
        sluice_receivers_after = [
            r for r in celeryd_init.receivers if r[0][0] == "sluice_celeryd_init"
        ]
        assert len(sluice_receivers_after) == 0
