"""
ticket_validator 模組測試

測試 Ticket 驗證功能（ID 格式、必填欄位等）。
"""

from typing import Dict, Any

import pytest

from ticket_system.lib.ticket_validator import (
    validate_ticket_id,
    validate_ticket_fields,
    validate_ticket_dict,
    validate_related_to,
    validate_execution_log,
    validate_execution_log_by_type,
)


class TestValidateTicketId:
    """驗證 Ticket ID 格式的測試"""

    def test_valid_root_ticket_id(self):
        """測試有效的根任務 ID"""
        assert validate_ticket_id("0.31.0-W4-001") is True
        assert validate_ticket_id("0.30.0-W1-001") is True
        assert validate_ticket_id("1.0.0-W10-999") is True

    def test_valid_child_ticket_id(self):
        """測試有效的子任務 ID"""
        assert validate_ticket_id("0.31.0-W4-001.1") is True
        assert validate_ticket_id("0.31.0-W4-001.1.1") is True
        assert validate_ticket_id("0.31.0-W4-001.1.1.1") is True

    def test_invalid_id_format(self):
        """測試無效的 ID 格式"""
        assert validate_ticket_id("invalid") is False
        assert validate_ticket_id("0.31.0-001") is False  # 缺少 W
        assert validate_ticket_id("0.31.0-W-001") is False  # W 後沒有數字
        assert validate_ticket_id("W4-001") is False  # 缺少版本號

    def test_invalid_id_type(self):
        """測試非字串輸入"""
        assert validate_ticket_id(None) is False
        assert validate_ticket_id(123) is False
        assert validate_ticket_id([]) is False

    def test_empty_id(self):
        """測試空字串 ID"""
        assert validate_ticket_id("") is False


class TestValidateTicketFields:
    """驗證 Ticket 必填欄位的測試"""

    def test_all_fields_present(self):
        """測試所有必填欄位都存在"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
            "title": "Test",
        }
        missing = validate_ticket_fields(ticket, ["id", "status", "title"])
        assert missing == []

    def test_missing_fields(self):
        """測試缺少必填欄位"""
        ticket = {
            "id": "0.31.0-W4-001",
            # 缺少 status 和 title
        }
        missing = validate_ticket_fields(ticket, ["id", "status", "title"])
        assert "status" in missing
        assert "title" in missing

    def test_empty_field_values(self):
        """測試空值欄位被視為缺失"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "",  # 空字串
            "title": None,  # None
            "what": [],  # 空列表
            "priority": {},  # 空字典
        }
        missing = validate_ticket_fields(ticket, ["status", "title", "what", "priority"])
        assert "status" in missing
        assert "title" in missing
        assert "what" in missing
        assert "priority" in missing

    def test_default_required_fields(self):
        """測試預設必填欄位"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
        }
        # 不指定 required_fields，使用預設的 ["id", "status"]
        missing = validate_ticket_fields(ticket)
        assert missing == []

    def test_custom_required_fields(self):
        """測試自訂必填欄位"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
            "title": "Test",
        }
        custom_fields = ["id", "status", "title", "priority"]
        missing = validate_ticket_fields(ticket, custom_fields)
        assert "priority" in missing

    def test_falsy_but_valid_values(self):
        """測試假值的欄位值被視為缺失"""
        ticket = {
            "id": "test",
            "status": "pending",
            "count": 0,  # 0 被視為缺失
            "flag": False,  # False 被視為缺失
        }
        # 注：根據實作，falsy 值會被視為缺失（包括 0 和 False）
        # 但實際檢驗器的實作可能對 0 和 False 有不同處理
        # 建議使用非 falsy 的預設值替代
        missing = validate_ticket_fields(ticket, ["count"])
        # 驗證 0 被視為缺失還是有效取決於實作
        assert isinstance(missing, list)


