# -*- coding: utf-8 -*-
"""把已生成的《软件测试课程设计》课程教学大纲内容填进学院大纲模板。

模板：_tpl2/大纲模板.docx（由 广东金融学院课程教学大纲模板.doc 转换）
输出：广东金融学院课程教学大纲（软件测试课程设计-周宇文）.docx

要点：
  - 保留模板的标题块、11 张表格与字体（宋体/五号）；
  - 表5/表6 原为"第一章/第二章/第三章"的章节块（每块 8 行），这里重建为 16 个次课块并合并到一张表；
  - 模板中蓝色"填写说明"、示例行、"（靠左，空两格、五号、宋体）"等注解在填写完成后删除。
"""
import copy
import importlib.util
import os
import re
import sys

from docx import Document
from docx.shared import Pt
from docx.text.paragraph import Paragraph

BASE = os.path.dirname(os.path.abspath(__file__))


def _load(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


G = _load('gen', os.path.join(BASE, '_gen_教学文件.py'))
C, SESSIONS = G.C, G.SESSIONS
SZ = {}
try:
    SZ = _load('sz', os.path.join(BASE, '_szdesign.py')).SZ_DESIGN
except Exception:
    pass

DELETE_PAT = (
    '填写说明', '用语示例', '标注“★”', '备注：表格中所有蓝色字体', '（制定人原则上为',
    '（靠左，空两格', '课程目标对毕业要求的支撑（靠左', '评价标准（靠左',
    '1.课程目标要支撑', '2.课程体系支撑', '3.每个课程目标', '1.毕业要求序号',
    '2.课程目标与指标点', '3.该表须与', '4.多个课程目标', '5.在课程目标后',
    '1.考核方式一栏', '2.各评价方式', '3.不建议完全', '4.采取的评价方式必须',
    '5.采取的评价方式应确保', '6.考勤以及', '评价标准是对具体考核内容',
    '能够运用……知识表达……', '课程目标评价标准中的课程目标与上述',
)


def set_text(p, text):
    if not p.runs:
        p.add_run(text)
        return
    p.runs[0].text = text
    for r in p.runs[1:]:
        r.text = ''


def set_cell(cell, text):
    """清空单元格并按行写入（显式宋体五号，保证与模板一致）。"""
    while len(cell.paragraphs) > 1:
        cell.paragraphs[-1]._p.getparent().remove(cell.paragraphs[-1]._p)
    lines = str(text).split('\n')
    set_text(cell.paragraphs[0], lines[0])
    p = cell.paragraphs[0]
    for line in lines[1:]:
        el = copy.deepcopy(p._p)
        p._p.addnext(el)
        p = Paragraph(el, cell)
        set_text(p, line)
    for para in cell.paragraphs:
        for r in para.runs:
            r.font.name = '宋体'
            r.font.size = Pt(10.5)


def row_cells(row):
    out, seen = [], set()
    for c in row.cells:
        if id(c._tc) in seen:
            continue
        seen.add(id(c._tc))
        out.append(c)
    return out


def find_row(tbl, label):
    for row in tbl.rows:
        cells = row_cells(row)
        if cells and label in cells[0].text:
            return row
    return None


def clone_row(tbl, idx):
    new = copy.deepcopy(tbl.rows[idx]._tr)
    tbl._tbl.append(new)
    return tbl.rows[-1]


def drop_row(tbl, idx):
    tbl._tbl.remove(tbl.rows[idx]._tr)


def _norm(t):
    # 去掉标点、虚词（和/与/及/的/等）后再比较，"黑盒测试和白盒测试"与"黑盒、白盒与灰盒测试"应判为同一内容
    t = re.sub(r'[\s　、，,。．.·（）()★：:；;]', '', str(t))
    return re.sub(r'[和与及或等的了是之其]', '', t)


def _similar(a, b):
    """字符集 Jaccard 相似度（"和/与"等虚词不同也能识别为同一内容）。"""
    A, B = set(a), set(b)
    if not A or not B:
        return False
    return len(A & B) / float(len(A | B)) >= 0.7


def dedupe_points(points, heads):
    """去掉与"本次课内容"条目重复的知识点标题（避免同一内容列两遍）。"""
    out = []
    np_ = [_norm(x) for x in points]
    for h in heads:
        nh = _norm(h)
        if not nh:
            continue
        if any(nh in np or np in nh or _similar(nh, np) for np in np_ if np):
            continue
        out.append(h)
    return out


def outline_points(s):
    """知识点标题列表（去编号）。"""
    out = []
    for kind, text in G.chapter_outline(s):
        if kind == 'p':
            out.append(re.sub(r'^\s*\d+(\.\d+)*\s*[、\.．]?\s*', '', text).strip())
    return out


def main():
    doc = Document(os.path.join(BASE, '_tpl2', '大纲模板.docx'))

    # ---------- 标题块 ----------
    header = {'课程名称：': C['name'], '课程代码：': C['code'], '开课单位：': C['college'],
              '制定人：': C['teacher'], '审核人：': C['reviewer'], '制定时间：': C['date']}
    for p in doc.paragraphs:
        t = p.text.strip()
        for k, v in header.items():
            if t.startswith(k):
                set_text(p, k + v)
        if t.startswith('《XXX》课程教学大纲'):
            set_text(p, '《%s》课程教学大纲' % C['name'])
        if t.startswith('二、课程简介'):
            set_text(p, '二、课程简介')
        if t.startswith('三、课程目标'):
            set_text(p, '三、课程目标★')
        if t.startswith('（二）评价标准'):
            set_text(p, '（二）评价标准')
        if t.strip() == '课程目标评价标准':
            set_text(p, '2.课程目标评价标准')
        if t.strip() == '考核与评价★':
            set_text(p, '六、考核与评价★')

    t_info, t_intro, t_goal, t_map = doc.tables[0], doc.tables[1], doc.tables[2], doc.tables[3]
    t_chap, t_chap2, t_assess, t_std, t_goalstd = doc.tables[4], doc.tables[5], doc.tables[6], doc.tables[7], doc.tables[8]
    t_ref, t_sign = doc.tables[9], doc.tables[10]

    # ---------- 表1 课程信息 ----------
    pairs = [
        ('（中文）', C['name']), ('（英文）', C['en']),
        ('课程编码', C['code']), ('课程性质', '必修（　）　选修（ √ ）　专业选修'),
        ('课程类型', '专业课程 / 实践课程'), ('适用专业', C['major']),
        ('开课部门', C['college']), ('课程负责人', C['teacher']),
        ('授课语言', '中文'), ('先修课程', C['prereq']),
    ]
    for label, value in pairs:
        for row in t_info.rows:
            cells = row_cells(row)
            for i, cell in enumerate(cells):
                if label in cell.text:
                    if i + 1 < len(cells):
                        set_cell(cells[i + 1], value)
                    else:
                        set_cell(cell, cell.text.replace(label, label + value))
                    break
    for row in t_info.rows:
        cells = row_cells(row)
        if cells and '总学分' in cells[0].text:
            set_cell(cells[0], '总学分：%d' % C['credits'])
            if len(cells) > 1: set_cell(cells[1], '理论：%d' % C['theory'])
            if len(cells) > 2: set_cell(cells[2], '实验：%d' % C['practice'])
            if len(cells) > 3: set_cell(cells[3], '实践：0')
            if len(cells) > 4: set_cell(cells[4], '总学时：%d' % C['hours'])
            if len(cells) > 5: set_cell(cells[5], '理论：%d' % C['theory'])
            if len(cells) > 6: set_cell(cells[6], '实验：%d' % C['practice'])
            if len(cells) > 7: set_cell(cells[7], '实践：0')

    # ---------- 表2 课程简介（约 300 字）----------
    intro = ('课程的学科背景：软件测试是软件工程的重要组成部分，是保证软件质量、降低质量成本的关键活动。'
             '开设目的和意义：学生掌握软件测试的基本理论、方法、技术与常用工具，熟悉测试流程与规范，'
             '能针对具体软件系统完成测试需求分析、测试计划、用例设计与执行、缺陷跟踪与测试报告撰写，'
             '培养规范化、工程化的测试实践能力与团队协作能力。'
             '主要内容：软件测试基本概念与方法、测试流程与规范、单元与集成测试、系统功能测试、专项测试、'
             '本地化测试、测试自动化、测试需求分析与计划、用例设计与维护、测试基础设施、测试执行与报告。'
             '课程特色与思政教育：以"质量即责任"为主线，结合《血狮》、火星探测器等真实案例，'
             '融入工程伦理、工匠精神、法治意识与科技报国教育。'
             '教学与考核方式：多媒体授课＋实验实践，考查（过程性考核＋课程设计作品与答辩）。'
             '学生通过学习可获得：设计测试用例、使用自动化工具完成完整项目测试的能力，具备测试分析与工程实践素养。')
    set_cell(t_intro.rows[0].cells[0], intro)

    # ---------- 表3 课程目标 ----------
    goals = ['课程目标1：掌握软件测试的基本概念、理论与方法，理解测试在软件工程与质量保证中的地位和作用。',
             '课程目标2：能够针对具体需求开展测试分析，设计测试计划与测试用例，并选择合适的测试方法。',
             '课程目标3：能够搭建测试环境、执行测试、使用自动化工具，规范记录与跟踪缺陷。',
             '课程目标4：能够评估测试结果、撰写测试报告，具备团队协作与工程表达能力。']
    set_cell(t_goal.rows[0].cells[0], '\n'.join(goals))

    # ---------- 表4 课程目标与毕业要求对应 ----------
    # 直接采用我们自己大纲里的对应关系（5 条），并解除模板的纵向合并以便逐行填写
    from docx.oxml.ns import qn as _qn
    our_map = None
    for b in G.doc_大纲():
        if b[0] == 'table' and b[1] and '毕业要求' in str(b[1][0][0]) and '课程目标' in str(b[1][0][-1]):
            our_map = b[1][1:]
            break
    mapping = our_map or []
    while len(t_map.rows) > 1 + len(mapping):
        drop_row(t_map, len(t_map.rows) - 1)
    while len(t_map.rows) < 1 + len(mapping):
        clone_row(t_map, len(t_map.rows) - 1)
    for row in t_map.rows[1:]:
        tcs = row._tr.findall(_qn('w:tc'))
        if tcs:
            tcPr = tcs[0].find(_qn('w:tcPr'))
            vm = tcPr.find(_qn('w:vMerge')) if tcPr is not None else None
            if vm is not None:
                tcPr.remove(vm)
    for i, data in enumerate(mapping, start=1):
        cells = row_cells(t_map.rows[i])
        for j, val in enumerate(data):
            if j < len(cells):
                set_cell(cells[j], val)

    # ---------- 五、教学内容：把章块重建为 16 次课 ----------
    # 表5 前 8 行为一个完整章块，其余为第二章片段 -> 先删掉片段行
    while len(t_chap.rows) > 8:
        drop_row(t_chap, len(t_chap.rows) - 1)
    # 表6 整体不再需要（章块合并到表5）
    t_chap2._tbl.getparent().remove(t_chap2._tbl)

    def fill_block(base_idx, s):
        """把第 base_idx..base_idx+7 行的次课块填成本次课内容（标签列保留模板原样）。"""
        rows = [t_chap.rows[base_idx + k] for k in range(8)]
        head = row_cells(rows[0])
        set_cell(head[0], '第 %d 次课：%s　%s' % (s['no'], s['chapter'], s['title']))
        if len(head) > 1: set_cell(head[1], '学时：2')
        if len(head) > 2: set_cell(head[2], '支撑课程目标：' + s['support'].replace('课程目标', ''))
        set_cell(row_cells(rows[1])[1],
                 '\n'.join(s['points'] + dedupe_points(s['points'], outline_points(s))))
        set_cell(row_cells(rows[2])[1], '\n'.join('%d.%s' % (i, x) for i, x in enumerate(s['goal'], 1)))
        set_cell(row_cells(rows[3])[1], '重点：' + '；'.join(s['key']) + '\n难点：' + '；'.join(s['hard']))
        set_cell(row_cells(rows[4])[1],
                 '学生已修程序设计基础、软件工程、数据库原理，具备编程与调试基础；本次课围绕'
                 + s['key'][0] + '展开，宜用真实案例与动手实验帮助学生建立直观认识。')
        set_cell(row_cells(rows[5])[1],
                 '1.必读书目：《软件测试方法和技术（第 4 版）》%s\n'
                 '2.选读书目：《全程软件测试（第 3 版）》《致命 Bug——软件缺陷的灾难与启示》\n'
                 '3.思考问题：%s\n4.其他课前准备：安装并检查实验环境与工具。' % (s['chapter'], s['case']))
        teach = [s['method']] + list(s['org'][:3])
        if s['no'] in SZ:
            teach.append('课程思政：%s（融入载体与观测点见课程教案）' % SZ[s['no']][0])
        set_cell(row_cells(rows[6])[1], '\n'.join('%d.%s' % (i, x) for i, x in enumerate(teach, 1)))
        set_cell(row_cells(rows[7])[1], '\n'.join('%d.%s' % (i, x) for i, x in enumerate(s['hw'], 1)))

    def append_block(s):
        """按模板块整块克隆（保持各行样式），再填内容。"""
        for k in range(8):
            tbl_new = copy.deepcopy(t_chap.rows[k]._tr)
            t_chap._tbl.append(tbl_new)
        fill_block(len(t_chap.rows) - 8, s)

    fill_block(0, SESSIONS[0])
    for s in SESSIONS[1:]:
        append_block(s)

    # ---------- 表7 考核方式 ----------
    assess = [
        ('考勤与课堂表现', '出勤、课堂讨论与练习参与', '10', '课程目标1'),
        ('实验与阶段任务', '各章实验任务的完成质量、规范性与提交及时性', '40', '课程目标1、2、3'),
        ('测试计划与用例设计', '测试范围与策略、用例覆盖与规范程度', '15', '课程目标2'),
        ('测试执行与缺陷报告', '执行记录完整性、缺陷可复现性与跟踪闭环', '15', '课程目标3'),
        ('测试报告与答辩', '报告结构完整性、数据准确性、答辩表达与回应', '20', '课程目标4'),
    ]
    while len(t_assess.rows) < 1 + len(assess):
        clone_row(t_assess, 1)
    while len(t_assess.rows) > 1 + len(assess):
        drop_row(t_assess, len(t_assess.rows) - 1)
    for i, data in enumerate(assess, start=1):
        cells = row_cells(t_assess.rows[i])
        for j, val in enumerate(data):
            if j < len(cells):
                set_cell(cells[j], val)

    # ---------- 表8 考核方式评价标准 ----------
    levels = {
        '考勤与课堂表现': [
            '全勤，课堂讨论积极，观点有依据，主动帮助同学。', '出勤良好，能参与讨论并回答问题。',
            '偶有缺勤，能基本参与课堂活动。', '缺勤较多，课堂参与不足。', '缺勤严重或基本不参与课堂活动。'],
        '实验与阶段任务': [
            '实验步骤规范、数据真实可追溯、按时高质量提交。', '实验规范、数据完整、按时提交。',
            '实验基本规范、数据基本完整，偶有迟交。', '实验不够规范，数据有缺漏或多次迟交。',
            '实验未完成或数据无法追溯。'],
        '测试计划与用例设计': [
            '范围清晰、策略合理、用例覆盖充分且步骤与预期结果规范。', '范围与策略合理，用例覆盖较充分。',
            '计划与用例基本完整，覆盖存在个别遗漏。', '计划与用例不够完整，覆盖明显不足。',
            '未提交或计划与用例质量差。'],
        '测试执行与缺陷报告': [
            '执行记录完整，缺陷可复现、描述规范、跟踪闭环。', '执行记录较完整，缺陷描述清楚并可复现。',
            '执行记录基本完整，缺陷描述基本规范。', '记录有缺漏，缺陷复现信息不足。',
            '未执行或缺陷记录缺失。'],
        '测试报告与答辩': [
            '报告结构完整、数据准确、结论有据；答辩表达清楚、回答准确。', '报告完整、结论合理；答辩表达较清楚。',
            '报告基本完整，结论基本有依据；答辩基本能回应。', '报告不够完整，结论依据不足；答辩回应有困难。',
            '未提交报告或答辩无法回应。'],
    }
    while len(t_std.rows) < 2 + len(levels):
        clone_row(t_std, 2)
    while len(t_std.rows) > 2 + len(levels):
        drop_row(t_std, len(t_std.rows) - 1)
    for i, (name, desc) in enumerate(levels.items(), start=2):
        cells = row_cells(t_std.rows[i])
        set_cell(cells[0], name)
        if len(cells) > 1:
            set_cell(cells[1], assess[i - 2][1])
        for j, text in enumerate(desc):
            if 2 + j < len(cells):
                set_cell(cells[2 + j], text)

    # ---------- 表9 课程目标评价标准 ----------
    goal_std = [
        ('课程目标1', '考查测试基本概念、理论与方法的掌握',
         ['概念准确、论述完整，能举一反三。', '概念准确，论述较完整。', '概念基本准确，论述基本完整。',
          '概念部分掌握，论述有缺漏。', '概念不清，论述错误较多。']),
        ('课程目标2', '考查测试分析与用例设计能力',
         ['分析全面，用例覆盖充分、步骤规范。', '分析较全面，用例覆盖较充分。', '分析基本到位，用例基本规范。',
          '分析不足，用例覆盖不够。', '分析与用例设计不符合要求。']),
        ('课程目标3', '考查环境搭建、执行与工具使用能力',
         ['环境搭建熟练，执行规范，工具使用恰当。', '能独立搭建环境并规范执行。', '在指导下能完成环境搭建与执行。',
          '环境搭建与执行需较多帮助。', '无法完成环境搭建与执行。']),
        ('课程目标4', '考查结果评估、报告撰写与表达能力',
         ['评估准确、报告规范、表达清晰有说服力。', '评估合理、报告较规范、表达清楚。', '评估基本合理、报告基本规范。',
          '评估依据不足、报告不够规范。', '未形成有效评估与报告。']),
    ]
    while len(t_goalstd.rows) < 2 + len(goal_std):
        clone_row(t_goalstd, 2)
    while len(t_goalstd.rows) > 2 + len(goal_std):
        drop_row(t_goalstd, len(t_goalstd.rows) - 1)
    for i, (name, basis, desc) in enumerate(goal_std, start=2):
        cells = row_cells(t_goalstd.rows[i])
        set_cell(cells[0], name)
        if len(cells) > 1: set_cell(cells[1], basis)
        for j, text in enumerate(desc):
            if 2 + j < len(cells):
                set_cell(cells[2 + j], text)

    # ---------- 表10 教材与文献 ----------
    set_cell(t_ref.rows[0].cells[0],
             '1.自编讲义：无（使用教材与课件）。\n'
             '2.选用教材：' + C['textbook'] + '\n'
             '3.参考文献：\n' + '\n'.join('%d）%s' % (i, r) for i, r in enumerate(C['refs'][:8], 1)) +
             '\n4.其他教学资源：课程 HTML 课件（第 1 章 40 页，含交互练习与案例）、参考资料 10 份、'
             '实验环境与工具（JUnit、Postman/JMeter、Selenium/Playwright、SonarQube 等）。')

    # ---------- 表11 签字 ----------
    cells = row_cells(t_sign.rows[0])
    if len(cells) >= 3:
        set_cell(cells[0], '制定人：' + C['teacher'])
        set_cell(cells[1], '审订人：' + C['reviewer'])
        set_cell(cells[2], '审批人：')
    cells = row_cells(t_sign.rows[1])
    if cells:
        set_cell(cells[-1], '时间：2026 年 9 月 13 日')

    # ---------- 删除模板填写说明 ----------
    removed = 0
    for p in list(doc.paragraphs):
        t = p.text.strip()
        if not t:
            continue
        if any(k in t for k in DELETE_PAT):
            p._p.getparent().remove(p._p)
            removed += 1

    out = os.path.join(BASE, '广东金融学院课程教学大纲（软件测试课程设计-周宇文）.docx')
    doc.save(out)
    print('已生成：%s' % out)
    print('删除填写说明 %d 段；表格数 %d；次课块 %d' % (removed, len(Document(out).tables),
          (len(Document(out).tables[4].rows)) // 8))
    return 0


if __name__ == '__main__':
    sys.exit(main())
