import logging
from unittest.mock import patch

from openhands.app_server.utils.logger import (
    RedactURLParamsFilter,
    SensitiveDataFilter,
    _uvicorn_default_log_config,
    _uvicorn_json_log_config,
)


@patch.dict(
    'os.environ',
    {
        'API_SECRET': 'super-secret-123',
        'AUTH_TOKEN': 'auth-token-456',
        'NORMAL_VAR': 'normal-value',
    },
    clear=True,
)
def test_sensitive_data_filter_basic():
    # Create a filter instance
    filter = SensitiveDataFilter()

    # Create a log record with sensitive data
    record = logging.LogRecord(
        name='test_logger',
        level=logging.INFO,
        pathname='test.py',
        lineno=1,
        msg='API Secret: super-secret-123, Token: auth-token-456, Normal: normal-value',
        args=(),
        exc_info=None,
    )

    # Apply the filter
    filter.filter(record)

    # Check that sensitive data is masked but normal data isn't
    assert '******' in record.msg
    assert 'super-secret-123' not in record.msg
    assert 'auth-token-456' not in record.msg
    assert 'normal-value' in record.msg


@patch.dict('os.environ', {}, clear=True)
def test_sensitive_data_filter_empty_values():
    # Test with empty environment variables
    filter = SensitiveDataFilter()

    record = logging.LogRecord(
        name='test_logger',
        level=logging.INFO,
        pathname='test.py',
        lineno=1,
        msg='No sensitive data here',
        args=(),
        exc_info=None,
    )

    # Apply the filter
    filter.filter(record)

    # Message should remain unchanged
    assert record.msg == 'No sensitive data here'


@patch.dict('os.environ', {'API_KEY': 'secret-key-789'}, clear=True)
def test_sensitive_data_filter_multiple_occurrences():
    # Test with multiple occurrences of the same sensitive data
    filter = SensitiveDataFilter()

    # Create a message with multiple occurrences of the same sensitive data
    record = logging.LogRecord(
        name='test_logger',
        level=logging.INFO,
        pathname='test.py',
        lineno=1,
        msg='Key1: secret-key-789, Key2: secret-key-789',
        args=(),
        exc_info=None,
    )

    # Apply the filter
    filter.filter(record)

    # Check that all occurrences are masked
    assert record.msg.count('******') == 2
    assert 'secret-key-789' not in record.msg


@patch.dict(
    'os.environ',
    {
        'secret_KEY': 'secret-value-1',
        'API_secret': 'secret-value-2',
        'TOKEN_code': 'secret-value-3',
    },
    clear=True,
)
def test_sensitive_data_filter_case_sensitivity():
    # Test with different case variations in environment variable names
    filter = SensitiveDataFilter()

    record = logging.LogRecord(
        name='test_logger',
        level=logging.INFO,
        pathname='test.py',
        lineno=1,
        msg='Values: secret-value-1, secret-value-2, secret-value-3',
        args=(),
        exc_info=None,
    )

    # Apply the filter
    filter.filter(record)

    # Check that all sensitive values are masked regardless of case
    assert 'secret-value-1' not in record.msg
    assert 'secret-value-2' not in record.msg
    assert 'secret-value-3' not in record.msg
    assert record.msg.count('******') == 3


# --------------------------------------------------------------------------
# RedactURLParamsFilter tests
# --------------------------------------------------------------------------


def test_redact_url_params_filter_websocket_log():
    """Test that session_api_key is redacted from WebSocket access logs."""
    log_filter = RedactURLParamsFilter()

    # Simulate uvicorn WebSocket access log format
    record = logging.LogRecord(
        name='uvicorn.access',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='%s - "%s" [%s]',
        args=(
            '127.0.0.1:8000',
            'GET /ws/abc123?resend_all=true&session_api_key=secret-token-12345',
            'accepted',
        ),
        exc_info=None,
    )

    # Apply the filter
    result = log_filter.filter(record)

    # Filter should always return True (never drop records)
    assert result is True

    # Check that secret is redacted but other params preserved
    args_str = str(record.args)
    assert 'secret-token-12345' not in args_str
    # URL-encoded <redacted> is %3Credacted%3E
    assert '<redacted>' in args_str or '%3Credacted%3E' in args_str
    assert 'resend_all=true' in args_str


def test_redact_url_params_filter_multiple_sensitive_params():
    """Test that multiple sensitive parameters are redacted."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='uvicorn.access',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='Request: %s',
        args=('GET /api?api_key=secret1&token=secret2&user_id=123',),
        exc_info=None,
    )

    log_filter.filter(record)

    args_str = str(record.args)
    assert 'secret1' not in args_str
    assert 'secret2' not in args_str
    assert 'user_id=123' in args_str


def test_redact_url_params_filter_non_url_passthrough():
    """Test that messages without URLs pass through unchanged."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='test',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='Normal log: %s %s',
        args=('hello', 'world'),
        exc_info=None,
    )

    log_filter.filter(record)

    # Message should remain unchanged
    assert record.args == ('hello', 'world')


