"""
scripts/validate_bd_manual.py 的单元测试。

不需要数据库连接，通过构造临时文件系统结构来验证各 check 函数。
"""

from __future__ import annotations

import textwrap
from pathlib import Path

import pytest

# 被测模块
from scripts.validate_bd_manual import (
    check_directory_structure,
    check_ods_doc_coverage,
    check_ods_doc_format,
    check_ods_doc_naming,
    check_mapping_doc_coverage,
    check_mapping_doc_content,
    check_mapping_doc_naming,
    check_ods_dictionary_coverage,
    BD_MANUAL_ROOT,
    ODS_MAIN_DIR,
    ODS_MAPPINGS_DIR,
    ODS_DICT_PATH,
    DATA_LAYERS,
)
import scripts.validate_bd_manual as mod


# ---------------------------------------------------------------------------
# 辅助：临时目录结构搭建
# ---------------------------------------------------------------------------

def _setup_layer_dirs(tmp_path: Path) -> None:
    """在 tmp_path 下创建完整的四层目录结构。"""
    for layer in DATA_LAYERS:
        (tmp_path / "docs" / "bd_manual" / layer / "main").mkdir(parents=True, exist_ok=True)
        (tmp_path / "docs" / "bd_manual" / layer / "changes").mkdir(parents=True, exist_ok=True)


SAMPLE_ODS_DOC = textwrap.dedent("""\
    # test_table 测试表

    > 生成时间：2026-01-01

    ## 表信息

    | 属性 | 值 |
    |------|-----|
    | Schema | billiards_ods |
    | 表名 | test_table |
    | 主键 | id, content_hash |
    | 数据来源 | TestEndpoint |
    | 说明 | 测试表 |

    ## 字段说明

    | 序号 | 字段名 | 类型 | 可空 | 说明 |
    |------|--------|------|------|------|
    | 1 | id | BIGINT | NO | 主键 |

    ## 使用说明

    ```sql
    SELECT * FROM billiards_ods.test_table LIMIT 10;
    ```

    ## ETL 元数据字段

    | 字段名 | 类型 | 说明 |
    |--------|------|------|
    | content_hash | TEXT | SHA256 |
    | source_file | TEXT | 文件名 |
    | source_endpoint | TEXT | 端点 |
    | fetched_at | TIMESTAMPTZ | 时间戳 |
    | payload | JSONB | 原始 JSON |

    ## 可回溯性

    | 项目 | 说明 |
    |------|------|
    | 可回溯 | ✅ 完全可回溯 |
    | 数据来源 | TestEndpoint |
""")


SAMPLE_MAPPING_DOC = textwrap.dedent("""\
    # TestEndpoint → test_table 字段映射

    > 生成时间：2026-01-01

    ## 端点信息

    | 属性 | 值 |
    |------|-----|
    | 接口路径 | `Test/TestEndpoint` |
    | 请求方法 | POST |
    | ODS 对应表 | `billiards_ods.test_table` |
    | JSON 数据路径 | `data.items` |

    ## 字段映射

    | JSON 字段 | ODS 列名 | 类型转换 | 说明 |
    |-----------|----------|----------|------|
    | id | id | int→BIGINT | 主键 |

    ## ETL 补充字段

    | ODS 列名 | 生成逻辑 |
    |-----------|----------|
    | content_hash | SHA256 |
    | source_file | 固定值 |
    | source_endpoint | 端点路径 |
    | fetched_at | 入库时间戳 |
    | payload | 原始 JSON |
""")


# ---------------------------------------------------------------------------
# Property 1: 目录结构一致性
# ---------------------------------------------------------------------------

