"""
Unit tests for Redis key namespacing.

Tests TNNT-03: Per-tenant Redis namespace isolation for cache and session state.

Every key function must:
1. Prepend {tenant_id}: to every key
2. Never be callable without a tenant_id argument
3. Produce predictable, stable key formats
"""

from __future__ import annotations

import inspect

import pytest

from shared.redis_keys import (
    engaged_thread_key,
    idempotency_key,
    rate_limit_key,
    session_key,
)


class TestRedisKeyFormats:
    """Tests for correct key format from all key constructor functions."""

    def test_rate_limit_key_format(self) -> None:
        """rate_limit_key must return '{tenant_id}:ratelimit:{channel}'."""
        key = rate_limit_key("tenant-a", "slack")
        assert key == "tenant-a:ratelimit:slack"

    def test_rate_limit_key_different_channel(self) -> None:
        """rate_limit_key must work for any channel type string."""
        key = rate_limit_key("tenant-a", "whatsapp")
        assert key == "tenant-a:ratelimit:whatsapp"

    def test_idempotency_key_format(self) -> None:
        """idempotency_key must return '{tenant_id}:dedup:{message_id}'."""
        key = idempotency_key("tenant-a", "msg-123")
        assert key == "tenant-a:dedup:msg-123"

    def test_session_key_format(self) -> None:
        """session_key must return '{tenant_id}:session:{thread_id}'."""
        key = session_key("tenant-a", "thread-456")
        assert key == "tenant-a:session:thread-456"

    def test_engaged_thread_key_format(self) -> None:
        """engaged_thread_key must return '{tenant_id}:engaged:{thread_id}'."""
        key = engaged_thread_key("tenant-a", "T12345")
        assert key == "tenant-a:engaged:T12345"


class TestTenantIsolation:
    """Tests that all key functions produce distinct namespaces per tenant."""

    def test_rate_limit_keys_are_tenant_scoped(self) -> None:
        """Two tenants with the same channel must produce different keys."""
        key_a = rate_limit_key("tenant-a", "slack")
        key_b = rate_limit_key("tenant-b", "slack")
        assert key_a != key_b
        assert key_a.startswith("tenant-a:")
        assert key_b.startswith("tenant-b:")

    def test_idempotency_keys_are_tenant_scoped(self) -> None:
        """Two tenants with the same message_id must produce different keys."""
        key_a = idempotency_key("tenant-a", "msg-999")
        key_b = idempotency_key("tenant-b", "msg-999")
        assert key_a != key_b
        assert key_a.startswith("tenant-a:")
        assert key_b.startswith("tenant-b:")

    def test_session_keys_are_tenant_scoped(self) -> None:
        """Two tenants with the same thread_id must produce different keys."""
        key_a = session_key("tenant-a", "thread-1")
        key_b = session_key("tenant-b", "thread-1")
        assert key_a != key_b

    def test_engaged_thread_keys_are_tenant_scoped(self) -> None:
        """Two tenants with same thread must produce different engaged keys."""
        key_a = engaged_thread_key("tenant-a", "thread-1")
        key_b = engaged_thread_key("tenant-b", "thread-1")
        assert key_a != key_b

    def test_all_keys_include_tenant_id_prefix(self) -> None:
        """Every key function must produce a key starting with the tenant_id."""
        tenant_id = "my-tenant-uuid"
        keys = [
            rate_limit_key(tenant_id, "slack"),
            idempotency_key(tenant_id, "msg-1"),
            session_key(tenant_id, "thread-1"),
            engaged_thread_key(tenant_id, "thread-1"),
        ]
        for key in keys:
            assert key.startswith(f"{tenant_id}:"), (
                f"Key {key!r} does not start with tenant_id prefix '{tenant_id}:'"
            )


class TestNoBarKeysIsPossible:
    """Tests that prove no key function can be called without tenant_id."""

    def test_rate_limit_key_requires_tenant_id(self) -> None:
        """rate_limit_key signature requires tenant_id as first argument."""
        sig = inspect.signature(rate_limit_key)
        params = list(sig.parameters.keys())
        assert params[0] == "tenant_id"

    def test_idempotency_key_requires_tenant_id(self) -> None:
        """idempotency_key signature requires tenant_id as first argument."""
        sig = inspect.signature(idempotency_key)
        params = list(sig.parameters.keys())
        assert params[0] == "tenant_id"

    def test_session_key_requires_tenant_id(self) -> None:
        """session_key signature requires tenant_id as first argument."""
        sig = inspect.signature(session_key)
        params = list(sig.parameters.keys())
        assert params[0] == "tenant_id"

    def test_engaged_thread_key_requires_tenant_id(self) -> None:
        """engaged_thread_key signature requires tenant_id as first argument."""
        sig = inspect.signature(engaged_thread_key)
        params = list(sig.parameters.keys())
        assert params[0] == "tenant_id"

    def test_calling_without_tenant_id_raises_type_error(self) -> None:
        """Calling any key function with zero args must raise TypeError."""
        with pytest.raises(TypeError):
            rate_limit_key()  # type: ignore[call-arg]

        with pytest.raises(TypeError):
            idempotency_key()  # type: ignore[call-arg]

        with pytest.raises(TypeError):
            session_key()  # type: ignore[call-arg]

        with pytest.raises(TypeError):
            engaged_thread_key()  # type: ignore[call-arg]
