"""DDL 对比脚本差异检测完整性 — 属性测试。

# AI_CHANGELOG [2026-02-13] max_examples 从 100 降至 30 以加速测试运行

**Property 2: DDL 对比脚本差异检测完整性**
**Validates: Requirements 2.1, 2.2, 2.3, 2.4**

使用 hypothesis 生成随机的 DDL 表定义和数据库表定义，
注入已知差异，验证 compare_tables 能检测到所有差异。
"""

from __future__ import annotations

import string

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

from scripts.compare_ddl_db import (
    ColumnDef,
    DiffKind,
    TableDef,
    compare_tables,
)

# =========================================================================
# 自定义 Strategy：生成随机的 ColumnDef / TableDef
# =========================================================================

# 可用的标准化类型池（与 normalize_type 输出一致）
_NORMALIZED_TYPES = [
    "bigint", "integer", "smallint", "boolean", "text",
    "jsonb", "json", "date", "bytea", "uuid",
    "timestamp", "timestamptz", "double precision",
    "varchar(50)", "varchar(100)", "varchar(255)",
    "char(1)", "char(10)",
    "numeric(18,2)", "numeric(10,6)", "numeric(5,0)",
]

# 合法的标识符字符集（小写字母 + 下划线 + 数字，首字符为字母）
_IDENT_ALPHABET = string.ascii_lowercase + "_"
_IDENT_FULL = _IDENT_ALPHABET + string.digits


def st_identifier() -> st.SearchStrategy[str]:
    """生成合法的 PostgreSQL 标识符（小写，3~20 字符）。"""
    return st.from_regex(r"[a-z][a-z0-9_]{2,19}", fullmatch=True)


def st_data_type() -> st.SearchStrategy[str]:
    """从标准化类型池中随机选取一个类型。"""
    return st.sampled_from(_NORMALIZED_TYPES)


def st_column_def(name: str | None = None) -> st.SearchStrategy[ColumnDef]:
    """生成随机的 ColumnDef。"""
    return st.builds(
        ColumnDef,
        name=st.just(name) if name else st_identifier(),
        data_type=st_data_type(),
        nullable=st.booleans(),
        is_pk=st.just(False),  # PK 由 TableDef 层面控制
        default=st.just(None),
    )


@st.composite
def st_table_def(draw, name: str | None = None) -> TableDef:
    """生成随机的 TableDef（1~8 个字段，可选主键）。"""
    tbl_name = name or draw(st_identifier())
    num_cols = draw(st.integers(min_value=1, max_value=8))

    # 生成不重复的列名
    col_names = draw(
        st.lists(st_identifier(), min_size=num_cols, max_size=num_cols, unique=True)
    )

    columns: dict[str, ColumnDef] = {}
    for cn in col_names:
        col = draw(st_column_def(name=cn))
        columns[cn] = col

    # 随机选取 0~2 个列作为主键
    pk_count = draw(st.integers(min_value=0, max_value=min(2, len(col_names))))
    pk_cols = draw(
        st.lists(
            st.sampled_from(col_names),
            min_size=pk_count, max_size=pk_count, unique=True,
        )
    ) if pk_count > 0 and col_names else []

    for pk_col in pk_cols:
        columns[pk_col].is_pk = True
        columns[pk_col].nullable = False

    return TableDef(name=tbl_name, columns=columns, pk_columns=pk_cols)


@st.composite
def st_table_dict(draw, min_tables: int = 0, max_tables: int = 5) -> dict[str, TableDef]:
    """生成 {表名: TableDef} 字典，表名不重复。"""
    num = draw(st.integers(min_value=min_tables, max_value=max_tables))
    names = draw(st.lists(st_identifier(), min_size=num, max_size=num, unique=True))
    tables: dict[str, TableDef] = {}
    for n in names:
        tables[n] = draw(st_table_def(name=n))
    return tables


# =========================================================================
# Property 2: DDL 对比脚本差异检测完整性
# **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
# =========================================================================


