"""测试：JATS XML 解析（结构化表格、许可信息、基线预抽取）"""

import pytest

from app.services.jats_parser import (
    _detect_table1,
    _match_any_keyword,
    _parse_age_value,
    _parse_pct_value,
    _parse_sample_size,
    extract_baseline_data,
    parse_full_text,
    parse_outcome_table_effects,
    parse_all_outcome_effects,
)

# ── 最小 JATS XML（无 DTD，避免 lxml 联网）──

_MINIMAL_JATS = """<?xml version="1.0" encoding="UTF-8"?>
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front>
<article-meta>
<article-id pub-id-type="pmc">PMC12345</article-id>
<title-group><article-title>Test Article</article-title></title-group>
<permissions>
<license xlink:href="http://creativecommons.org/licenses/by/4.0/">
<license-p>This is an open access article under the CC BY 4.0 license.</license-p>
</license>
</permissions>
</article-meta>
</front>
<body>
<sec><title>Introduction</title><p>This is a test.</p></sec>
</body>
</article>"""

_TABLE_JATS = """<?xml version="1.0" encoding="UTF-8"?>
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front><article-meta><title-group><article-title>Test</article-title></title-group></article-meta></front>
<body>
<sec><title>Results</title>
<table-wrap id="t1">
<caption><title>Table 1. Baseline patient characteristics.</title></caption>
<table>
<thead>
<tr><th align="left">Characteristic</th><th align="center">Arm A</th><th align="center">Arm B</th></tr>
</thead>
<tbody>
<tr><td>Age, mean ± SD</td><td>65.2 ± 3.4</td><td>64.8 ± 4.1</td></tr>
<tr><td>Female, n (%)</td><td>45 (48%)</td><td>42 (46%)</td></tr>
<tr><td>ECOG 0, n (%)</td><td>60 (64%)</td><td>58 (63%)</td></tr>
</tbody>
</table>
<table-wrap-foot><fn><p>Data are mean ± SD or n (%)</p></fn></table-wrap-foot>
</table-wrap>
</sec>
</body>
</article>"""


# ── License ──

def test_parse_full_text_minimal():
    r = parse_full_text(_MINIMAL_JATS)
    assert r is not None
    assert "license" in r
    assert r["license"]["type"] == "CC-BY-4.0"
    assert "sections" in r
    assert r["sections"][0]["heading"] == "Introduction"


def test_parse_full_text_empty():
    assert parse_full_text(None) is None
    assert parse_full_text("") is None
    assert parse_full_text("<short/>") is None


# ── Table extraction ──

def test_extract_tables_basic():
    r = parse_full_text(_TABLE_JATS)
    assert r is not None
    tables = r.get("tables")
    assert tables is not None
    assert len(tables) == 1
    tbl = tables[0]
    assert tbl["is_table1"] is True
    assert "Table 1" in tbl["caption"]
    assert tbl["headers"] == ["Characteristic", "Arm A", "Arm B"]
    assert len(tbl["rows"]) == 3
    assert "Age" in tbl["rows"][0][0]
    assert tbl["footnotes"] == ["Data are mean ± SD or n (%)"]


def test_detect_table1_caption():
    """仅标题含 Table 1 不够，还需表头或首列含特征关键词"""
    assert _detect_table1("Table 1. Baseline.", ["Characteristic"], [["Age"]])
    assert not _detect_table1("Table 2. Results.", ["Outcome"], [["ORR"]])
    assert _detect_table1("Table I.", ["Characteristic"], [["Age"]])
    # Caption + header + row 全命中
    assert _detect_table1("Table 1", ["Baseline"], [["Age", "65"]])
    # Caption only
    assert not _detect_table1("Table 1", ["Outcome"], [["ORR"]])
    # Header + Row (no caption match)
    assert _detect_table1("Supplementary Table", ["Characteristic"], [["Age"]])


# ── Keyword matching ──

def test_match_any_keyword():
    kw_set = {"age", "年"}
    assert _match_any_keyword("age, mean ± sd", kw_set)
    assert _match_any_keyword("年龄", kw_set)
    assert not _match_any_keyword("sex", kw_set)


def test_match_any_keyword_regex():
    kw_set = {"sex.*f"}
    assert _match_any_keyword("sex: female", kw_set)
    # _match_any_keyword 内部不做 lowercasing，调用方需自行小写
    assert _match_any_keyword("sex (f)", kw_set)
    assert not _match_any_keyword("Age", kw_set)


