"""测试 G-code 准备阶段缓存行为，确保重复副本不会重复执行预处理。

验证内容：
- 同一 G-code 成员多次复制时，补丁只应用一次
- 同一 G-code 成员多次复制时，M73 偏移模板只计算一次
"""
from __future__ import annotations

import hashlib
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import patch

import a1_swap_mod_packer.builder as builder
from a1_swap_mod_packer.models import BuildOptions, GcodePatchConfig, GcodePatchRule, PlateJob


class GcodePrepareCacheTest(unittest.TestCase):
    """测试 G-code 准备阶段的缓存：补丁和 M73 模板应每成员仅执行一次。"""

    def test_repeated_copies_prepare_gcode_once_per_member(self) -> None:
        """验证同一 G-code 成员多次复制时，补丁仅在首次准备时应用一次。"""
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            source_3mf = root / "source.3mf"
            output_3mf = root / "out.3mf"
            swap_gcode = root / "swap.gcode"
            self.write_archive(source_3mf)  # 创建测试用 3MF 归档
            swap_gcode.write_text("; swap\nG1 X5\n", encoding="utf-8")

            # 配置一条简单的替换规则
            patch_config = GcodePatchConfig(
                rules=(
                    GcodePatchRule(
                        name="test",
                        find="G0 Y254 F3000",
                        replace="G0 Y250 F3000 ;Patched",
                    ),
                )
            )
            patch_calls = 0  # 记录补丁被调用次数
            captured_gcode = ""  # 捕获最终 G-code 内容

            def count_patch_calls(text: str, config: GcodePatchConfig) -> str:
                """模拟补丁应用并累计调用次数。"""
                nonlocal patch_calls
                patch_calls += 1
                return text.replace("G0 Y254 F3000", "G0 Y250 F3000 ;Patched", 1)

            def capture_gcode(
                base_3mf: Path,
                output_3mf: Path,
                gcode_bytes: bytes,
                sources: list[builder.PlateSource],
                options: BuildOptions,
            ) -> str:
                """截获最终写入的 G-code 字节以便断言。"""
                nonlocal captured_gcode
                captured_gcode = gcode_bytes.decode("utf-8")
                return hashlib.md5(gcode_bytes).hexdigest()

            options = BuildOptions(
                swap_gcode=swap_gcode,
                output_3mf=output_3mf,
                apply_gcode_patches=True,
                show_plate_number=False,
                swap_after_final=False,
                line_ending="lf",
                add_preview_label=False,
            )

            # 构建 3 份副本，验证补丁仅调用 1 次但 3 份均被替换
            with patch.object(builder, "parse_patch_config", return_value=patch_config), patch.object(
                builder,
                "apply_gcode_patches",
                side_effect=count_patch_calls,
            ), patch.object(builder, "write_output_3mf", side_effect=capture_gcode):
                result = builder.build_packed_3mf([PlateJob(source_3mf, 3)], options)

            self.assertEqual(result.plate_count, 3)  # 3 份副本
            self.assertEqual(patch_calls, 1)  # 补丁仅调用 1 次（缓存生效）
            self.assertEqual(captured_gcode.count("G0 Y250 F3000 ;Patched"), 3)  # 但 3 份都已替换
            self.assertNotIn("G0 Y254 F3000", captured_gcode)  # 原始行已不存在

    def test_repeated_copies_prepare_m73_template_once_per_member(self) -> None:
        """验证同一 G-code 成员多次复制时，M73 偏移模板仅计算一次。"""
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            source_3mf = root / "source.3mf"
            output_3mf = root / "out.3mf"
            swap_gcode = root / "swap.gcode"
            self.write_archive(source_3mf)
            swap_gcode.write_text("; swap\nG1 X5\n", encoding="utf-8")

            m73_template_calls = 0  # 记录模板准备调用次数
            captured_gcode = ""
            original_prepare_m73 = builder.prepare_m73_offset_template  # 保存原始函数引用

            def count_m73_template_calls(text: str):
                """模拟 M73 模板准备并累计调用次数。"""
                nonlocal m73_template_calls
                m73_template_calls += 1
                return original_prepare_m73(text)

            def capture_gcode(
                base_3mf: Path,
                output_3mf: Path,
                gcode_bytes: bytes,
                sources: list[builder.PlateSource],
                options: BuildOptions,
            ) -> str:
                """截获最终写入的 G-code 字节以便断言。"""
                nonlocal captured_gcode
                captured_gcode = gcode_bytes.decode("utf-8")
                return hashlib.md5(gcode_bytes).hexdigest()

            options = BuildOptions(
                swap_gcode=swap_gcode,
                output_3mf=output_3mf,
                apply_gcode_patches=False,
                show_plate_number=True,  # 启用板号显示，触发 M73 偏移
                swap_after_final=False,
                line_ending="lf",
                add_preview_label=False,
            )

            # 构建 3 份副本，验证模板仅准备 1 次，但每份各自偏移
            with (
                patch.object(builder, "prepare_m73_offset_template", side_effect=count_m73_template_calls),
                patch.object(
                    builder,
                    "write_output_3mf",
                    side_effect=capture_gcode,
                ),
            ):
                result = builder.build_packed_3mf([PlateJob(source_3mf, 3)], options)

            self.assertEqual(result.plate_count, 3)
            self.assertEqual(m73_template_calls, 1)  # 模板仅准备 1 次
            # 3 份副本的 M73 剩余时间已分别偏移
            self.assertIn("M73 P0 R6010", captured_gcode)
            self.assertIn("M73 P0 R12010", captured_gcode)
            self.assertIn("M73 P0 R18010", captured_gcode)

    @staticmethod
    def write_archive(path: Path) -> None:
        """创建包含带 M73 指令和待替换行的 plate_1 G-code 的 3MF 测试归档。"""
        slice_info = """<?xml version="1.0" encoding="UTF-8"?>
<config>
  <plate>
    <metadata key="index" value="1"/>
  </plate>
</config>
"""
        gcode = """G0 X128 F30000
G0 Y254 F3000
M73 P0 R10
;=====printer finish  sound=========
"""
        with zipfile.ZipFile(path, "w") as archive:
            archive.writestr("Metadata/slice_info.config", slice_info)
            archive.writestr("Metadata/plate_1.gcode", gcode)


if __name__ == "__main__":
    unittest.main()