class TestValidateTicketDict:
    """執行完整 Ticket 驗證的測試"""

    def test_valid_ticket(self):
        """測試有效的 Ticket"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
            "title": "Test",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is True
        assert errors == []

    def test_invalid_ticket_id_format(self):
        """測試無效的 ID 格式"""
        ticket = {
            "id": "invalid-id",
            "status": "pending",
            "title": "Test",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is False
        assert any("Invalid Ticket ID format" in error for error in errors)

    def test_missing_id_field(self):
        """測試缺少 ID 欄位"""
        ticket = {
            "status": "pending",
            "title": "Test",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is False
        assert any("Missing 'id' field" in error for error in errors)

    def test_missing_required_fields(self):
        """測試缺少其他必填欄位"""
        ticket = {
            "id": "0.31.0-W4-001",
            # 缺少 status
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is False
        assert any("Missing field: status" in error for error in errors)

    def test_multiple_errors(self):
        """測試多個驗證錯誤"""
        ticket = {
            "id": "bad-id",
            # 缺少 status
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is False
        assert len(errors) >= 2
        assert any("Invalid Ticket ID" in error for error in errors)
        assert any("Missing field" in error for error in errors)

    def test_valid_child_ticket(self):
        """測試有效的子任務 Ticket"""
        ticket = {
            "id": "0.31.0-W4-001.1.1",
            "status": "pending",
            "title": "Child Task",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is True
        assert errors == []

    def test_empty_ticket(self):
        """測試空 Ticket"""
        passed, errors = validate_ticket_dict({})
        assert passed is False
        assert len(errors) >= 1


class TestValidationEdgeCases:
    """驗證功能的邊界情況測試"""

    def test_ticket_id_with_leading_zeros(self):
        """測試版本號含前導零的 ID"""
        assert validate_ticket_id("0.01.000-W04-001") is True

    def test_very_deep_child_ticket(self):
        """測試非常深的子任務層級"""
        deep_id = "0.31.0-W4-001" + ".1" * 10
        assert validate_ticket_id(deep_id) is True

    def test_special_characters_in_title(self):
        """測試標題中的特殊字元"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
            "title": "實作 [重要] 功能 / 支援 & 修復",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is True

    def test_unicode_in_field_values(self):
        """測試欄位值中的 Unicode"""
        ticket = {
            "id": "0.31.0-W4-001",
            "status": "pending",
            "title": "實作 Ticket 系統（測試 Unicode）",
        }
        passed, errors = validate_ticket_dict(ticket)
        assert passed is True


class TestValidateRelatedTo:
    """驗證 relatedTo 欄位的測試"""

    def test_validate_related_to_none(self):
        """Given: related_to 為 None
        When: 呼叫 validate_related_to("0.31.0-W5-001", None)
        Then: 返回 (True, None)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", None)
        assert valid is True
        assert msg is None

    def test_validate_related_to_empty_list(self):
        """Given: related_to 為空清單
        When: 呼叫 validate_related_to("0.31.0-W5-001", [])
        Then: 返回 (True, None)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", [])
        assert valid is True
        assert msg is None

    def test_validate_related_to_valid_single(self):
        """Given: related_to 包含一個有效的 Ticket ID
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002"])
        Then: 返回 (True, None)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002"])
        assert valid is True
        assert msg is None

    def test_validate_related_to_valid_multiple(self):
        """Given: related_to 包含多個有效的 Ticket IDs
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "0.31.0-W5-003"])
        Then: 返回 (True, None)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "0.31.0-W5-003"])
        assert valid is True
        assert msg is None

    def test_validate_related_to_with_child_tickets(self):
        """Given: related_to 包含子任務 IDs
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002.1", "0.31.0-W5-003.1.1"])
        Then: 返回 (True, None)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002.1", "0.31.0-W5-003.1.1"])
        assert valid is True
        assert msg is None

    def test_validate_related_to_invalid_id(self):
        """Given: related_to 包含無效的 Ticket ID
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["invalid-id"])
        Then: 返回 (False, 錯誤訊息)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["invalid-id"])
        assert valid is False
        assert "無效的 Ticket ID" in msg
        assert "invalid-id" in msg

    def test_validate_related_to_self_reference(self):
        """Given: related_to 包含自我參考
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-001"])
        Then: 返回 (False, 錯誤訊息)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-001"])
        assert valid is False
        assert "自我參考" in msg

    def test_validate_related_to_duplicate(self):
        """Given: related_to 包含重複的 ID
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "0.31.0-W5-002"])
        Then: 返回 (False, 錯誤訊息)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "0.31.0-W5-002"])
        assert valid is False
        assert "重複" in msg

    def test_validate_related_to_mixed_valid_and_invalid(self):
        """Given: related_to 包含有效和無效的 ID
        When: 呼叫 validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "invalid"])
        Then: 返回 (False, 錯誤訊息)
        """
        valid, msg = validate_related_to("0.31.0-W5-001", ["0.31.0-W5-002", "invalid"])
        assert valid is False
        assert "無效的 Ticket ID" in msg