# ── Baseline extraction ──

def test_extract_baseline_no_table10():
    """无 Table 1 时不提取"""
    sections = {"sections": [{"heading": "Intro", "text": "Some text"}]}
    r = extract_baseline_data(sections)
    assert r is None


def test_extract_baseline_table1():
    sections = {
        "tables": [{
            "is_table1": True,
            "caption": "Table 1. Baseline.",
            "headers": ["Characteristic", "Arm A", "Arm B"],
            "rows": [
                ["No. of patients", "100", "98"],
                ["Age, mean ± SD", "65.2 ± 3.4", "64.8 ± 4.1"],
                ["Female, n (%)", "45 (48%)", "42 (46%)"],
                ["ECOG 0, n (%)", "60 (64%)", "58 (63%)"],
                ["Smoking, n (%)", "30 (32%)", "28 (31%)"],
            ],
        }]
    }
    r = extract_baseline_data(sections)
    assert r is not None
    assert r["table1_caption"] == "Table 1. Baseline."
    assert r["arms"] == ["Arm A", "Arm B"]
    extracted = r["extracted_fields"]
    assert "sample_size" in extracted
    assert extracted["sample_size"]["Arm A"] == "100"
    assert extracted["sample_size"]["Arm B"] == "98"
    assert "age" in extracted
    assert extracted["age"]["Arm A"] == "65.2 ± 3.4"
    assert "female" in extracted
    assert "ecog" in extracted
    assert "smoking" in extracted


def test_extract_baseline_from_sections():
    """无表格时从段落正则提取"""
    sections = {
        "sections": [{"heading": "Results", "text": "A total of N=200 patients were enrolled. Mean age = 65.2 years."}]
    }
    r = extract_baseline_data(sections)
    assert r is not None
    assert "sample_size" in r["extracted_fields"]
    assert "age" in r["extracted_fields"]


def test_extract_baseline_arms_from_header():
    """表头中的 Characteristic 列被跳过"""
    sections = {
        "tables": [{
            "is_table1": True,
            "caption": "Table 1",
            "headers": ["Characteristic", "Drug X (N=100)", "Placebo (N=98)"],
            "rows": [["Age", "65", "64"]],
        }]
    }
    r = extract_baseline_data(sections)
    assert r["arms"] == ["Drug X (N=100)", "Placebo (N=98)"]


def test_extract_baseline_no_matching_keyword():
    """行标签不匹配任何已知字段 → 跳过"""
    sections = {
        "tables": [{
            "is_table1": True,
            "caption": "Table 1",
            "headers": ["Char", "A"],
            "rows": [["Some obscure metric", "42"]],
        }]
    }
    r = extract_baseline_data(sections)
    assert r is None


# ── Table parsing edge cases ──

def test_table_colspan():
    """colspan 重复单元格内容"""
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front><article-meta><title-group><article-title>T</article-title></title-group></article-meta></front>
<body>
<sec><p>Background: This study examines outcomes in patients with advanced NSCLC treated with
combination immunotherapy. Baseline characteristics are summarized in Table 1 below.</p>
<table-wrap><caption><title>Table 1</title></caption>
<table>
<thead><tr><th>Label</th><th colspan="2">Combined</th></tr></thead>
<tbody><tr><td>Age</td><td>65</td><td>64</td></tr></tbody>
</table>
</table-wrap>
</sec>
</body>
</article>"""
    r = parse_full_text(xml)
    assert r is not None
    tbl = r["tables"][0]
    # <th>Label</th> + <th colspan="2">Combined</th> → colspan 展开为 ["Combined", "Combined"]
    assert len(tbl["headers"]) == 3
    assert tbl["headers"] == ["Label", "Combined", "Combined"]
    assert tbl["rows"] == [["Age", "65", "64"]]


def test_table_no_thead():
    """无 <thead> 时从第一行 <tr> 取表头"""
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front><article-meta><title-group><article-title>T</article-title></title-group></article-meta></front>
<body>
<sec><p>This study enrolled 200 patients with NSCLC across multiple centers. Baseline
characteristics and demographics are shown in the table below.</p>
<table-wrap><caption><title>Table 1</title></caption>
<table>
<tr><th>Char</th><th>A</th><th>B</th></tr>
<tr><td>Age</td><td>65</td><td>64</td></tr>
</table>
</table-wrap>
</sec>
</body>
</article>"""
    r = parse_full_text(xml)
    assert r is not None
    tbl = r["tables"][0]
    assert tbl["headers"] == ["Char", "A", "B"]
    assert tbl["rows"] == [["Age", "65", "64"]]


