# -*- coding: utf-8 -*-
"""环境配置属性测试（Property-Based Testing）。

使用 hypothesis 验证环境配置管理的通用正确性属性：
- Property 15: .env 解析与敏感值掩码
- Property 16: .env 写入往返一致性

测试策略：
- Property 15: 生成随机 .env 内容（含敏感和非敏感键），验证 _parse_env + _is_sensitive 对敏感值掩码
- Property 16: 生成随机键值对，序列化为 .env 格式后再解析，验证往返一致性
"""

import os

os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-env-config-properties")

from hypothesis import given, settings, assume
from hypothesis import strategies as st

from app.routers.env_config import _parse_env, _is_sensitive, _MASK, _SENSITIVE_KEYWORDS


# ---------------------------------------------------------------------------
# 通用策略（Strategies）
# ---------------------------------------------------------------------------

# 合法的环境变量键名：字母或下划线开头，后跟字母、数字、下划线
_key_start_char = st.sampled_from(
    list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_")
)
_key_rest_char = st.sampled_from(
    list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
)

_env_key_st = st.builds(
    lambda first, rest: first + rest,
    first=_key_start_char,
    rest=st.text(alphabet=_key_rest_char, min_size=0, max_size=30),
)

# 值：不含换行符的可打印字符串（排除引号以避免解析歧义）
_env_value_st = st.text(
    alphabet=st.characters(
        whitelist_categories=("L", "N", "P", "S"),
        blacklist_characters='\n\r"\'#',
    ),
    min_size=0,
    max_size=50,
)

# 敏感键：在随机键名中嵌入敏感关键词
_sensitive_keyword_st = st.sampled_from(list(_SENSITIVE_KEYWORDS))

_sensitive_key_st = st.builds(
    lambda prefix, kw, suffix: prefix + kw + suffix,
    prefix=st.text(alphabet=_key_rest_char, min_size=0, max_size=10),
    kw=_sensitive_keyword_st,
    suffix=st.text(alphabet=_key_rest_char, min_size=0, max_size=10),
).filter(lambda k: len(k) > 0 and k[0].isalpha() or k[0] == "_")

# 确保敏感键以字母或下划线开头
_safe_sensitive_key_st = st.builds(
    lambda prefix, kw: prefix + "_" + kw,
    prefix=st.sampled_from(["DB", "API", "ETL", "APP", "MY"]),
    kw=_sensitive_keyword_st,
)


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 15: .env 解析与敏感值掩码
# **Validates: Requirements 6.1, 6.3**
# ---------------------------------------------------------------------------

@settings(max_examples=100)
@given(
    sensitive_keys=st.lists(_safe_sensitive_key_st, min_size=1, max_size=5, unique=True),
    sensitive_values=st.lists(
        st.text(min_size=1, max_size=30, alphabet=st.characters(
            whitelist_categories=("L", "N"),
        )),
        min_size=1, max_size=5,
    ),
    normal_keys=st.lists(_env_key_st, min_size=1, max_size=5, unique=True),
    normal_values=st.lists(_env_value_st, min_size=1, max_size=5),
)
def test_sensitive_values_masked(sensitive_keys, sensitive_values, normal_keys, normal_values):
    """Property 15: .env 解析与敏感值掩码。

    包含敏感键（PASSWORD、TOKEN、SECRET、DSN）的 .env 文件内容，
    API 返回的键值对列表中这些键的值应被掩码替换，不包含原始敏感值。
    """
    # 确保敏感键和普通键不重叠
    normal_keys_filtered = [k for k in normal_keys if k not in sensitive_keys]
    assume(len(normal_keys_filtered) >= 1)

    # 对齐列表长度
    s_vals = (sensitive_values * ((len(sensitive_keys) // len(sensitive_values)) + 1))[:len(sensitive_keys)]
    n_vals = (normal_values * ((len(normal_keys_filtered) // len(normal_values)) + 1))[:len(normal_keys_filtered)]

    # 构造 .env 内容
    lines = []
    for k, v in zip(sensitive_keys, s_vals):
        lines.append(f"{k}={v}")
    for k, v in zip(normal_keys_filtered, n_vals):
        lines.append(f"{k}={v}")
    env_content = "\n".join(lines) + "\n"

    # 解析
    parsed = _parse_env(env_content)
    entries = [line for line in parsed if line["type"] == "entry"]

    # 模拟 GET 端点的掩码逻辑
    masked_entries = {}
    for entry in entries:
        if _is_sensitive(entry["key"]):
            masked_entries[entry["key"]] = _MASK
        else:
            masked_entries[entry["key"]] = entry["value"]

    # 验证：敏感键的值应被掩码
    for k, v in zip(sensitive_keys, s_vals):
        assert k in masked_entries, f"敏感键 {k} 应出现在解析结果中"
        assert masked_entries[k] == _MASK, (
            f"敏感键 {k} 的值应为掩码 '{_MASK}'，实际为 '{masked_entries[k]}'"
        )
        # 原始敏感值不应出现在掩码后的结果中
        assert masked_entries[k] != v, (
            f"敏感键 {k} 的原始值 '{v}' 不应出现在掩码结果中"
        )

    # 验证：非敏感键的值应保持原样
    for k, v in zip(normal_keys_filtered, n_vals):
        if not _is_sensitive(k):
            assert k in masked_entries, f"普通键 {k} 应出现在解析结果中"
            assert masked_entries[k] == v, (
                f"普通键 {k} 的值应为 '{v}'，实际为 '{masked_entries[k]}'"
            )


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 16: .env 写入往返一致性
# **Validates: Requirements 6.2**
# ---------------------------------------------------------------------------

@settings(max_examples=100)
@given(
    entries=st.lists(
        st.tuples(_env_key_st, _env_value_st),
        min_size=1,
        max_size=10,
        unique_by=lambda t: t[0],  # 键唯一
    ),
)
def test_env_write_read_round_trip(entries):
    """Property 16: .env 写入往返一致性。

    有效的键值对集合（不含注释和空行），写入 .env 文件后再读取解析，
    应得到与原始集合等价的键值对。
    """
    # 过滤掉值中可能导致解析歧义的情况（值前后空白会被 strip）
    clean_entries = [(k, v.strip()) for k, v in entries]
    # 排除空键（策略已保证非空，但防御性检查）
    clean_entries = [(k, v) for k, v in clean_entries if k]

    assume(len(clean_entries) >= 1)

    # 模拟写入：构造 .env 文件内容
    lines = [f"{k}={v}" for k, v in clean_entries]
    env_content = "\n".join(lines) + "\n"

    # 解析
    parsed = _parse_env(env_content)
    parsed_entries = {
        line["key"]: line["value"]
        for line in parsed
        if line["type"] == "entry"
    }

    # 验证往返一致性：每个写入的键值对都应在解析结果中
    for k, v in clean_entries:
        assert k in parsed_entries, (
            f"键 '{k}' 应出现在解析结果中，实际键集合: {list(parsed_entries.keys())}"
        )
        assert parsed_entries[k] == v, (
            f"键 '{k}' 的值不一致：写入='{v}'，解析='{parsed_entries[k]}'"
        )

    # 验证：解析结果的键数量应与写入的一致
    assert len(parsed_entries) == len(clean_entries), (
        f"解析结果键数量 {len(parsed_entries)} 应等于写入数量 {len(clean_entries)}"
    )