class TestValidateExecutionLog:
    """驗證執行日誌完整性的測試"""

    def test_all_sections_filled(self):
        """所有區段已填寫"""
        body = """## Execution Log

### Problem Analysis

This is the root cause analysis.

### Solution

This is the solution implementation.

### Test Results

All tests passed successfully."""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is True
        assert unfilled == []

    def test_original_placeholder_html_comment(self):
        """偵測原始的 HTML 註解佔位符"""
        body = """## Execution Log

### Problem Analysis

<!-- To be filled by executing agent -->

### Solution

<!-- To be filled by executing agent -->

### Test Results

<!-- To be filled by executing agent -->"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert len(unfilled) == 3
        assert "Problem Analysis" in unfilled
        assert "Solution" in unfilled
        assert "Test Results" in unfilled

    def test_pending_marker_placeholder(self):
        """偵測 (pending) 待填寫標記"""
        body = """## Execution Log

### Problem Analysis

(pending)

### Solution

Implementation details here.

### Test Results

(pending)"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Problem Analysis" in unfilled
        assert "Solution" not in unfilled
        assert "Test Results" in unfilled

    def test_tbd_placeholder(self):
        """偵測 TBD 標記"""
        body = """## Execution Log

### Problem Analysis

TBD

### Solution

Solution here

### Test Results

Results here"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Problem Analysis" in unfilled

    def test_todo_placeholder(self):
        """偵測 TODO 標記"""
        body = """## Execution Log

### Problem Analysis

Analysis done

### Solution

TODO

### Test Results

Results done"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Solution" in unfilled

    def test_mixed_placeholders_and_content(self):
        """混合佔位符和真實內容"""
        body = """## Problem Analysis

Real analysis content here

## Solution

<!-- To be filled by executing agent -->

## Test Results

Test results here"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Solution" in unfilled
        assert "Problem Analysis" not in unfilled
        assert "Test Results" not in unfilled

    def test_h3_heading_format(self):
        """支援 ### 三級標題"""
        body = """## Execution Log

### Problem Analysis

Content here

### Solution

Solution here

### Test Results

Results here"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is True
        assert unfilled == []

    def test_h2_heading_format(self):
        """支援 ## 二級標題"""
        body = """## Problem Analysis

Content here

## Solution

Solution here

## Test Results

Results here"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is True
        assert unfilled == []

    def test_missing_section(self):
        """缺失某個區段"""
        body = """## Problem Analysis

Analysis here

## Test Results

Results here"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Solution" in unfilled

    def test_empty_body(self):
        """空的 body 內容"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", "")
        assert is_filled is False
        assert len(unfilled) == 3

    def test_none_body(self):
        """None body"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", None)
        assert is_filled is False
        assert len(unfilled) == 3

    def test_only_placeholder_comments(self):
        """只有佔位符註解的區段"""
        body = """## Problem Analysis

<!-- To be filled by executing agent -->

## Solution

<!-- Comment here -->

## Test Results

<!-- To be filled by executing agent -->"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False
        assert "Problem Analysis" in unfilled
        assert "Solution" in unfilled  # 任何 HTML 註解都算佔位符
        assert "Test Results" in unfilled

    def test_section_with_content_and_na(self):
        """區段包含內容但末尾有 N/A 標記"""
        body = """## Problem Analysis