def test_table_no_headers():
    """既无 <thead> 也无 <th> → 无表头"""
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front><article-meta><title-group><article-title>T</article-title></title-group></article-meta></front>
<body>
<sec><p>This section presents the primary analysis results from the randomized controlled
trial comparing Drug X versus placebo in patients with advanced melanoma.</p>
<table-wrap><caption><title>Table</title></caption>
<table>
<tbody><tr><td>Data</td><td>1</td></tr></tbody>
</table>
</table-wrap>
</sec>
</body>
</article>"""
    r = parse_full_text(xml)
    assert r is not None
    tbl = r["tables"][0]
    assert tbl["headers"] == []
    assert tbl["rows"] == [["Data", "1"]]


# ── License extraction ──

CC_LICENSE_TESTS = [
    ("creativecommons.org/licenses/by/4.0", "CC-BY-4.0"),
    ("creativecommons.org/licenses/by-nc/4.0", "CC-BY-NC-4.0"),
    ("creativecommons.org/licenses/by/3.0", "CC-BY-3.0"),
    ("creativecommons.org/licenses/by-nc/3.0", "CC-BY-NC-3.0"),
    ("creativecommons.org/licenses/by/2.0", "CC-BY-2.0"),
    ("creativecommons.org/licenses/by-nc-nd/4.0", "CC-BY-NC-ND"),
    ("creativecommons.org/licenses/by-sa/4.0", "CC-BY-SA"),
    ("creativecommons.org/publicdomain/zero/1.0", "CC0"),
    ("creativecommons.org/publicdomain/mark/1.0", "PDM"),
    ("creativecommons.org/licenses/by/2.5", "CC-BY-2.5"),
]


def _make_article_with_license(href: str) -> str:
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
<article xmlns:xlink="http://www.w3.org/1999/xlink">
<front><article-meta>
<title-group><article-title>T</article-title></title-group>
<permissions>
<license xlink:href="{href}">
<license-p>Open access</license-p>
</license>
</permissions>
</article-meta></front>
<body>
<sec><title>Introduction</title><p>This is the introduction section of the test article examining
the efficacy of a novel therapeutic agent in the treatment of advanced malignancies.
The study enrolled patients with histologically confirmed diagnoses.</p></sec>
</body>
</article>"""


def test_all_cc_licenses():
    for href, expected in CC_LICENSE_TESTS:
        r = parse_full_text(_make_article_with_license(href))
        assert r is not None, f"Failed to parse for {href}"
        assert r["license"]["type"] == expected, f"For {href}: got {r['license']['type']}, expected {expected}"


# ═══ 数值解析测试 ═══

@pytest.mark.parametrize("raw,expected", [
    ("58.3±12.1", {"mean": 58.3, "sd": 12.1, "format": "mean_sd"}),
    ("58.3 ± 12.1", {"mean": 58.3, "sd": 12.1, "format": "mean_sd"}),
    ("58.3 (12.1)", {"mean": 58.3, "sd": 12.1, "format": "mean_sd"}),
    ("58.3, SD 12.1", {"mean": 58.3, "sd": 12.1, "format": "mean_sd"}),
    ("median 59 (IQR 48-71)", {"median": 59, "q1": 48, "q3": 71, "format": "median_iqr"}),
    ("median 59 (IQR 48 – 71)", {"median": 59, "q1": 48, "q3": 71, "format": "median_iqr"}),
    ("median 59 (48-71)", {"median": 59, "q1": 48, "q3": 71, "format": "median_range"}),
    ("18-75", {"min": 18, "max": 75, "format": "range"}),
    ("18 – 75", {"min": 18, "max": 75, "format": "range"}),
])
def test_parse_age_value(raw, expected):
    result = _parse_age_value(raw)
    assert result is not None, f"Failed to parse age: {raw}"
    for k, v in expected.items():
        if isinstance(v, float):
            assert abs(result.get(k, 0) - v) < 0.01, f"Mismatch for {k} in '{raw}': expected {v}, got {result.get(k)}"
        else:
            assert result.get(k) == v, f"Mismatch for {k} in '{raw}': expected {v}, got {result.get(k)}"


