"""
CRITICAL SECURITY TESTS for API Token plaintext storage.

These tests verify that live/production tokens can NEVER have their
plaintext keys stored in the database, only sandbox/test tokens.
"""
import pytest
from unittest.mock import Mock, patch
from django.core.exceptions import ValidationError
from smoothschedule.platform.api.models import APIToken


class TestAPITokenPlaintextSecurity:
    """
    Test suite to verify that plaintext tokens are NEVER stored for live tokens.

    SECURITY CRITICAL: These tests ensure that production API tokens cannot
    accidentally leak by being stored in plaintext.

    NOTE: Uses mocks to avoid database overhead - these tests verify
    the validation logic in APIToken.clean(), not ORM behavior.
    """

    def setup_method(self):
        """Set up mock tenant and user for each test."""
        # Mock tenant
        self.tenant = Mock()
        self.tenant.id = 1
        self.tenant.schema_name = 'test_security'
        self.tenant.name = 'Test Security Tenant'

        # Mock user
        self.user = Mock()
        self.user.id = 1
        self.user.username = 'testuser'
        self.user.email = 'test@example.com'

    def test_sandbox_token_can_store_plaintext(self):
        """
        Sandbox tokens SHOULD be allowed to store plaintext keys.
        This is safe because they only work with test data.
        """
        # Generate a sandbox token
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=True)

        # Verify it's a test token
        assert full_key.startswith('ss_test_')

        # Create token with plaintext - should succeed validation
        # Use _id fields to bypass foreign key validation
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Test Sandbox Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=True,
            plaintext_key=full_key  # ALLOWED for sandbox tokens
        )

        # Should not raise ValidationError
        token.clean()

        # Verify the token properties
        assert token.plaintext_key == full_key
        assert token.is_sandbox is True

    def test_live_token_cannot_store_plaintext(self):
        """
        SECURITY TEST: Live tokens must NEVER store plaintext keys.
        This test verifies the model validation prevents this.
        """
        # Generate a live token
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)

        # Verify it's a live token
        assert full_key.startswith('ss_live_')

        # Try to create token with plaintext - should FAIL validation
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Test Live Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=False,
            plaintext_key=full_key  # NOT ALLOWED for live tokens
        )

        # Validation should raise ValidationError
        with pytest.raises(ValidationError) as exc_info:
            token.clean()

        # Verify the error message mentions security violation
        error_dict = exc_info.value.message_dict
        assert 'plaintext_key' in error_dict
        assert 'SECURITY VIOLATION' in str(error_dict['plaintext_key'][0])
        assert 'live/production tokens' in str(error_dict['plaintext_key'][0])

    def test_cannot_store_ss_live_in_plaintext(self):
        """
        SECURITY TEST: Even for sandbox tokens, we should never accept
        a plaintext key that starts with ss_live_*.

        This is a belt-and-suspenders check to catch bugs in token generation.
        """
        # Generate a live token (to get the ss_live_* format)
        live_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)

        # Try to create a token marked as sandbox but with a live key plaintext
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Malicious Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=True,  # Marked as sandbox
            plaintext_key=live_key  # But trying to store ss_live_* plaintext
        )

        # Validation should raise ValidationError
        with pytest.raises(ValidationError) as exc_info:
            token.clean()

        # Verify the error mentions ss_live_
        error_dict = exc_info.value.message_dict
        assert 'plaintext_key' in error_dict
        assert 'ss_live_' in str(error_dict['plaintext_key'][0])
        assert 'SECURITY VIOLATION' in str(error_dict['plaintext_key'][0])

    def test_plaintext_must_start_with_ss_test(self):
        """
        SECURITY TEST: Any plaintext key must start with ss_test_*.
        Invalid formats should be rejected.
        """
        _, key_hash, key_prefix = APIToken.generate_key(is_sandbox=True)

        # Try with an invalid plaintext format
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Invalid Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=True,
            plaintext_key='invalid_format_123456789'  # Wrong format
        )

        # Validation should raise ValidationError
        with pytest.raises(ValidationError) as exc_info:
            token.clean()

        # Verify the error
        error_dict = exc_info.value.message_dict
        assert 'plaintext_key' in error_dict
        assert 'ss_test_' in str(error_dict['plaintext_key'][0])

    def test_live_token_without_plaintext_succeeds(self):
        """
        Live tokens WITHOUT plaintext should pass validation successfully.
        This is the normal, secure operation.
        """
        # Generate a live token
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)

        # Create token WITHOUT plaintext - should succeed validation
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Normal Live Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=False,
            plaintext_key=None  # Correct: no plaintext for live tokens
        )

        # Should not raise ValidationError
        token.clean()

        # Verify the token properties
        assert token.plaintext_key is None
        assert token.is_sandbox is False

    def test_updating_live_token_to_add_plaintext_fails(self):
        """
        SECURITY TEST: Even updating an existing live token to add
        plaintext should fail validation.
        """
        # Create a live token without plaintext (normal case)
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Live Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=False,
            plaintext_key=None
        )

        # First validation passes
        token.clean()

        # Try to update it to add plaintext
        token.plaintext_key = full_key  # Try to add plaintext

        # Validation should fail
        with pytest.raises(ValidationError):
            token.clean()

    def test_sandbox_token_plaintext_matches_hash(self):
        """
        Verify that for sandbox tokens, the plaintext key when hashed
        matches the stored key_hash.
        """
        # Generate a sandbox token
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=True)

        # Create token with plaintext
        token = APIToken(
            tenant_id=self.tenant.id,
            name='Test Token',
            key_hash=key_hash,
            key_prefix=key_prefix,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=True,
            plaintext_key=full_key
        )

        # Should pass validation
        token.clean()

        # Verify the plaintext hashes to the same value
        computed_hash = APIToken.hash_key(token.plaintext_key)
        assert computed_hash == token.key_hash

    def test_bulk_create_cannot_bypass_validation(self):
        """
        SECURITY TEST: Document that bulk_create bypasses validation.

        Note: Django's bulk_create doesn't call save() or clean(), so it bypasses
        our security validation. This test documents that limitation.

        PRODUCTION RULE: Never use bulk_create for APIToken. Always use create()
        or save() to ensure validation runs.
        """
        # This test documents the risk - no database needed
        # The save() override in APIToken calls full_clean() to prevent this
        # But bulk_create would bypass it entirely

        # Document the expected behavior
        live_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)

        # In production, this pattern should NEVER be used:
        # APIToken.objects.bulk_create([
        #     APIToken(plaintext_key=live_key, is_sandbox=False, ...)
        # ])
        # Because it would bypass validation!

        # Instead, always use:
        # token = APIToken(...)
        # token.save()  # This calls full_clean() automatically
        pass  # Documenting the risk

    def test_none_plaintext_always_allowed(self):
        """
        Both sandbox and live tokens can have plaintext_key=None.
        This should always be allowed.
        """
        # Test with sandbox token
        sandbox_key, key_hash1, key_prefix1 = APIToken.generate_key(is_sandbox=True)
        sandbox_token = APIToken(
            tenant_id=self.tenant.id,
            name='Sandbox No Plaintext',
            key_hash=key_hash1,
            key_prefix=key_prefix1,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=True,
            plaintext_key=None  # Allowed
        )
        sandbox_token.clean()  # Should not raise
        assert sandbox_token.plaintext_key is None

        # Test with live token
        live_key, key_hash2, key_prefix2 = APIToken.generate_key(is_sandbox=False)
        live_token = APIToken(
            tenant_id=self.tenant.id,
            name='Live No Plaintext',
            key_hash=key_hash2,
            key_prefix=key_prefix2,
            scopes=['services:read'],
            created_by_id=self.user.id,
            is_sandbox=False,
            plaintext_key=None  # Allowed
        )
        live_token.clean()  # Should not raise
        assert live_token.plaintext_key is None