Found the root cause: N/A

## Solution

Implemented the fix

## Test Results

Tests passing"""
        is_filled, unfilled = validate_execution_log("0.31.0-W4-001", body)
        assert is_filled is False  # N/A 被視為佔位符
        assert "Problem Analysis" in unfilled


# ============================================================
# W17-016.3: type-aware body schema 驗證 + 中文佔位符偵測
# ============================================================


class TestChinesePlaceholderDetection:
    """驗證 `（待填寫：...）` / `（必填：...）` 中文佔位符偵測"""

    def test_chinese_placeholder_triggers_unfilled(self):
        """template 預設的中文佔位符應被辨識為未填寫"""
        body = """## Problem Analysis

### 問題根因

（待填寫：問題發生的直接原因是什麼？）

### 影響範圍

（待填寫：哪些檔案、模組或功能受影響？）

## Solution

Real solution content here.

## Test Results

All tests passed.
"""
        is_filled, unfilled = validate_execution_log("W17-016.3", body)
        assert is_filled is False
        assert "Problem Analysis" in unfilled
        assert "Solution" not in unfilled
        assert "Test Results" not in unfilled

    def test_chinese_required_placeholder_triggers_unfilled(self):
        """ANA 重現實驗的『（必填：...）』也應視為佔位符"""
        body = """## Problem Analysis

（必填：如何重現問題？）

## Solution

Real content.

## Test Results

Passed.
"""
        is_filled, unfilled = validate_execution_log("W17-016.3", body)
        assert is_filled is False
        assert "Problem Analysis" in unfilled


class TestValidateExecutionLogByType:
    """依 type-aware schema 驗證必填章節"""

    def _body(self, pa: str, sol: str, tr: str) -> str:
        return f"""## Problem Analysis

{pa}

## Solution

{sol}

## Test Results

{tr}
"""

    # ---------- ANA: Problem Analysis + Solution 必填 ----------

    def test_ana_all_required_filled_passes(self):
        body = self._body("root cause identified", "adopted fix A", "<!-- optional -->")
        passed, unfilled = validate_execution_log_by_type("ANA", body)
        assert passed is True
        assert unfilled == []

    def test_ana_missing_problem_analysis_fails(self):
        body = self._body("（待填寫：問題根因？）", "adopted fix", "n/a")
        passed, unfilled = validate_execution_log_by_type("ANA", body)
        assert passed is False
        assert "Problem Analysis" in unfilled

    def test_ana_test_results_optional(self):
        """ANA 的 Test Results 為選填，空 placeholder 不影響通過"""
        body = self._body("root cause", "solution picked", "<!-- To be filled -->")
        passed, unfilled = validate_execution_log_by_type("ANA", body)
        assert passed is True
        assert "Test Results" not in unfilled

    # ---------- IMP: Test Results 必填 ----------

    def test_imp_test_results_filled_passes(self):
        body = self._body("<!-- optional -->", "<!-- optional -->", "pytest 42 passed")
        passed, unfilled = validate_execution_log_by_type("IMP", body)
        assert passed is True

    def test_imp_test_results_placeholder_fails(self):
        body = self._body("context", "impl details", "<!-- To be filled by executing agent -->")
        passed, unfilled = validate_execution_log_by_type("IMP", body)
        assert passed is False
        assert "Test Results" in unfilled

    def test_imp_problem_analysis_optional(self):
        """IMP 的 Problem Analysis 為選填"""
        body = self._body("（待填寫：）", "（待填寫：）", "pytest all green")
        passed, unfilled = validate_execution_log_by_type("IMP", body)
        assert passed is True

    # ---------- DOC: 無強制章節 ----------

    def test_doc_has_no_required_body_sections(self):
        body = self._body("<!-- -->", "<!-- -->", "<!-- -->")
        passed, unfilled = validate_execution_log_by_type("DOC", body)
        assert passed is True
        assert unfilled == []

    # ---------- 未知 type 回退通用檢查 ----------

    def test_unknown_type_falls_back_to_generic(self):
        body = self._body("content", "content", "content")
        passed, unfilled = validate_execution_log_by_type("TST", body)
        assert passed is True

    def test_empty_body_for_ana_fails(self):
        passed, unfilled = validate_execution_log_by_type("ANA", "")
        assert passed is False
        assert "Problem Analysis" in unfilled
        assert "Solution" in unfilled


# ============================================================
# W10-133: 非表格描述性 N/A 字面豁免（PC-138 / PC-144 家族延伸）
# ============================================================


class TestDescriptiveNAExemption:
    """`Layer N: N/A` / `Phase X N/A` 等描述性標記不應誤判 placeholder。

    對應 ticket 0.18.0-W10-133：W10-125（PC-138）已修表格 cell N/A 誤判主場景；
    本測試覆蓋「非表格描述性 N/A 字面」家族延伸。
    """

    def _import_is_placeholder(self):
        from ticket_system.lib.ticket_validator import _is_placeholder
        return _is_placeholder

    # ---------- 既有行為 regression 守護 ----------

    def test_table_cell_na_not_placeholder(self):
        """W10-125 regression：表格 cell 內 N/A 仍視為實質內容（非 placeholder）"""
        _is_placeholder = self._import_is_placeholder()
        body = """| Layer | 結論 |