@pytest.mark.parametrize("raw,expected", [
    ("45 (56.2%)", {"n": 45, "pct": 56.2, "format": "n_pct"}),
    ("45/80 (56.2%)", {"n": 45, "total": 80, "pct": 56.2, "format": "frac_pct"}),
    ("45/80 (56.2)", {"n": 45, "total": 80, "pct": 56.2, "format": "frac_pct"}),
    ("56.2%", {"pct": 56.2, "format": "only_pct"}),
    ("45", {"n": 45, "format": "only_n"}),
])
def test_parse_pct_value(raw, expected):
    result = _parse_pct_value(raw)
    assert result is not None, f"Failed to parse pct: {raw}"
    for k, v in expected.items():
        if isinstance(v, float):
            assert abs(result.get(k, 0) - v) < 0.01, f"Mismatch for {k} in '{raw}': expected {v}, got {result.get(k)}"
        else:
            assert result.get(k) == v, f"Mismatch for {k} in '{raw}': expected {v}, got {result.get(k)}"


@pytest.mark.parametrize("raw,expected", [
    ("Arm A (n=100)", {"n": 100}),
    ("Intervention (N = 98)", {"n": 98}),
    ("Control (n≈95)", {"n": 95}),
    ("Placebo", None),
])
def test_parse_sample_size(raw, expected):
    result = _parse_sample_size(raw)
    if expected is None:
        assert result is None, f"Expected None for '{raw}', got {result}"
    else:
        assert result is not None, f"Failed to parse sample_size: {raw}"
        assert result["n"] == expected["n"], f"Mismatch for '{raw}': expected {expected}, got {result}"


def test_extract_baseline_with_parsed_values():
    """验证 extract_baseline_data 返回 parsed_values"""
    xml = """<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
    <article xmlns:xlink="http://www.w3.org/1999/xlink">
    <front><article-meta>
    <title-group><article-title>Test</article-title></title-group>
    </article-meta></front>
    <body>
    <sec><p>Introduction text here that is sufficiently long to pass the 500 character threshold for minimal valid parsing.
    We present our findings from a comprehensive study of advanced non-small cell lung cancer patients treated with
    combination immunotherapy and chemotherapy in a multi-center setting.</p></sec>
    <table-wrap>
    <caption><p>Table 1. Patient Baseline Characteristics</p></caption>
    <table>
    <thead>
    <tr><th>Characteristic</th><th>Arm A (n=100)</th><th>Arm B (n=98)</th></tr>
    </thead>
    <tbody>
    <tr><td>Age</td><td>58.3 ± 12.1</td><td>62.1 ± 11.3</td></tr>
    <tr><td>Female</td><td>45 (56.2%)</td><td>48 (60.0%)</td></tr>
    <tr><td>ECOG 0-1</td><td>80 (80.0%)</td><td>78 (79.6%)</td></tr>
    </tbody>
    </table>
    </table-wrap>
    </body>
    </article>"""
    r = parse_full_text(xml)
    assert r is not None
    baseline = extract_baseline_data(r)
    assert baseline is not None
    assert baseline["parsed_values"] is not None
    # age 应有解析值
    age_parsed = baseline["parsed_values"].get("age", {})
    assert age_parsed, "age should have parsed values"
    arm_a_age = age_parsed.get("Arm A (n=100)")
    assert arm_a_age is not None
    assert arm_a_age.get("mean") == 58.3
    assert arm_a_age.get("sd") == 12.1
    # 表头样本量解析
    assert baseline.get("header_sample_sizes") is not None
    assert baseline["header_sample_sizes"]["Arm A (n=100)"]["n"] == 100


# ═══ Outcome 表识别测试 ═══

from app.services.jats_parser import _detect_outcome_table, _classify_table


def test_detect_outcome_table_pfs():
    """PFS/OS survival table → outcome"""
    assert _detect_outcome_table("Table 2. Progression-Free Survival",
                                  ["Endpoint", "Arm A", "Arm B"],
                                  [["Median PFS", "8.4", "5.2"]])
    # 开头 space and newline format
    assert _detect_outcome_table("Table 3. Efficacy Outcomes",
                                  ["Parameter", "HR", "95% CI"],
                                  [["OS", "0.62", "0.48-0.79"]])


