package com.njcn.gather.report.util;

import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.Tbl;
import org.docx4j.wml.Tc;
import org.docx4j.wml.Text;
import org.docx4j.wml.Tr;
import org.junit.Test;

import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class ItemAnchorAssemblerTest {

    private static final String W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

    private final ObjectFactory f = new ObjectFactory();

    private P para(String text) {
        P p = f.createP();
        R r = f.createR();
        Text t = f.createText();
        t.setValue(text);
        r.getContent().add(t);
        p.getContent().add(r);
        return p;
    }

    private Tc cell(String text) {
        Tc tc = f.createTc();
        tc.getContent().add(para(text));
        return tc;
    }

    private Tr row(String... cellTexts) {
        Tr tr = f.createTr();
        for (String t : cellTexts) {
            tr.getContent().add(f.createTrTc(cell(t)));
        }
        return tr;
    }

    /** 模板：1 行表头 + 1 行 key 行（standard/test/error/result） */
    private Docx4jUtil.HeadingContent template(String... keys) {
        Tbl tbl = f.createTbl();
        String[] headers = new String[keys.length];
        Arrays.fill(headers, "表头");
        tbl.getContent().add(row(headers));
        tbl.getContent().add(row(keys));
        JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
        Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
        hc.setHeadingText("FREQ");
        hc.addSubContent(el);
        return hc;
    }

    private Docx4jUtil.HeadingContent hvTemplate(String... keys) {
        Tbl tbl = f.createTbl();
        tbl.getContent().add(row("谐波电压检验结果", "基波电压（V）", "57.7000"));
        tbl.getContent().add(row("总谐波畸变率（%）", "", "standardThd", "", ""));
        tbl.getContent().add(row("测点", "标准源输出值", "被检仪器测量值", "误差", "标准限值", "检验结论"));
        tbl.getContent().add(row("", "", "A", "B", "C", "A", "B", "C", "", "A", "B", "C"));
        tbl.getContent().add(row("THD", "standardThd", "thdA", "thdB", "thdC", "", "", "", "", "", "", ""));
        tbl.getContent().add(row(keys));
        JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
        Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
        hc.setHeadingText("HV");
        hc.addSubContent(el);
        return hc;
    }

    private Docx4jUtil.HeadingContent hpTemplate(String... keys) {
        Tbl tbl = f.createTbl();
        tbl.getContent().add(row("谐波功率检验结果", "基波电压（V）", "", "基波电流（A）", ""));
        tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论"));
        tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", ""));
        tbl.getContent().add(row(keys));
        JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
        Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
        hc.setHeadingText("HP");
        hc.addSubContent(el);
        return hc;
    }

    private Docx4jUtil.HeadingContent hpTemplateWithSplitHeader(String... keys) {
        Tbl tbl = f.createTbl();
        tbl.getContent().add(row("谐波功率检验结果", "基波电压（V）", ""));
        tbl.getContent().add(row("", "基波电流（A）", ""));
        tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论"));
        tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", ""));
        tbl.getContent().add(row(keys));
        JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
        Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
        hc.setHeadingText("HP");
        hc.addSubContent(el);
        return hc;
    }

    private SingleTestResult resultWithRows(String errorScope, List<Map<String, String>> rows) {
        Map<String, List<Map<String, String>>> byError = new LinkedHashMap<>();
        byError.put(errorScope, rows);
        Map<String, List<Map<String, List<Map<String, String>>>>> detail = new LinkedHashMap<>();
        detail.put("额定工作条件", Collections.singletonList(byError));
        SingleTestResult r = new SingleTestResult();
        r.setDetail(detail);
        return r;
    }

    private String cellText(Tbl tbl, int rowIdx, int colIdx) {
        Tr tr = (Tr) tbl.getContent().get(rowIdx);
        @SuppressWarnings("unchecked")
        JAXBElement<Tc> tc = (JAXBElement<Tc>) tr.getContent().get(colIdx);
        return Docx4jUtil.getTextFromCell(tc.getValue());
    }

    @Test
    public void buildTables_fillsRowsAndRemovesKeyRow() {
        Docx4jUtil.HeadingContent tpl = template("standard", "test", "error", "result");
        Map<String, String> row1 = new HashMap<>();
        row1.put("standard", "50.0000");
        row1.put("test", "50.0001");
        row1.put("error", "0.0001");
        row1.put("result", "合格");
        Map<String, String> row2 = new HashMap<>(row1);
        row2.put("standard", "48.0000");
        SingleTestResult r = resultWithRows("0.01", Arrays.asList(row1, row2));
        List<Object> out = ItemAnchorAssembler.buildTables("FREQ", Collections.singletonList(r), tpl,
                Arrays.asList("standard", "test", "error", "result"), f);
        assertEquals(1, out.size());
        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        // 表头 + 2 行数据（key 行已删）
        assertEquals(3, tbl.getContent().size());
        assertEquals("50.0000", cellText(tbl, 1, 0));
        assertEquals("48.0000", cellText(tbl, 2, 0));
        assertEquals("合格", cellText(tbl, 1, 3));
        // 原模板未被改动（深拷贝）
        @SuppressWarnings("unchecked")
        Tbl original = ((JAXBElement<Tbl>) tpl.getSubContent().get(0)).getValue();
        assertEquals(2, original.getContent().size());
    }

    @Test
    public void buildTables_missingKeysBlank_andErrorScopeInjected() {
        Docx4jUtil.HeadingContent tpl = template("time", "standard", "errorScope", "posSeq");
        Map<String, String> row1 = new HashMap<>();
        row1.put("standard", "0.0000"); // 无 time/posSeq/errorScope
        SingleTestResult r = resultWithRows("0.15%", Collections.singletonList(row1));
        List<Object> out = ItemAnchorAssembler.buildTables("IMBV", Collections.singletonList(r), tpl,
                Arrays.asList("time", "standard", "errorScope", "posSeq"), f);
        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("测点1", cellText(tbl, 1, 0));   // time 缺失 → 测点{n} 兜底
        assertEquals("0.15%", cellText(tbl, 1, 2));  // errorScope ← 分组 key
        assertEquals("", cellText(tbl, 1, 3));       // 数据侧没有的列留空
    }

    @Test
    public void buildTables_harmonicVoltageAndPowerStripErrorScopeUnit() {
        Docx4jUtil.HeadingContent tpl = template("time", "errorScope");
        Map<String, String> row = new HashMap<>();
        row.put("time", "2");
        SingleTestResult hv = resultWithRows("0.15%", Collections.singletonList(row));
        SingleTestResult hp = resultWithRows("-0.1~0.1W", Collections.singletonList(row));

        List<Object> hvOut = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(hv), tpl,
                Arrays.asList("time", "errorScope"), f);
        List<Object> hpOut = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(hp), tpl,
                Arrays.asList("time", "errorScope"), f);

        @SuppressWarnings("unchecked")
        Tbl hvTbl = ((JAXBElement<Tbl>) hvOut.get(0)).getValue();
        @SuppressWarnings("unchecked")
        Tbl hpTbl = ((JAXBElement<Tbl>) hpOut.get(0)).getValue();
        assertEquals("0.15", cellText(hvTbl, 1, 1));
        assertEquals("-0.1~0.1", cellText(hpTbl, 1, 1));
    }

    @Test
    public void buildTables_powerMapping_sumsThreePhases() {
        Docx4jUtil.HeadingContent tpl = template("standardV", "activePower", "reactivePower");
        Map<String, String> row1 = new HashMap<>();
        row1.put("testA", "288.4515");
        row1.put("testB", "288.4515");
        row1.put("testC", "288.4515");
        SingleTestResult r = resultWithRows("1%", Collections.singletonList(row1));
        List<Object> out = ItemAnchorAssembler.buildTables("P", Collections.singletonList(r), tpl,
                Arrays.asList("standardV", "activePower", "reactivePower"), f);
        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("", cellText(tbl, 1, 0));            // 设置值数据侧暂无 → 留空
        assertEquals("865.3545", cellText(tbl, 1, 1));    // 有功 = 三相实测之和
        assertEquals("", cellText(tbl, 1, 2));            // 无功暂无 → 留空
    }

    @Test
    public void buildTables_powerMapping_keepsExistingActivePower() {
        Docx4jUtil.HeadingContent tpl = template("activePower");
        Map<String, String> row1 = new HashMap<>();
        row1.put("activePower", "1000.0000");
        row1.put("testA", "1.0");
        row1.put("testB", "2.0");
        row1.put("testC", "3.0");
        SingleTestResult r = resultWithRows("1%", Collections.singletonList(row1));

        List<Object> out = ItemAnchorAssembler.buildTables("P", Collections.singletonList(r), tpl,
                Collections.singletonList("activePower"), f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("1000.0000", cellText(tbl, 1, 0));
    }

    @Test
    public void buildTables_multipleResults_multipleTables() {
        Docx4jUtil.HeadingContent tpl = template("time", "standard");
        Map<String, String> row = new HashMap<>();
        row.put("time", "2");
        row.put("standard", "0.5000");
        SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
        SingleTestResult r2 = resultWithRows("0.05", Collections.singletonList(row));
        SingleTestResult empty = new SingleTestResult(); // detail 为 null → 跳过
        List<Object> out = ItemAnchorAssembler.buildTables("HV", Arrays.asList(r1, empty, r2), tpl,
                Arrays.asList("time", "standard"), f);
        // 空结果跳过 → 2 张表；多表之间允许有空段落防止 Word 合并，但不得出现可见的表外"测点"标题
        assertEquals(2, countTables(out));
        assertTrue(nonBlankParagraphTexts(out).isEmpty());
    }

    /** HP 多测点：测点号必须写在表格首行标题单元格内，不能生成表外可见标题段 */
    @Test
    public void buildTables_harmonicPowerMultipleTables_putsPointCaptionInTitleCell() {
        List<String> keys = Arrays.asList("time", "standard");
        Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0]));
        Map<String, String> row = new HashMap<>();
        row.put("time", "3");
        row.put("standard", "0.5000");
        SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
        SingleTestResult r2 = resultWithRows("0.05", Collections.singletonList(row));
        List<Object> out = ItemAnchorAssembler.buildTables("HP", Arrays.asList(r1, r2), tpl, keys, f);

        assertEquals(2, countTables(out));
        assertTrue(nonBlankParagraphTexts(out).isEmpty());
        List<Tbl> tables = collectTables(out);
        assertEquals("谐波功率检验结果（测点1）", cellText(tables.get(0), 0, 0));
        assertEquals("谐波功率检验结果（测点2）", cellText(tables.get(1), 0, 0));
    }

    @Test
    public void buildTables_harmonicPowerFillsBaseVoltageAndCurrentHeaderCells() {
        List<String> keys = Arrays.asList("time", "standard");
        Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0]));
        Map<String, String> row = new HashMap<>();
        row.put("time", "3");
        row.put("standard", "23.0800");
        row.put("standardV", "57.7000");
        row.put("standardI", "5.0000");
        SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("57.7000", cellText(tbl, 0, 2));
        assertEquals("5.0000", cellText(tbl, 0, 4));
    }

    @Test
    public void buildTables_harmonicPowerFillsCurrentWhenHeaderIsOnSecondRow() {
        List<String> keys = Arrays.asList("time", "standard");
        Docx4jUtil.HeadingContent tpl = hpTemplateWithSplitHeader(keys.toArray(new String[0]));
        Map<String, String> row = new HashMap<>();
        row.put("time", "3");
        row.put("standard", "23.0800");
        row.put("standardV", "57.7000");
        row.put("standardI", "5.0000");
        SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("57.7000", cellText(tbl, 0, 2));
        assertEquals("5.0000", cellText(tbl, 1, 2));
    }

    /** F1 边界：单表不加标题段，既有单表输出不变 */
    @Test
    public void buildTables_singleTable_noCaption() {
        Docx4jUtil.HeadingContent tpl = template("time", "standard");
        Map<String, String> row = new HashMap<>();
        row.put("time", "2");
        row.put("standard", "0.5000");
        SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
        List<Object> out = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(r1), tpl,
                Arrays.asList("time", "standard"), f);
        assertEquals(1, out.size());
        assertTrue(unwrap(out.get(0)) instanceof Tbl);
    }

    /** F4：bookmarkEnd 漏成 w:tbl 直接子节点（本仓 _GoBack 崩溃同类）时，key 行定位不得 ClassCastException */
    @Test
    public void buildTables_bookmarkEndAsTblChild_noClassCast() {
        Docx4jUtil.HeadingContent tpl = template("standard", "test");
        @SuppressWarnings("unchecked")
        Tbl tplTbl = ((JAXBElement<Tbl>) tpl.getSubContent().get(0)).getValue();
        CTMarkupRange end = f.createCTMarkupRange();
        end.setId(BigInteger.ONE);
        tplTbl.getContent().add(f.createPBookmarkEnd(end));
        Map<String, String> row1 = new HashMap<>();
        row1.put("standard", "50.0000");
        row1.put("test", "50.0001");
        SingleTestResult r = resultWithRows("0.01", Collections.singletonList(row1));
        List<Object> out = ItemAnchorAssembler.buildTables("FREQ", Collections.singletonList(r), tpl,
                Arrays.asList("standard", "test"), f);
        assertEquals(1, out.size());
        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        // 表头 + 数据行（key 行已删，bookmarkEnd 保留不参与行计数）
        List<Tr> trs = collectTrs(tbl);
        assertEquals(2, trs.size());
        assertEquals("50.0000", Docx4jUtil.getTextFromCell(cellOf(trs.get(1), 0)));
    }

    /** F2：模板 key 行取不到占位符属模板错误，必须显性报错而不是静默走"未测→整节隐藏" */
    @Test
    public void requireTableKeys_empty_throwsWithScriptCode() {
        try {
            ItemAnchorAssembler.requireTableKeys("FREQ", Collections.<String>emptyList());
            fail("空 tableKeys 应抛 BusinessException");
        } catch (BusinessException e) {
            assertTrue(String.valueOf(e.getMessage()), String.valueOf(e.getMessage()).contains("FREQ"));
        }
    }

    @Test
    public void requireTableKeys_nonEmpty_passes() {
        ItemAnchorAssembler.requireTableKeys("FREQ", Collections.singletonList("standard"));
    }

    @Test
    public void extractTableKeys_itemAnchorReadsLastRowEvenWhenOldCrossHeuristicRejectsFirstRow() {
        List<String> expected = Arrays.asList("time", "standard", "testA", "testB", "testC",
                "errorA", "errorB", "errorC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(expected.toArray(new String[0]));
        assertTrue("old getTableFillKeys should reject this HV title row",
                Docx4jUtil.getTableFillKeys(Collections.singletonList(tpl)).isEmpty());

        assertEquals(expected, ItemAnchorAssembler.extractTableKeys(Collections.singletonList(tpl)));
    }

    @Test
    public void buildTables_harmonicVoltageCalculatesThdFixedRowFromPhaseRates() {
        List<String> keys = Arrays.asList("time", "standard", "testA", "testB", "testC",
                "errorA", "errorB", "errorC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row1 = harmonicRow("2", "3.0000", "6.0000", "8.0000");
        row1.put("standard", "3.0000");
        Map<String, String> row2 = harmonicRow("3", "4.0000", "8.0000", "15.0000");
        row2.put("standard", "4.0000");
        SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2));

        List<Object> out = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("5.0000", cellText(tbl, 1, 2));
        assertEquals("5.0000", cellText(tbl, 4, 1));
        assertEquals("5.0000", cellText(tbl, 4, 2));
        assertEquals("10.0000", cellText(tbl, 4, 3));
        assertEquals("17.0000", cellText(tbl, 4, 4));
    }

    @Test
    public void buildTables_harmonicCurrentCalculatesRateRowsAndThdFromBaseCurrent() {
        List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
                "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row1 = harmonicRow("2", "1.2000", "1.6000", "2.0000");
        row1.put("standard", "1.0000");
        row1.put("standardRate", "25.0000");
        row1.put("errorA", "0.2000");
        row1.put("errorB", "0.6000");
        row1.put("errorC", "1.0000");
        row1.put("baseCurrentA", "4.0000");
        row1.put("baseCurrentB", "4.0000");
        row1.put("baseCurrentC", "4.0000");
        Map<String, String> row2 = harmonicRow("3", "0.9000", "1.2000", "1.5000");
        row2.put("standard", "1.0000");
        row2.put("standardRate", "25.0000");
        row2.put("errorA", "-0.1000");
        row2.put("errorB", "0.2000");
        row2.put("errorC", "0.5000");
        row2.put("baseCurrentA", "4.0000");
        row2.put("baseCurrentB", "4.0000");
        row2.put("baseCurrentC", "4.0000");
        SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2));

        List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("35.3553", cellText(tbl, 1, 2));
        assertEquals("35.3553", cellText(tbl, 4, 1));
        assertEquals("37.5000", cellText(tbl, 4, 2));
        assertEquals("50.0000", cellText(tbl, 4, 3));
        assertEquals("62.5000", cellText(tbl, 4, 4));
        assertEquals("25.0000", cellText(tbl, 5, 1));
        assertEquals("30.0000", cellText(tbl, 5, 2));
        assertEquals("40.0000", cellText(tbl, 5, 3));
        assertEquals("50.0000", cellText(tbl, 5, 4));
        assertEquals("5.0000", cellText(tbl, 5, 5));
        assertEquals("15.0000", cellText(tbl, 5, 6));
        assertEquals("25.0000", cellText(tbl, 5, 7));
        assertEquals("±1.2500", cellText(tbl, 5, 8));
        assertEquals("±1.2500", cellText(tbl, 6, 8));
    }

    @Test
    public void buildTables_harmonicCurrentLeavesStandardRateBlankWhenScriptRateMissing() {
        List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
                "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row = harmonicRow("2", "1.0000", "1.0000", "1.0000");
        row.put("standard", "1.0000");
        row.put("baseCurrentA", "5.0000");
        row.put("baseCurrentB", "5.0000");
        row.put("baseCurrentC", "5.0000");
        SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("", cellText(tbl, 1, 2));
        assertEquals("", cellText(tbl, 4, 1));
        assertEquals("", cellText(tbl, 5, 1));
        assertEquals("20.0000", cellText(tbl, 5, 2));
        assertEquals("", cellText(tbl, 5, 5));
        assertEquals("", cellText(tbl, 5, 8));
    }

    @Test
    public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeAvailable() {
        List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
                "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row = harmonicRow("2", "1.0000", "1.0000", "1.0000");
        row.put("standard", "2.0000");
        row.put("standardRate", "1.0000");
        row.put("baseCurrentA", "10.0000");
        row.put("baseCurrentB", "10.0000");
        row.put("baseCurrentC", "10.0000");
        SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("1.0000", cellText(tbl, 5, 1));
        assertEquals("9.0000", cellText(tbl, 5, 5));
        assertEquals("±0.1500", cellText(tbl, 5, 8));
    }

    @Test
    public void buildTables_harmonicCurrentErrorRateUsesRateDifference() {
        List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
                "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row = harmonicRow("2", "2.0000", "1.0000", "1.0000");
        row.put("standard", "1.0000");
        row.put("standardRate", "10.0000");
        row.put("errorA", "0.0100");
        row.put("baseCurrentA", "10.0000");
        row.put("baseCurrentB", "10.0000");
        row.put("baseCurrentC", "10.0000");
        SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("10.0000", cellText(tbl, 5, 1));
        assertEquals("20.0000", cellText(tbl, 5, 2));
        assertEquals("10.0000", cellText(tbl, 5, 5));
    }

    @Test
    public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeMissing() {
        List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
                "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
        Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
        Map<String, String> row = harmonicRow("2", "0.040144", "0.040248", "0.040116");
        row.put("standard", "/");
        row.put("standardRate", "1.0000");
        row.put("baseCurrentA", "4.0000");
        row.put("baseCurrentB", "4.0000");
        row.put("baseCurrentC", "4.0000");
        SingleTestResult r = resultWithRows("0.0075A", Collections.singletonList(row));

        List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);

        @SuppressWarnings("unchecked")
        Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
        assertEquals("1.0000", cellText(tbl, 1, 2));
        assertEquals("1.0000", cellText(tbl, 4, 1));
        assertEquals("1.0000", cellText(tbl, 5, 1));
        assertEquals("1.0036", cellText(tbl, 5, 2));
        assertEquals("0.0036", cellText(tbl, 5, 5));
    }

    private Map<String, String> harmonicRow(String time, String testA, String testB, String testC) {
        Map<String, String> row = new HashMap<>();
        row.put("time", time);
        row.put("standard", "0.0000");
        row.put("testA", testA);
        row.put("testB", testB);
        row.put("testC", testC);
        row.put("errorA", "0.0000");
        row.put("errorB", "0.0000");
        row.put("errorC", "0.0000");
        row.put("resultA", "合格");
        row.put("resultB", "合格");
        row.put("resultC", "合格");
        return row;
    }

    private Object unwrap(Object o) {
        return (o instanceof JAXBElement) ? ((JAXBElement<?>) o).getValue() : o;
    }

    private int countTables(List<Object> out) {
        int n = 0;
        for (Object o : out) {
            if (unwrap(o) instanceof Tbl) {
                n++;
            }
        }
        return n;
    }

    private List<Tbl> collectTables(List<Object> out) {
        List<Tbl> tables = new ArrayList<>();
        for (Object o : out) {
            Object v = unwrap(o);
            if (v instanceof Tbl) {
                tables.add((Tbl) v);
            }
        }
        return tables;
    }

    private List<String> nonBlankParagraphTexts(List<Object> out) {
        List<String> texts = new ArrayList<>();
        for (Object o : out) {
            Object v = unwrap(o);
            if (v instanceof P) {
                String text = Docx4jUtil.getTextFromP((P) v);
                if (text != null && !text.trim().isEmpty()) {
                    texts.add(text);
                }
            }
        }
        return texts;
    }

    private List<Tr> collectTrs(Tbl tbl) {
        List<Tr> trs = new ArrayList<>();
        for (Object o : tbl.getContent()) {
            Object v = unwrap(o);
            if (v instanceof Tr) {
                trs.add((Tr) v);
            }
        }
        return trs;
    }

    private Tc cellOf(Tr tr, int colIdx) {
        @SuppressWarnings("unchecked")
        JAXBElement<Tc> tc = (JAXBElement<Tc>) tr.getContent().get(colIdx);
        return tc.getValue();
    }
}