def test_redact_url_params_filter_no_query_string():
    """Test that URLs without query strings pass through unchanged."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='test',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='Request: %s',
        args=('GET /api/v1/users',),
        exc_info=None,
    )

    log_filter.filter(record)

    # URL without query string should remain unchanged
    assert record.args == ('GET /api/v1/users',)


def test_redact_url_params_filter_empty_args():
    """Test that records with no args are handled gracefully."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='test',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='Simple message',
        args=(),
        exc_info=None,
    )

    result = log_filter.filter(record)

    assert result is True
    assert record.args == ()


def test_redact_url_params_filter_none_args():
    """Test that records with None args are handled gracefully."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='test',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='Simple message',
        args=None,
        exc_info=None,
    )

    result = log_filter.filter(record)

    assert result is True
    assert record.args is None


def test_redact_url_params_filter_dict_args():
    """Test that records with dict args have URL params redacted."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='test',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='%(method)s %(path)s',
        args={'method': 'GET', 'path': '/api?secret=test'},
        exc_info=None,
    )

    result = log_filter.filter(record)

    assert result is True
    assert record.args['method'] == 'GET'
    assert 'test' not in record.args['path']
    assert (
        '<redacted>' in record.args['path'] or '%3Credacted%3E' in record.args['path']
    )


def test_redact_url_params_filter_msg_embedded_url():
    """Test that URLs with query params embedded in record.msg are redacted."""
    log_filter = RedactURLParamsFilter()

    record = logging.LogRecord(
        name='uvicorn.access',
        level=logging.INFO,
        pathname='',
        lineno=0,
        msg='10.0.0.1 - "GET /ws/abc?resend_all=true&session_api_key=secret-uuid-123" [accepted]',
        args=None,
        exc_info=None,
    )

    result = log_filter.filter(record)

    assert result is True
    assert 'secret-uuid-123' not in record.msg
    assert 'resend_all=true' in record.msg
    assert '<redacted>' in record.msg or '%3Credacted%3E' in record.msg


def test_uvicorn_default_config_default_handler_has_redact_filter():
    """The 'default' handler (used by uvicorn.error) must have the redact filter.

    Ensures WebSocket [accepted] logs don't leak session_api_key.
    """
    config = _uvicorn_default_log_config()
    assert 'redact_url_params' in config['handlers']['default']['filters']


def test_uvicorn_json_config_default_handler_has_redact_filter():
    """The 'default' handler in JSON config must also have the redact filter."""
    config = _uvicorn_json_log_config()
    assert 'redact_url_params' in config['handlers']['default']['filters']


def test_uvicorn_configs_all_handlers_have_redact_filter():
    """Every handler in both uvicorn configs must include the redact filter."""
    for config_fn in (_uvicorn_default_log_config, _uvicorn_json_log_config):
        config = config_fn()
        for handler_name, handler in config['handlers'].items():
            assert 'redact_url_params' in handler.get('filters', []), (
                f"Handler '{handler_name}' in {config_fn.__name__} is missing "
                f"the 'redact_url_params' filter"
            )


@patch.dict('os.environ', {}, clear=True)
def test_sensitive_data_filter_redacts_api_key_literals():
    """SensitiveDataFilter must redact common API key prefixes via SDK utils."""
    sensitive_filter = SensitiveDataFilter()

    # Use values that match the SDK's actual patterns (20+ chars after prefix)
    cases = [
        'sk-proj-' + 'a' * 25,
        'ghp_' + 'A' * 25,
        "api_key='should-be-hidden'",
        "{'GITHUB_TOKEN': 'mytoken'}",
    ]
    for secret in cases:
        record = logging.LogRecord(
            name='test',
            level=logging.INFO,
            pathname='test.py',
            lineno=1,
            msg=f'Value: {secret}',
            args=(),
            exc_info=None,
        )
        sensitive_filter.filter(record)
        assert secret not in record.msg, f'Secret {secret!r} was not redacted'
        assert '<redacted>' in record.msg


@patch.dict('os.environ', {}, clear=True)
def test_sensitive_filter_applied_to_handler_catches_child_logger_records():
    """Filter on the handler must catch records propagated from child loggers.

    Python only checks the originating logger's filters during propagation,
    not ancestor logger filters. The filter must therefore be attached to
    the handler as well as to openhands_logger so that child-logger records
    (e.g. openhands.app_server.*) are still redacted.
    """
    import io

    sensitive_filter = SensitiveDataFilter()

    stream = io.StringIO()
    handler = logging.StreamHandler(stream)
    handler.setLevel(logging.DEBUG)
    handler.addFilter(sensitive_filter)

    parent = logging.getLogger('openhands.test_parent')
    parent.addHandler(handler)
    parent.setLevel(logging.DEBUG)
    parent.propagate = False

    child = logging.getLogger('openhands.test_parent.child')
    child.setLevel(logging.DEBUG)

    # Use a value that matches the SDK's actual redaction patterns (20+ chars)
    secret = 'sk-proj-' + 'x' * 25
    child.info(f'Token: {secret}')

    output = stream.getvalue()
    assert secret not in output, (
        'Child logger record was not redacted by handler filter'
    )
    assert '<redacted>' in output