def test_detect_outcome_table_not_baseline():
    """Baseline table should NOT be outcome"""
    assert not _detect_outcome_table("Table 1. Baseline Characteristics",
                                      ["Characteristic", "Arm A"],
                                      [["Age", "58.3"]])


def test_detect_outcome_table_endpoint():
    """Primary endpoint table → outcome"""
    assert _detect_outcome_table("Table 2. Primary and Secondary Endpoints",
                                  ["Endpoint", "Treatment", "Control"],
                                  [["Overall Survival", "12.3", "9.8"]])
    assert _detect_outcome_table("Table 4. Subgroup Analysis of OS",
                                  ["Subgroup", "HR", "95% CI"],
                                  [["Age <65", "0.71", "0.52-0.97"]])


def test_classify_table_outcome():
    """_classify_table() 正确分类 outcome 表"""
    cls = _classify_table("Table 2. Overall Survival",
                           ["Endpoint", "HR", "95% CI"],
                           [["OS", "0.62", "0.48-0.79"]])
    assert cls["is_table1"] is False
    assert cls["is_outcome_table"] is True
    assert cls["table_type"] == "outcome"
    assert "仅供参考" in cls["_note"]


def test_classify_table_baseline():
    """_classify_table() 正确分类 baseline 表"""
    cls = _classify_table("Table 1. Baseline Characteristics",
                           ["Characteristic", "Arm A"],
                           [["Age", "58.3"]])
    assert cls["is_table1"] is True
    assert cls["is_outcome_table"] is False
    assert cls["table_type"] == "baseline"
    assert "仅供参考" in cls["_note"]


def test_classify_table_other():
    """不认识的表格 → other"""
    cls = _classify_table("Table 5. Adverse Events",
                           ["Event", "n (%)"],
                           [["Nausea", "12 (15%)"]])
    assert cls["is_table1"] is False
    assert cls["is_outcome_table"] is False
    assert cls["table_type"] == "other"
    assert cls["_note"] == ""


def test_extract_outcome_tables_from_full_text():
    """parse_full_text + extract_baseline_data 应收集 outcome_tables"""
    xml = """<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN" "journal-publishing.dtd">
    <article xmlns:xlink="http://www.w3.org/1999/xlink">
    <front><article-meta>
    <title-group><article-title>Test</article-title></title-group>
    </article-meta></front>
    <body>
    <sec><p>Introduction text here that is sufficiently long to pass the 500 character threshold for minimal valid parsing.
    We present our findings from a comprehensive study of advanced non-small cell lung cancer patients treated with
    combination immunotherapy and chemotherapy in a multi-center setting.</p>
    <table-wrap><caption><p>Table 1. Baseline Characteristics</p></caption>
    <table>
    <thead><tr><th>Characteristic</th><th>Arm A</th><th>Arm B</th></tr></thead>
    <tbody><tr><td>Age</td><td>65</td><td>64</td></tr></tbody>
    </table>
    </table-wrap>
    <table-wrap><caption><p>Table 2. Efficacy Outcomes (PFS)</p></caption>
    <table>
    <thead><tr><th>Endpoint</th><th>HR</th><th>95% CI</th></tr></thead>
    <tbody><tr><td>PFS</td><td>0.62</td><td>0.48-0.79</td></tr></tbody>
    </table>
    </table-wrap>
    </sec>
    </body>
    </article>"""
    r = parse_full_text(xml)
    assert r is not None
    assert len(r["tables"]) == 2
    assert r["tables"][0]["is_table1"] is True
    assert r["tables"][0]["table_type"] == "baseline"
    assert r["tables"][1]["is_outcome_table"] is True
    assert r["tables"][1]["table_type"] == "outcome"
    assert "仅供参考" in r["tables"][1]["_note"]

    baseline = extract_baseline_data(r)
    assert baseline is not None
    assert len(baseline["outcome_tables"]) == 1
    assert "Efficacy" in baseline["outcome_tables"][0]["caption"]
    assert "仅供参考" in baseline["outcome_tables"][0]["_note"]


# ═══ 效应量数值解析测试 ═══