| --- | --- |
| Layer 1 | OK |
| Layer 2 | N/A |
"""
        assert _is_placeholder(body) is False

    def test_bare_na_still_placeholder(self):
        """無描述性前綴的 bare N/A 仍視為 placeholder（規避豁免擴散）"""
        _is_placeholder = self._import_is_placeholder()
        assert _is_placeholder("N/A") is True
        assert _is_placeholder("Found the root cause: N/A") is True

    def test_bare_todo_still_placeholder(self):
        """TODO 不在豁免範圍"""
        _is_placeholder = self._import_is_placeholder()
        assert _is_placeholder("TODO") is True

    # ---------- W10-133 新增豁免案例 ----------

    def test_descriptive_layer_na_not_placeholder(self):
        """`Layer 2: N/A` 描述性標記為合法 ANA 多視角表述，不應判 placeholder"""
        _is_placeholder = self._import_is_placeholder()
        content = """多視角審查結論：

- Layer 1: 已覆蓋
- Layer 2: N/A
- Layer 3: 已覆蓋
"""
        assert _is_placeholder(content) is False

    def test_descriptive_phase_na_not_placeholder(self):
        """`Phase 4 N/A` 描述性標記為合法 TDD 階段表述，不應判 placeholder"""
        _is_placeholder = self._import_is_placeholder()
        content = """TDD 流程記錄：

Phase 3b: 已實作
Phase 4 N/A
"""
        assert _is_placeholder(content) is False

    def test_only_descriptive_na_lines_not_placeholder(self):
        """章節內容僅由描述性 N/A 行組成（作者明示各層皆不適用），不應判 placeholder"""
        _is_placeholder = self._import_is_placeholder()
        content = """Layer 1: N/A
Layer 2: N/A
Layer 3: N/A
"""
        assert _is_placeholder(content) is False

    # ---------- 防豁免擴散：混合情境 ----------

    def test_descriptive_layer_with_todo_still_placeholder(self):
        """`Layer 2: TODO N/A` 含 TODO 仍判 placeholder（豁免不擴散）"""
        _is_placeholder = self._import_is_placeholder()
        # Layer X: N/A 行剝除後仍剩 TODO 行 → 真實 placeholder
        content = """Layer 1: TODO
Layer 2: N/A
"""
        assert _is_placeholder(content) is True

    def test_descriptive_na_mixed_with_bare_na_still_placeholder(self):
        """描述性 N/A 行旁邊有 bare N/A 仍判 placeholder（保守設計）"""
        _is_placeholder = self._import_is_placeholder()
        content = """Layer 1: N/A
這段需要實作: N/A
"""
        assert _is_placeholder(content) is True
