"""Tests for key rotation handler (F4)."""

from __future__ import annotations

import os
import signal
import tempfile
import time
from unittest.mock import MagicMock, patch


from qremotex.connection.auth import AuthManager
from qremotex.connection.key_rotation import KeyRotationHandler
from qremotex.config import ExecutorConfig


def _make_config(api_key: str = "qrx_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6") -> ExecutorConfig:
    """Create a minimal ExecutorConfig for testing."""
    with patch.dict(os.environ, {
        "QRAPTOR_GATEWAY_URL": "wss://gw.example.com/ws",
        "QRAPTOR_API_KEY": api_key,
    }):
        return ExecutorConfig()


def _make_handler(
    api_key: str = "qrx_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    on_key_rotated: MagicMock | None = None,
) -> tuple[KeyRotationHandler, ExecutorConfig]:
    config = _make_config(api_key)
    auth = AuthManager(config)
    handler = KeyRotationHandler(config, auth, on_key_rotated)
    return handler, config


# ──────────────────────────────────────────────── Init & Properties ──


class TestKeyRotationInit:
    def test_initial_state(self) -> None:
        handler, _ = _make_handler()
        assert handler.pending_rotation is False
        assert handler.in_grace_period is False
        assert handler.rotation_count == 0

    def test_key_file_from_env(self) -> None:
        with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": "/tmp/test_key"}):
            handler, _ = _make_handler()
            assert handler._key_file == "/tmp/test_key"

    def test_no_key_file(self) -> None:
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("QRAPTOR_API_KEY_FILE", None)
            handler, _ = _make_handler()
            assert handler._key_file is None


# ──────────────────────────────────────────────── Rotation Notice ──


class TestRotationNotice:
    def test_handle_notice_sets_pending(self) -> None:
        handler, _ = _make_handler()
        result = handler.handle_rotation_notice({
            "key_id": "ak_newkey123",
            "reason": "manual_rotation",
        })
        assert result["ok"] is True
        assert handler.pending_rotation is True

    def test_handle_notice_with_grace_period(self) -> None:
        handler, _ = _make_handler()
        future = time.time() + 86400  # 24h from now
        from datetime import datetime, timezone
        grace_str = datetime.fromtimestamp(future, tz=timezone.utc).isoformat()

        handler.handle_rotation_notice({
            "key_id": "ak_newkey123",
            "reason": "manual_rotation",
            "grace_period_until": grace_str,
        })
        assert handler.in_grace_period is True

    def test_handle_notice_expired_grace(self) -> None:
        handler, _ = _make_handler()
        # Grace period in the past
        handler.handle_rotation_notice({
            "key_id": "ak_newkey123",
            "grace_period_until": "2020-01-01T00:00:00Z",
        })
        assert handler.in_grace_period is False

    def test_handle_notice_with_key_file_auto_reload(self) -> None:
        new_key = "qrx_prod_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"
        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(new_key)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                callback = MagicMock()
                handler, config = _make_handler(on_key_rotated=callback)
                result = handler.handle_rotation_notice({
                    "key_id": "ak_newkey123",
                    "reason": "manual_rotation",
                })
                assert result["action"] == "auto_reloaded"
                assert config.api_key == new_key
                callback.assert_called_once()
        finally:
            os.unlink(key_file)

    def test_handle_notice_without_key_file(self) -> None:
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("QRAPTOR_API_KEY_FILE", None)
            handler, _ = _make_handler()
            result = handler.handle_rotation_notice({
                "key_id": "ak_newkey123",
                "reason": "security",
            })
            assert result["action"] == "pending_manual_update"

    def test_handle_notice_bad_grace_format_defaults_24h(self) -> None:
        handler, _ = _make_handler()
        handler.handle_rotation_notice({
            "key_id": "ak_newkey123",
            "grace_period_until": "not-a-date",
        })
        # Should default to 24h grace
        assert handler.in_grace_period is True


# ──────────────────────────────────────────────── Key Reload ──