def test_parse_outcome_table_effects_hr_ci():
    """HR + 95%CI + p value 完整解析"""
    effects = parse_outcome_table_effects(
        caption="Table 2. Overall Survival",
        headers=["Endpoint", "HR (95%CI)", "p value"],
        rows=[
            ["Overall Survival", "HR 0.72 (95% CI 0.60-0.86)", "p=0.002"],
            ["Progression-Free Survival", "HR 0.65 (0.55-0.78)", "p<0.001"],
        ],
    )
    assert len(effects) == 2
    e1 = effects[0]
    assert e1["endpoint"] == "Overall Survival"
    assert e1["effect_type"] == "HR"
    assert e1["value"] == 0.72
    assert e1["ci_lower"] == 0.60
    assert e1["ci_upper"] == 0.86
    assert e1["p_value"] == 0.002


def test_parse_outcome_table_effects_or():
    """OR + 95%CI"""
    effects = parse_outcome_table_effects(
        caption="Table 3. Subgroup Analysis",
        headers=["Subgroup", "OR (95% CI)"],
        rows=[
            ["Age <65", "OR 1.35 (1.12-1.63)"],
            ["Female", "OR 0.89 (0.72-1.10)"],
        ],
    )
    assert len(effects) == 2
    assert effects[0]["effect_type"] == "OR"
    assert effects[0]["value"] == 1.35
    assert effects[0]["ci_lower"] == 1.12
    assert effects[0]["ci_upper"] == 1.63


def test_parse_outcome_table_effects_bare_ci():
    """省略标签的纯 CI 格式"""
    effects = parse_outcome_table_effects(
        caption="Table 4. Efficacy",
        headers=["Endpoint", "95% CI"],
        rows=[
            ["OS", "0.82 (95%CI 0.70-0.97)"],
            ["PFS", "0.71 (0.59-0.85)"],
        ],
    )
    assert len(effects) == 2
    assert effects[0]["effect_type"] is None  # 无标签时推测
    assert effects[0]["value"] == 0.82


def test_parse_outcome_table_effects_p_only():
    """仅 p 值无效应量"""
    effects = parse_outcome_table_effects(
        caption="Table 5. Safety",
        headers=["Event", "p value"],
        rows=[
            ["Grade 3-4 AEs", "p=0.03"],
        ],
    )
    assert len(effects) == 1
    assert effects[0]["endpoint"] == "Grade 3-4 AEs"
    assert effects[0]["value"] is None
    assert effects[0]["p_value"] == 0.03


def test_parse_outcome_table_effects_skip_header_rows():
    """跳过类表头行"""
    effects = parse_outcome_table_effects(
        caption="Table 2. OS",
        headers=["x", "y"],
        rows=[
            ["Endpoint", "HR (CI)", "p value"],
            ["Overall Survival", "HR 0.72 (0.60-0.86)", "p=0.002"],
        ],
    )
    assert len(effects) == 1
    assert effects[0]["endpoint"] == "Overall Survival"


def test_parse_outcome_table_effects_empty():
    """空表返回空列表"""
    assert parse_outcome_table_effects("Table 1", [], []) == []


def test_parse_all_outcome_effects():
    """从结构化 full_text_sections 中提取所有 effect"""
    fts = {
        "tables": [
            {
                "is_outcome_table": True,
                "caption": "Table 2. OS",
                "headers": ["Endpoint", "HR"],
                "rows": [
                    ["Overall Survival", "HR 0.72 (0.60-0.86, p=0.003)"],
                ],
            },
            {
                "is_outcome_table": True,
                "caption": "Table 3. PFS",
                "headers": ["x", "y"],
                "rows": [
                    ["PFS", "HR 0.65 (0.55-0.78, p<0.001)"],
                ],
            },
            {
                "is_outcome_table": False,  # not an outcome table
                "caption": "Table 1. Baseline",
                "headers": ["x", "y"],
                "rows": [["Age", "65"]],
            },
        ]
    }
    effects = parse_all_outcome_effects(fts)
    assert len(effects) == 2
    assert effects[0]["endpoint"] == "Overall Survival"
    assert effects[1]["endpoint"] == "PFS"


def test_parse_all_outcome_effects_empty():
    """无 outcome table 时返回空"""
    assert parse_all_outcome_effects({"tables": []}) == []
    assert parse_all_outcome_effects({}) == []