class TestCheckDirectoryStructure:
    """Property 1: 数据层目录结构一致性。"""

    def test_pass_when_all_dirs_exist(self, tmp_path, monkeypatch):
        _setup_layer_dirs(tmp_path)
        monkeypatch.setattr(mod, "BD_MANUAL_ROOT", tmp_path / "docs" / "bd_manual")
        result = check_directory_structure()
        assert result.passed is True
        assert result.property_id == "Property 1"

    def test_fail_when_missing_subdir(self, tmp_path, monkeypatch):
        _setup_layer_dirs(tmp_path)
        # 删除 ETL_Admin/changes/
        import shutil
        shutil.rmtree(tmp_path / "docs" / "bd_manual" / "ETL_Admin" / "changes")
        monkeypatch.setattr(mod, "BD_MANUAL_ROOT", tmp_path / "docs" / "bd_manual")
        result = check_directory_structure()
        assert result.passed is False
        assert any("ETL_Admin" in d and "changes" in d for d in result.details)


# ---------------------------------------------------------------------------
# Property 4: ODS 文档覆盖率
# ---------------------------------------------------------------------------

class TestCheckOdsDocCoverage:
    """Property 4: ODS 表级文档覆盖率。"""

    def test_pass_when_all_docs_exist(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        tables = ["member_profiles", "payment_transactions"]
        for t in tables:
            (ods_main / f"BD_manual_{t}.md").write_text("# doc", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_coverage(tables)
        assert result.passed is True

    def test_fail_when_doc_missing(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        (ods_main / "BD_manual_member_profiles.md").write_text("# doc", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_coverage(["member_profiles", "missing_table"])
        assert result.passed is False
        assert any("missing_table" in d for d in result.details)


# ---------------------------------------------------------------------------
# Property 5: ODS 文档格式完整性
# ---------------------------------------------------------------------------

class TestCheckOdsDocFormat:
    """Property 5: ODS 表级文档格式完整性。"""

    def test_pass_with_complete_doc(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        (ods_main / "BD_manual_test_table.md").write_text(SAMPLE_ODS_DOC, encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_format()
        assert result.passed is True

    def test_fail_when_missing_section(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        # 去掉"可回溯性"章节
        incomplete = SAMPLE_ODS_DOC.replace("## 可回溯性", "## 其他")
        (ods_main / "BD_manual_test_table.md").write_text(incomplete, encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_format()
        assert result.passed is False
        assert any("可回溯性" in d for d in result.details)

    def test_fail_when_missing_etl_field(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        # 去掉 content_hash
        incomplete = SAMPLE_ODS_DOC.replace("content_hash", "xxx_hash")
        (ods_main / "BD_manual_test_table.md").write_text(incomplete, encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_format()
        assert result.passed is False
        assert any("content_hash" in d for d in result.details)


# ---------------------------------------------------------------------------
# Property 6: ODS 文档命名规范
# ---------------------------------------------------------------------------

class TestCheckOdsDocNaming:
    """Property 6: ODS 表级文档命名规范。"""

    def test_pass_with_valid_names(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        (ods_main / "BD_manual_member_profiles.md").write_text("# doc", encoding="utf-8")
        (ods_main / "BD_manual_payment_transactions.md").write_text("# doc", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_naming()
        assert result.passed is True

    def test_fail_with_invalid_name(self, tmp_path, monkeypatch):
        """ODS/main/ 下所有 .md 文件都应符合命名规范，BadName.md 应被检出。"""
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        (ods_main / "BD_manual_member_profiles.md").write_text("# doc", encoding="utf-8")
        (ods_main / "BadName.md").write_text("# doc", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_naming()
        assert result.passed is False
        assert any("BadName.md" in d for d in result.details)

    def test_fail_with_uppercase_table_name(self, tmp_path, monkeypatch):
        ods_main = tmp_path / "ODS" / "main"
        ods_main.mkdir(parents=True)
        # BD_manual_ 后面跟大写字母，不符合 snake_case
        (ods_main / "BD_manual_MemberProfiles.md").write_text("# doc", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAIN_DIR", ods_main)
        result = check_ods_doc_naming()
        assert result.passed is False


# ---------------------------------------------------------------------------
# Property 7: 映射文档覆盖率
# ---------------------------------------------------------------------------

class TestCheckMappingDocCoverage:
    """Property 7: 映射文档覆盖率。"""

    def test_pass_when_all_mappings_exist(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        (mappings / "mapping_GetTest_test_table.md").write_text("# map", encoding="utf-8")
        (mappings / "mapping_GetOther_other_table.md").write_text("# map", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_coverage(["test_table", "other_table"])
        assert result.passed is True

    def test_fail_when_mapping_missing(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        (mappings / "mapping_GetTest_test_table.md").write_text("# map", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_coverage(["test_table", "missing_table"])
        assert result.passed is False
        assert any("missing_table" in d for d in result.details)


# ---------------------------------------------------------------------------
# Property 8: 映射文档内容完整性
# ---------------------------------------------------------------------------

class TestCheckMappingDocContent:
    """Property 8: 映射文档内容完整性。"""

    def test_pass_with_complete_mapping(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        (mappings / "mapping_TestEndpoint_test_table.md").write_text(
            SAMPLE_MAPPING_DOC, encoding="utf-8"
        )
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_content()
        assert result.passed is True

    def test_fail_when_missing_section(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        incomplete = SAMPLE_MAPPING_DOC.replace("## 字段映射", "## 其他映射")
        (mappings / "mapping_TestEndpoint_test_table.md").write_text(
            incomplete, encoding="utf-8"
        )
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_content()
        assert result.passed is False
        assert any("字段映射" in d for d in result.details)


# ---------------------------------------------------------------------------
# Property 9: 映射文档命名规范
# ---------------------------------------------------------------------------

class TestCheckMappingDocNaming:
    """Property 9: 映射文档命名规范。"""

    def test_pass_with_valid_names(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        (mappings / "mapping_GetTenantMemberList_member_profiles.md").write_text("# m", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_naming()
        assert result.passed is True

    def test_fail_with_lowercase_endpoint(self, tmp_path, monkeypatch):
        mappings = tmp_path / "ODS" / "mappings"
        mappings.mkdir(parents=True)
        # 端点名以小写开头，不符合 PascalCase
        (mappings / "mapping_getTenantMemberList_member_profiles.md").write_text("# m", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_MAPPINGS_DIR", mappings)
        result = check_mapping_doc_naming()
        assert result.passed is False


# ---------------------------------------------------------------------------
# Property 10: ODS 数据字典覆盖率
# ---------------------------------------------------------------------------

class TestCheckOdsDictionaryCoverage:
    """Property 10: ODS 数据字典覆盖率。"""

    def test_pass_when_all_tables_in_dict(self, tmp_path, monkeypatch):
        dict_file = tmp_path / "ods_tables_dictionary.md"
        dict_file.write_text(
            "| `member_profiles` | 会员 |\n| `payment_transactions` | 支付 |",
            encoding="utf-8",
        )
        monkeypatch.setattr(mod, "ODS_DICT_PATH", dict_file)
        result = check_ods_dictionary_coverage(["member_profiles", "payment_transactions"])
        assert result.passed is True

    def test_fail_when_table_missing_from_dict(self, tmp_path, monkeypatch):
        dict_file = tmp_path / "ods_tables_dictionary.md"
        dict_file.write_text("| `member_profiles` | 会员 |", encoding="utf-8")
        monkeypatch.setattr(mod, "ODS_DICT_PATH", dict_file)
        result = check_ods_dictionary_coverage(["member_profiles", "missing_table"])
        assert result.passed is False
        assert any("missing_table" in d for d in result.details)

    def test_fail_when_dict_file_missing(self, tmp_path, monkeypatch):
        monkeypatch.setattr(mod, "ODS_DICT_PATH", tmp_path / "nonexistent.md")
        result = check_ods_dictionary_coverage(["some_table"])
        assert result.passed is False