class TestKeyReload:
    def test_reload_from_file(self) -> None:
        old_key = "qrx_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
        new_key = "qrx_prod_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"

        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(new_key)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                callback = MagicMock()
                handler, config = _make_handler(api_key=old_key, on_key_rotated=callback)

                # Trigger pending
                handler._pending_rotation = True
                result = handler.try_reload()

                assert result is True
                assert config.api_key == new_key
                assert handler.pending_rotation is False
                assert handler.rotation_count == 1
                callback.assert_called_once()
        finally:
            os.unlink(key_file)

    def test_reload_no_pending(self) -> None:
        handler, _ = _make_handler()
        assert handler.try_reload() is False

    def test_reload_same_key_no_change(self) -> None:
        key = "qrx_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(key)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                handler, config = _make_handler(api_key=key)
                handler._pending_rotation = True
                result = handler.try_reload()
                # Same key — no rotation
                assert result is False
                assert handler.rotation_count == 0
        finally:
            os.unlink(key_file)

    def test_reload_empty_file(self) -> None:
        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write("")
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                handler, _ = _make_handler()
                handler._pending_rotation = True
                assert handler.try_reload() is False
        finally:
            os.unlink(key_file)

    def test_reload_missing_file(self) -> None:
        with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": "/nonexistent/key.file"}):
            handler, _ = _make_handler()
            handler._pending_rotation = True
            assert handler.try_reload() is False

    def test_reload_no_key_file_configured(self) -> None:
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("QRAPTOR_API_KEY_FILE", None)
            handler, _ = _make_handler()
            handler._pending_rotation = True
            assert handler.try_reload() is False

    def test_multiple_rotations(self) -> None:
        key1 = "qrx_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
        key2 = "qrx_prod_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"
        key3 = "qrx_prod_m1n2o3p4q5r6s7t8u9v0w1x2y3z4a5b6"

        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(key2)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                handler, config = _make_handler(api_key=key1)

                # First rotation
                handler._pending_rotation = True
                handler.try_reload()
                assert config.api_key == key2
                assert handler.rotation_count == 1

                # Write third key
                with open(key_file, "w") as kf:
                    kf.write(key3)

                # Second rotation
                handler._pending_rotation = True
                handler.try_reload()
                assert config.api_key == key3
                assert handler.rotation_count == 2
        finally:
            os.unlink(key_file)


# ──────────────────────────────────────────────── Grace Period ──


class TestGracePeriod:
    def test_no_grace_period(self) -> None:
        handler, _ = _make_handler()
        assert handler.check_grace_expiry() is False

    def test_grace_not_expired(self) -> None:
        handler, _ = _make_handler()
        handler._grace_deadline = time.time() + 86400
        assert handler.check_grace_expiry() is False

    def test_grace_expired(self) -> None:
        handler, _ = _make_handler()
        handler._grace_deadline = time.time() - 10
        assert handler.check_grace_expiry() is True

    def test_grace_cleared_after_rotation(self) -> None:
        new_key = "qrx_prod_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"
        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(new_key)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                handler, _ = _make_handler()
                handler._grace_deadline = time.time() + 86400
                handler._pending_rotation = True
                handler.try_reload()
                # Grace should be cleared after successful rotation
                assert handler.in_grace_period is False
        finally:
            os.unlink(key_file)


# ──────────────────────────────────────────────── SIGHUP ──


class TestSighupHandler:
    def test_sighup_sets_pending(self) -> None:
        handler, _ = _make_handler()
        # Simulate SIGHUP
        handler._sighup_handler(signal.SIGHUP, None)
        assert handler.pending_rotation is True

    def test_register_signal_handler(self) -> None:
        handler, _ = _make_handler()
        # Should not raise (SIGHUP available on macOS/Linux)
        handler.register_signal_handler()


# ──────────────────────────────────────────────── Auth Manager Access ──


class TestAuthManagerAccess:
    def test_auth_manager_property(self) -> None:
        handler, _ = _make_handler()
        auth = handler.auth_manager
        assert isinstance(auth, AuthManager)

    def test_auth_manager_updated_after_rotation(self) -> None:
        new_key = "qrx_prod_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"
        with tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as f:
            f.write(new_key)
            key_file = f.name

        try:
            with patch.dict(os.environ, {"QRAPTOR_API_KEY_FILE": key_file}):
                handler, _ = _make_handler()
                old_auth = handler.auth_manager
                handler._pending_rotation = True
                handler.try_reload()
                new_auth = handler.auth_manager
                assert old_auth is not new_auth
        finally:
            os.unlink(key_file)
