# -*- coding: utf-8 -*-
"""
属性测试 — 清单渲染完整性与分类分组

Feature: repo-audit
- Property 2: 清单渲染完整性
- Property 8: 清单按分类分组

Validates: Requirements 1.4, 1.10
"""

from __future__ import annotations

import string

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

from scripts.audit import Category, Disposition, InventoryItem
from scripts.audit.inventory_analyzer import render_inventory_report

# ---------------------------------------------------------------------------
# 生成器策略
# ---------------------------------------------------------------------------

_PATH_CHARS = string.ascii_letters + string.digits + "_-."

_path_segment = st.text(
    alphabet=_PATH_CHARS,
    min_size=1,
    max_size=15,
)

# 随机相对路径（1~3 层）
_rel_path = st.lists(
    _path_segment,
    min_size=1,
    max_size=3,
).map(lambda parts: "/".join(parts))

# 随机非空描述（不含管道符和换行符，避免破坏 Markdown 表格解析）
_description = st.text(
    alphabet=st.characters(
        whitelist_categories=("L", "N", "P", "S", "Z"),
        blacklist_characters="|\n\r",
    ),
    min_size=1,
    max_size=40,
)


def _inventory_item_strategy() -> st.SearchStrategy[InventoryItem]:
    """生成随机 InventoryItem 的 hypothesis 策略。"""
    return st.builds(
        InventoryItem,
        rel_path=_rel_path,
        category=st.sampled_from(list(Category)),
        disposition=st.sampled_from(list(Disposition)),
        description=_description,
    )


# 生成 0~20 个 InventoryItem 的列表
_inventory_list = st.lists(
    _inventory_item_strategy(),
    min_size=0,
    max_size=20,
)


# ---------------------------------------------------------------------------
# Property 2: 清单渲染完整性
# ---------------------------------------------------------------------------

@given(items=_inventory_list)
@settings(max_examples=100)
def test_render_inventory_completeness(items: list[InventoryItem]) -> None:
    """Property 2: 清单渲染完整性

    Feature: repo-audit, Property 2: 清单渲染完整性
    Validates: Requirements 1.4

    对于任意 InventoryItem 列表，render_inventory_report 生成的 Markdown 中，
    每个条目的 rel_path、category.value、disposition.value 和 description
    四个字段都应出现在输出文本中。
    """
    report = render_inventory_report(items, "/tmp/test-repo")

    for item in items:
        # rel_path 出现在表格行中
        assert item.rel_path in report, (
            f"rel_path '{item.rel_path}' 未出现在报告中"
        )
        # category.value 出现在分组标题中
        assert item.category.value in report, (
            f"category '{item.category.value}' 未出现在报告中"
        )
        # disposition.value 出现在表格行中
        assert item.disposition.value in report, (
            f"disposition '{item.disposition.value}' 未出现在报告中"
        )
        # description 出现在表格行中
        assert item.description in report, (
            f"description '{item.description}' 未出现在报告中"
        )


# ---------------------------------------------------------------------------
# Property 8: 清单按分类分组
# ---------------------------------------------------------------------------

@given(items=_inventory_list)
@settings(max_examples=100)
def test_render_inventory_grouped_by_category(items: list[InventoryItem]) -> None:
    """Property 8: 清单按分类分组

    Feature: repo-audit, Property 8: 清单按分类分组
    Validates: Requirements 1.10

    对于任意 InventoryItem 列表，render_inventory_report 生成的 Markdown 中，
    同一 Category 的条目应连续出现（不应被其他 Category 的条目打断）。
    """
    report = render_inventory_report(items, "/tmp/test-repo")

    if not items:
        return  # 空列表无需验证

    # 从报告中按行提取条目对应的 category 顺序
    # 表格行格式: | `{rel_path}` | {disposition} | {description} |
    # 分组标题格式: ## {category.value}
    lines = report.split("\n")

    # 收集每个分组标题下的条目，按出现顺序记录 category
    categories_in_order: list[Category] = []
    current_category: Category | None = None

    # 建立 category.value -> Category 的映射
    value_to_cat = {c.value: c for c in Category}

    for line in lines:
        stripped = line.strip()
        # 检测分组标题 "## {category.value}"
        if stripped.startswith("## ") and stripped[3:] in value_to_cat:
            current_category = value_to_cat[stripped[3:]]
            continue
        # 检测表格数据行（跳过表头和分隔行）
        if (
            current_category is not None
            and stripped.startswith("| `")
            and not stripped.startswith("| 相对路径")
            and not stripped.startswith("|---")
        ):
            categories_in_order.append(current_category)

    # 验证同一 Category 的条目连续出现
    seen: set[Category] = set()
    prev: Category | None = None
    for cat in categories_in_order:
        if cat != prev:
            assert cat not in seen, (
                f"Category '{cat.value}' 的条目不连续——"
                f"在其他分类条目之后再次出现"
            )
            seen.add(cat)
            prev = cat