class TestProperty2DiffDetection:
    """Property 2: 对比脚本能检测到所有注入的差异。

    **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
    """

    @given(base=st_table_dict(min_tables=1, max_tables=4))
    @settings(max_examples=30)
    def test_identical_tables_produce_no_diffs(self, base: dict[str, TableDef]):
        """完全相同的定义不应产生任何差异。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        diffs = compare_tables(base, base)
        assert diffs == [], f"相同定义不应有差异，但得到: {diffs}"

    @given(
        common=st_table_dict(min_tables=0, max_tables=3),
        db_only=st_table_dict(min_tables=1, max_tables=3),
    )
    @settings(max_examples=30)
    def test_missing_table_detected(
        self,
        common: dict[str, TableDef],
        db_only: dict[str, TableDef],
    ):
        """数据库有但 DDL 没有的表应报告为 MISSING_TABLE。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        # 确保 db_only 的表名与 common 不重叠
        overlap = set(common.keys()) & set(db_only.keys())
        assume(not overlap)

        ddl_tables = dict(common)
        db_tables = {**common, **db_only}

        diffs = compare_tables(ddl_tables, db_tables)
        missing_tables = {d.table for d in diffs if d.kind == DiffKind.MISSING_TABLE}

        for tbl in db_only:
            assert tbl in missing_tables, (
                f"表 '{tbl}' 仅存在于数据库中，应报告为 MISSING_TABLE"
            )

    @given(
        common=st_table_dict(min_tables=0, max_tables=3),
        ddl_only=st_table_dict(min_tables=1, max_tables=3),
    )
    @settings(max_examples=30)
    def test_extra_table_detected(
        self,
        common: dict[str, TableDef],
        ddl_only: dict[str, TableDef],
    ):
        """DDL 有但数据库没有的表应报告为 EXTRA_TABLE。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        overlap = set(common.keys()) & set(ddl_only.keys())
        assume(not overlap)

        ddl_tables = {**common, **ddl_only}
        db_tables = dict(common)

        diffs = compare_tables(ddl_tables, db_tables)
        extra_tables = {d.table for d in diffs if d.kind == DiffKind.EXTRA_TABLE}

        for tbl in ddl_only:
            assert tbl in extra_tables, (
                f"表 '{tbl}' 仅存在于 DDL 中，应报告为 EXTRA_TABLE"
            )

    @given(
        table=st_table_def(),
        extra_cols=st.lists(
            st.tuples(st_identifier(), st_data_type(), st.booleans()),
            min_size=1, max_size=4, unique_by=lambda x: x[0],
        ),
    )
    @settings(max_examples=30)
    def test_missing_column_detected(
        self,
        table: TableDef,
        extra_cols: list[tuple[str, str, bool]],
    ):
        """数据库有但 DDL 没有的字段应报告为 MISSING_COLUMN。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        # 确保注入的列名与现有列不重叠
        existing_names = set(table.columns.keys())
        injected_names = {c[0] for c in extra_cols}
        assume(not (existing_names & injected_names))

        # DDL 侧：原始表
        ddl_tables = {table.name: table}

        # DB 侧：原始表 + 额外字段
        db_table = TableDef(
            name=table.name,
            columns=dict(table.columns),
            pk_columns=list(table.pk_columns),
        )
        for col_name, col_type, nullable in extra_cols:
            db_table.columns[col_name] = ColumnDef(
                name=col_name, data_type=col_type, nullable=nullable,
            )
        db_tables = {table.name: db_table}

        diffs = compare_tables(ddl_tables, db_tables)
        missing_cols = {
            d.column for d in diffs
            if d.kind == DiffKind.MISSING_COLUMN and d.table == table.name
        }

        for col_name, _, _ in extra_cols:
            assert col_name in missing_cols, (
                f"字段 '{table.name}.{col_name}' 仅存在于数据库中，"
                f"应报告为 MISSING_COLUMN"
            )

    @given(
        table=st_table_def(),
        extra_cols=st.lists(
            st.tuples(st_identifier(), st_data_type(), st.booleans()),
            min_size=1, max_size=4, unique_by=lambda x: x[0],
        ),
    )
    @settings(max_examples=30)
    def test_extra_column_detected(
        self,
        table: TableDef,
        extra_cols: list[tuple[str, str, bool]],
    ):
        """DDL 有但数据库没有的字段应报告为 EXTRA_COLUMN。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        existing_names = set(table.columns.keys())
        injected_names = {c[0] for c in extra_cols}
        assume(not (existing_names & injected_names))

        # DDL 侧：原始表 + 额外字段
        ddl_table = TableDef(
            name=table.name,
            columns=dict(table.columns),
            pk_columns=list(table.pk_columns),
        )
        for col_name, col_type, nullable in extra_cols:
            ddl_table.columns[col_name] = ColumnDef(
                name=col_name, data_type=col_type, nullable=nullable,
            )
        ddl_tables = {table.name: ddl_table}

        # DB 侧：原始表
        db_tables = {table.name: table}

        diffs = compare_tables(ddl_tables, db_tables)
        extra_col_set = {
            d.column for d in diffs
            if d.kind == DiffKind.EXTRA_COLUMN and d.table == table.name
        }

        for col_name, _, _ in extra_cols:
            assert col_name in extra_col_set, (
                f"字段 '{table.name}.{col_name}' 仅存在于 DDL 中，"
                f"应报告为 EXTRA_COLUMN"
            )

    @given(table=st_table_def())
    @settings(max_examples=30)
    def test_type_mismatch_detected(self, table: TableDef):
        """字段类型不一致时应报告为 TYPE_MISMATCH。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
        """
        assume(len(table.columns) >= 1)

        # 选取第一个字段，修改其类型
        target_col_name = next(iter(table.columns))
        original_type = table.columns[target_col_name].data_type

        # 选一个不同的类型
        alt_types = [t for t in _NORMALIZED_TYPES if t != original_type]
        assume(len(alt_types) > 0)
        new_type = alt_types[0]

        # DDL 侧：原始定义
        ddl_tables = {table.name: table}

        # DB 侧：修改目标字段类型
        db_table = TableDef(
            name=table.name,
            columns={
                cn: ColumnDef(
                    name=c.name,
                    data_type=new_type if cn == target_col_name else c.data_type,
                    nullable=c.nullable,
                    is_pk=c.is_pk,
                    default=c.default,
                )
                for cn, c in table.columns.items()
            },
            pk_columns=list(table.pk_columns),
        )
        db_tables = {table.name: db_table}

        diffs = compare_tables(ddl_tables, db_tables)
        type_mismatches = [
            d for d in diffs
            if d.kind == DiffKind.TYPE_MISMATCH
            and d.table == table.name
            and d.column == target_col_name
        ]

        assert len(type_mismatches) == 1, (
            f"字段 '{table.name}.{target_col_name}' 类型从 "
            f"'{original_type}' 变为 '{new_type}'，应报告 TYPE_MISMATCH"
        )
        assert type_mismatches[0].ddl_value == original_type
        assert type_mismatches[0].db_value == new_type

    @given(
        ddl_tables=st_table_dict(min_tables=0, max_tables=4),
        db_tables=st_table_dict(min_tables=0, max_tables=4),
    )
    @settings(max_examples=30)
    def test_all_diff_kinds_are_accounted_for(
        self,
        ddl_tables: dict[str, TableDef],
        db_tables: dict[str, TableDef],
    ):
        """对任意输入，所有差异都应属于已知的 DiffKind 枚举值。

        **Validates: Requirements 2.1, 2.2, 2.3, 2.4**

        这是一个"元属性"：验证对比函数不会产生未定义的差异类型，
        且每条差异都有合理的 table/column 引用。
        """
        diffs = compare_tables(ddl_tables, db_tables)

        valid_kinds = set(DiffKind)
        all_table_names = set(ddl_tables.keys()) | set(db_tables.keys())

        for d in diffs:
            # 差异类型必须是已知枚举值
            assert d.kind in valid_kinds, f"未知差异类型: {d.kind}"
            # 差异引用的表必须存在于某一侧
            assert d.table in all_table_names, (
                f"差异引用了不存在的表: {d.table}"
            )
            # 表级差异不应有 column
            if d.kind in (DiffKind.MISSING_TABLE, DiffKind.EXTRA_TABLE):
                assert d.column is None, (
                    f"表级差异 {d.kind} 不应包含 column 字段"
                )
            # 字段级差异必须有 column
            if d.kind in (
                DiffKind.MISSING_COLUMN, DiffKind.EXTRA_COLUMN,
                DiffKind.TYPE_MISMATCH, DiffKind.NULLABLE_MISMATCH,
            ):
                assert d.column is not None, (
                    f"字段级差异 {d.kind} 必须包含 column 字段"
                )


# =========================================================================
# Property 3: DDL 修正后零差异（不动点）
# **Validates: Requirements 2.5**
# =========================================================================


class TestProperty3FixpointZeroDiff:
    """Property 3: DDL 修正后零差异（不动点）。

    核心思想：以数据库实际状态修正 DDL 文件后，再次运行对比脚本，
    差异列表应为空。即 compare_tables(db_state, db_state) == []。

    **Validates: Requirements 2.5**
    """

    @given(db_state=st_table_dict(min_tables=0, max_tables=5))
    @settings(max_examples=30)
    def test_identical_state_yields_no_diff(self, db_state: dict[str, TableDef]):
        """将同一份定义同时作为 DDL 和 DB 输入，差异应为零。

        模拟场景：修正完成后 DDL 与数据库完全一致。

        **Validates: Requirements 2.5**
        """
        diffs = compare_tables(db_state, db_state)
        assert diffs == [], (
            f"不动点违反：相同定义应产生零差异，但得到 {len(diffs)} 项: "
            f"{[str(d) for d in diffs]}"
        )

    @given(db_state=st_table_dict(min_tables=1, max_tables=5))
    @settings(max_examples=30)
    def test_copy_db_as_ddl_yields_no_diff(self, db_state: dict[str, TableDef]):
        """模拟修正过程：从 DB 定义深拷贝构造 DDL 定义，对比应为零差异。

        这验证了"以数据库为准生成 DDL"后的不动点性质。

        **Validates: Requirements 2.5**
        """
        # 模拟修正：逐表逐字段从 DB 定义复制出一份新的 DDL 定义
        ddl_fixed: dict[str, TableDef] = {}
        for tbl_name, tbl in db_state.items():
            new_columns: dict[str, ColumnDef] = {}
            for col_name, col in tbl.columns.items():
                new_columns[col_name] = ColumnDef(
                    name=col.name,
                    data_type=col.data_type,
                    nullable=col.nullable,
                    is_pk=col.is_pk,
                    default=col.default,
                )
            ddl_fixed[tbl_name] = TableDef(
                name=tbl.name,
                columns=new_columns,
                pk_columns=list(tbl.pk_columns),
            )

        diffs = compare_tables(ddl_fixed, db_state)
        assert diffs == [], (
            f"不动点违反：从 DB 复制的 DDL 定义应与 DB 零差异，"
            f"但得到 {len(diffs)} 项: {[str(d) for d in diffs]}"
        )

    @given(
        db_state=st_table_dict(min_tables=1, max_tables=4),
        extra_tables=st_table_dict(min_tables=1, max_tables=3),
    )
    @settings(max_examples=30)
    def test_fixpoint_after_adding_missing_tables(
        self,
        db_state: dict[str, TableDef],
        extra_tables: dict[str, TableDef],
    ):
        """模拟修正流程：DDL 缺少部分表 → 补齐后再对比应为零差异。

        步骤：
        1. DB 有 common + extra 表，DDL 只有 common 表
        2. 发现 MISSING_TABLE 差异
        3. 将缺失的表补入 DDL（模拟修正）
        4. 再次对比，差异应为零

        **Validates: Requirements 2.5**
        """
        # 确保表名不重叠
        overlap = set(db_state.keys()) & set(extra_tables.keys())
        assume(not overlap)

        full_db = {**db_state, **extra_tables}
        ddl_before_fix = dict(db_state)  # 缺少 extra_tables

        # 第一次对比：应有差异
        diffs_before = compare_tables(ddl_before_fix, full_db)
        missing = {d.table for d in diffs_before if d.kind == DiffKind.MISSING_TABLE}
        assert missing == set(extra_tables.keys()), (
            f"修正前应检测到缺失表 {set(extra_tables.keys())}，实际 {missing}"
        )

        # 模拟修正：将缺失的表补入 DDL
        ddl_after_fix = {**ddl_before_fix, **extra_tables}

        # 第二次对比：不动点，差异应为零
        diffs_after = compare_tables(ddl_after_fix, full_db)
        assert diffs_after == [], (
            f"不动点违反：修正后应零差异，但得到 {len(diffs_after)} 项: "
            f"{[str(d) for d in diffs_after]}"
        )

    @given(table=st_table_def())
    @settings(max_examples=30)
    def test_fixpoint_after_correcting_type_mismatch(self, table: TableDef):
        """模拟修正流程：字段类型不一致 → 以 DB 为准修正后零差异。

        步骤：
        1. DDL 中某字段类型与 DB 不同
        2. 发现 TYPE_MISMATCH
        3. 将 DDL 字段类型改为 DB 的类型（模拟修正）
        4. 再次对比，差异应为零

        **Validates: Requirements 2.5**
        """
        assume(len(table.columns) >= 1)

        target_col = next(iter(table.columns))
        original_type = table.columns[target_col].data_type
        alt_types = [t for t in _NORMALIZED_TYPES if t != original_type]
        assume(len(alt_types) > 0)
        wrong_type = alt_types[0]

        # DDL 侧：目标字段使用错误类型
        ddl_table = TableDef(
            name=table.name,
            columns={
                cn: ColumnDef(
                    name=c.name,
                    data_type=wrong_type if cn == target_col else c.data_type,
                    nullable=c.nullable,
                    is_pk=c.is_pk,
                    default=c.default,
                )
                for cn, c in table.columns.items()
            },
            pk_columns=list(table.pk_columns),
        )
        ddl_before = {table.name: ddl_table}
        db_tables = {table.name: table}

        # 第一次对比：应有 TYPE_MISMATCH
        diffs_before = compare_tables(ddl_before, db_tables)
        type_diffs = [
            d for d in diffs_before
            if d.kind == DiffKind.TYPE_MISMATCH and d.column == target_col
        ]
        assert len(type_diffs) >= 1, "修正前应检测到 TYPE_MISMATCH"

        # 模拟修正：以 DB 为准，直接使用 DB 定义作为 DDL
        ddl_after = dict(db_tables)

        # 第二次对比：不动点
        diffs_after = compare_tables(ddl_after, db_tables)
        assert diffs_after == [], (
            f"不动点违反：类型修正后应零差异，但得到 {len(diffs_after)} 项: "
            f"{[str(d) for d in diffs_after]}"
        )
