from __future__ import annotations

import importlib.util
import json
import sys
import tempfile
import unittest
import zlib
from pathlib import Path
from unittest.mock import patch

WORKER_PATH = (
    Path(__file__).resolve().parents[1]
    / "windows-node"
    / "adapters"
    / "origin.plot@v2"
    / "worker.py"
)
ADAPTER_MANIFEST_PATH = (
    Path(__file__).resolve().parents[1]
    / "windows-node"
    / "adapters"
    / "origin.plot@v2"
    / "adapter.json"
)
SPEC = importlib.util.spec_from_file_location("zcbot_origin_worker", WORKER_PATH)
assert SPEC and SPEC.loader
worker = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(worker)


class OriginWorkerUnitTests(unittest.TestCase):
    @staticmethod
    def _request() -> dict:
        return {
            "inputs": [{"key": "sample"}],
            "operation": {
                "plot": {
                    "type": "line",
                    "series": [{"input": "sample", "x": "x", "y": "y"}],
                }
            },
            "outputs": [{"key": "figure_png", "format": "png"}],
        }

    def test_worker_version_matches_adapter_manifest(self) -> None:
        manifest = json.loads(ADAPTER_MANIFEST_PATH.read_text(encoding="utf-8"))
        self.assertEqual(worker.ADAPTER_VERSION, manifest["adapter_version"])

    def test_registered_origin_version_comes_from_com_server_executable(self) -> None:
        class FakeKey:
            def __init__(self, path: str):
                self.path = path

            def __enter__(self):
                return self

            def __exit__(self, *_):
                return None

        class FakeRegistry:
            HKEY_CLASSES_ROOT = object()
            values = {
                r"Origin.ApplicationSI\CLSID": "{origin-clsid}",
                r"CLSID\{origin-clsid}\LocalServer32": (
                    r'"C:\Program Files\OriginLab\Origin2025\Origin64.exe" /Automation'
                ),
            }

            @classmethod
            def OpenKey(cls, _root, path):
                return FakeKey(path)

            @classmethod
            def QueryValueEx(cls, key, _name):
                return cls.values[key.path], 1

        with patch.object(worker, "_windows_file_version", return_value="2025") as read:
            self.assertEqual(worker._registered_origin_version(FakeRegistry), "2025")
        read.assert_called_once_with(Path(r"C:\Program Files\OriginLab\Origin2025\Origin64.exe"))
        FakeRegistry.values[r"CLSID\{origin-clsid}\LocalServer32"] = "invalid"
        self.assertIsNone(worker._registered_origin_version(FakeRegistry))

    def test_configured_origin_executable_takes_priority_for_version(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            executable = Path(directory) / "Origin64.exe"
            executable.touch()
            with (
                patch.dict("os.environ", {"ZCBOT_ORIGIN_EXE": str(executable)}),
                patch.object(worker, "_windows_file_version", return_value="2024b") as read,
            ):
                self.assertEqual(worker._registered_origin_version(object()), "2024b")
        read.assert_called_once_with(executable.resolve())

    def test_csv_and_json_inputs_are_read_without_origin(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            csv_path = root / "input.csv"
            csv_path.write_text("x,y\n1,2\n3,4\n", encoding="utf-8")
            self.assertEqual(
                worker._read_rows(csv_path, None),
                (["x", "y"], [["1", "2"], ["3", "4"]]),
            )

            json_path = root / "input.json"
            json_path.write_text(json.dumps([{"x": 1, "y": 2}, {"x": 3, "y": 4}]), encoding="utf-8")
            self.assertEqual(worker._read_rows(json_path, None), (["x", "y"], [[1, 2], [3, 4]]))

    def test_manifest_uses_stable_id_and_streaming_digest(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory) / "plot-spec.json"
            path.write_text("{}", encoding="utf-8")
            manifest = worker._manifest(path, "application/json")
            self.assertEqual(manifest["artifact_id"], "plot_spec")
            self.assertEqual(manifest["sha256"], worker._file_sha256(path))
            self.assertEqual(manifest["size_bytes"], 2)

    def test_axis_title_includes_units(self) -> None:
        self.assertEqual(
            worker._axis_title({"title": "Stress", "unit": "MPa"}, "Y"), "Stress (MPa)"
        )
        self.assertEqual(worker._axis_title(None, "Time"), "Time")

    def test_new_graph_converts_template_layers_before_applying_canvas(self) -> None:
        events = []

        class Layer:
            def set_int(self, name, value):
                events.append(("layer", name, value))

        class Graph:
            class Obj:
                @staticmethod
                def LT_execute(command):
                    events.append(("page", "lt", command))

            obj = Obj()

            def __iter__(self):
                return iter([Layer()])

            @staticmethod
            def activate():
                events.append(("page", "activate"))

        class Origin:
            @staticmethod
            def new_graph(*, template):
                events.append(("new", template))
                return Graph()

        graph = worker._new_graph(
            Origin(), "3D", {"width_mm": 140, "height_mm": 100}
        )

        self.assertIsInstance(graph, Graph)
        self.assertEqual(events[0], ("new", "3D"))
        self.assertEqual(events[1], ("layer", "unit", 1))
        self.assertEqual(events[2], ("page", "activate"))
        self.assertEqual(events[3][0:2], ("page", "lt"))
        self.assertIn(f"page.width={140 / 25.4 * 600}", events[3][2])
        self.assertIn(f"page.height={100 / 25.4 * 600}", events[3][2])

    def test_surface_3d_presentation_hides_color_scale_with_legend(self) -> None:
        created = []

        class Label:
            def __init__(self, text=""):
                self.text = text
                self.name = ""
                self.show = True
                self.values = {}

            def set_int(self, name, value):
                self.values[name] = value

        class Obj:
            commands = []

            @classmethod
            def LT_execute(cls, command):
                cls.commands.append(command)

        class Layer:
            obj = Obj()
            values = {}
            spectrum = Label()

            @classmethod
            def set_int(cls, name, value):
                cls.values[name] = value

            @classmethod
            def set_float(cls, name, value):
                cls.values[name] = value

            @classmethod
            def label(cls, name):
                return cls.spectrum if name == "Spectrum1" else None

            @staticmethod
            def add_label(text):
                label = Label(text)
                created.append(label)
                return label

        worker._apply_surface_3d_presentation(
            Layer(),
            {"enabled": False},
            x_title="Temperature (°C)",
            y_title="Time (min)",
            z_title="Strength (MPa)",
        )

        self.assertEqual(Layer.values["unit"], 1)
        self.assertEqual(Layer.values["left"], 22)
        self.assertEqual(Layer.values["width"], 58)
        self.assertEqual(Layer.values["z.label.pt"], 7)
        self.assertFalse(Layer.spectrum.show)
        self.assertEqual(
            [(label.name, label.text) for label in created],
            [
                ("ZCBOT_X_TITLE", "Temperature (°C)"),
                ("ZCBOT_Y_TITLE", "Time (min)"),
                ("ZCBOT_Z_TITLE", "Strength (MPa)"),
            ],
        )
        self.assertEqual(created[0].values["attach"], 0)
        self.assertEqual(created[2].values["rotate"], 90)
        self.assertIn("label -r zf", Obj.commands[0])

    def test_layout_helpers_apply_validated_values(self) -> None:
        class FakeAxis:
            title = ""
            scale = "linear"
            limits = None

            def set_limits(self, begin, end, step):
                self.limits = (begin, end, step)

        class FakeLayer:
            def __init__(self):
                self.axes = {name: FakeAxis() for name in ("x", "y", "z")}
                self.values = {}
                self.labels = {}

            def axis(self, name):
                return self.axes[name]

            def set_float(self, name, value):
                self.values[name] = value

            def set_int(self, name, value):
                self.values[name] = value

            def label(self, name):
                class FakeLabel:
                    def set_int(_, prop, value):
                        self.labels[(name, prop)] = value

                return FakeLabel()

        layer = FakeLayer()
        worker._apply_axis(
            layer,
            "x",
            {
                "title": "Time",
                "unit": "d",
                "scale": "log10",
                "minimum": 1,
                "maximum": 100,
                "major_step": 1,
                "minor_ticks": 4,
                "reverse": True,
                "tick_label_angle": 45,
                "tick_label_font_size": 10,
                "title_font_size": 12,
                "grid": "major_minor",
            },
            "X",
        )
        self.assertEqual(layer.axes["x"].title, "Time (d)")
        self.assertEqual(layer.axes["x"].scale, "log10")
        self.assertEqual(layer.axes["x"].limits, (1, 100, 1))
        self.assertEqual(
            layer.values,
            {
                "x.label.rotate": 45,
                "x.label.pt": 10,
                "x.minorTicks": 4,
                "x.reverse": 1,
                "x.grid.show": 3,
            },
        )
        self.assertEqual(layer.labels, {("xb", "fsize"): 12})

    def test_annotations_compile_to_fixed_origin_objects(self) -> None:
        class Label:
            def __init__(self, text, x, y):
                self.text = text
                self.x = x
                self.y = y
                self.name = ""
                self.values = {}

            def set_int(self, name, value):
                self.values[name] = value

            def set_float(self, name, value):
                self.values[name] = value

        class Obj:
            def __init__(self):
                self.commands = []

            def LT_execute(self, command):
                self.commands.append(command)

        class Layer:
            def __init__(self):
                self.obj = Obj()
                self.xlim = (0, 10)
                self.ylim = (0, 100)
                self.labels = []
                self.activated = False

            def activate(self):
                self.activated = True

            def add_label(self, text, x, y):
                label = Label(text, x, y)
                self.labels.append(label)
                return label

        layer = Layer()
        worker._apply_annotations(layer, [
            {
                "kind": "reference_line", "axis": "x", "value": 3.5,
                "color": "#CC0000", "line_width": 1.5, "line_style": "dash_dot",
                "label": "Peak",
            },
            {
                "kind": "reference_band", "axis": "y", "from": 20, "to": 40,
                "color": "#3366CC", "transparency": 75, "label": "Window",
            },
            {
                "kind": "text", "text": "Sample A", "x": 8, "y": 90,
                "color": "#222222", "font_size": 11, "rotation": 15,
                "background": "white",
            },
        ])

        self.assertTrue(layer.activated)
        self.assertIn("draw -n ZCBOT_ANN_001 -l -v 3.5", layer.obj.commands[0])
        self.assertIn("ZCBOT_ANN_001.lineType=4", layer.obj.commands[0])
        self.assertIn("draw -n ZCBOT_ANN_002 -b", layer.obj.commands[2])
        self.assertIn("ZCBOT_ANN_002.y1=20", layer.obj.commands[2])
        self.assertIn("ZCBOT_ANN_002.y3=40", layer.obj.commands[2])
        self.assertIn("ZCBOT_ANN_002.transparency=75", layer.obj.commands[2])
        self.assertEqual(
            [(label.name, label.text) for label in layer.labels],
            [
                ("ZCBOT_ANN_001_LABEL", "Peak"),
                ("ZCBOT_ANN_002_LABEL", "Window"),
                ("ZCBOT_ANN_003", "Sample A"),
            ],
        )
        self.assertEqual(layer.labels[-1].values["attach"], 2)
        self.assertEqual(layer.labels[-1].values["background"], 4)
        self.assertEqual(layer.labels[-1].values["rotate"], 15)

    def test_annotation_semantics_reject_invalid_band_range(self) -> None:
        request = self._request()
        request["operation"]["plot"]["annotations"] = [{
            "kind": "reference_band", "axis": "x", "from": 5, "to": 5,
        }]
        with self.assertRaisesRegex(ValueError, "ANNOTATION_BAND_RANGE_INVALID"):
            worker._validate_semantics(request)

    def test_rescale_applies_log_scales_before_deriving_limits(self) -> None:
        class FakeAxis:
            def __init__(self, name, events):
                self.name = name
                self.events = events
                self._scale = "linear"

            @property
            def scale(self):
                return self._scale

            @scale.setter
            def scale(self, value):
                self._scale = value
                self.events.append(("scale", self.name, value))

        class FakeLayer:
            def __init__(self):
                self.events = []
                self.axes = {name: FakeAxis(name, self.events) for name in ("x", "y")}

            def axis(self, name):
                return self.axes[name]

            def rescale(self):
                self.events.append(("rescale",))

        layer = FakeLayer()
        worker._rescale_with_axis_scales(
            layer,
            [
                ("x", {"scale": "log10", "minimum": 1, "maximum": 100}),
                ("y", {"scale": "ln"}),
            ],
        )

        self.assertEqual(
            layer.events,
            [
                ("scale", "x", "log10"),
                ("scale", "y", "ln"),
                ("rescale",),
            ],
        )

    def test_rescale_does_not_apply_explicit_limits_before_rescaling(self) -> None:
        class FakeAxis:
            scale = "linear"

            def set_limits(self, *_):
                raise AssertionError("limits must be applied after rescale")

        class FakeLayer:
            def __init__(self):
                self.axis_value = FakeAxis()
                self.rescaled = False

            def axis(self, _name):
                return self.axis_value

            def rescale(self):
                self.rescaled = True

        layer = FakeLayer()
        worker._rescale_with_axis_scales(
            layer,
            [
                ("x", {"minimum": 1, "maximum": 100}),
            ],
        )

        self.assertTrue(layer.rescaled)
        self.assertEqual(layer.axis_value.scale, "linear")

    def test_series_style_maps_to_origin_properties(self) -> None:
        class FakePlot:
            def __init__(self):
                self.values = {}
                self.color = None
                self.symbol_kind = None
                self.symbol_size = None
                self.transparency = None

            def set_float(self, name, value):
                self.values[name] = value

            def set_int(self, name, value):
                self.values[name] = value

        plot = FakePlot()
        worker._apply_series_style(
            plot,
            {
                "color": "#3366CC",
                "line_width": 1.5,
                "line_style": "dash_dot",
                "symbol": "diamond",
                "symbol_size": 8,
                "transparency": 20,
            },
        )
        self.assertEqual(plot.color, (51, 102, 204))
        self.assertEqual(plot.values, {"line.width": 1.5, "line.type": 4})
        self.assertEqual(plot.symbol_kind, 5)
        self.assertEqual(plot.symbol_size, 8)
        self.assertEqual(plot.transparency, 20)

    def test_origin_symbol_ids_match_labtalk_values(self) -> None:
        self.assertEqual(
            worker.SYMBOLS,
            {
                "square": 1,
                "circle": 2,
                "triangle_up": 3,
                "diamond": 5,
                "plus": 6,
                "cross": 7,
            },
        )

    def test_keyed_input_directory_requires_exactly_one_file(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            keyed = root / "input" / "sample"
            keyed.mkdir(parents=True)
            path = keyed / "data.csv"
            path.write_text("x,y\n1,2\n", encoding="utf-8")
            self.assertEqual(worker._input_file(root, "sample"), path)
            (keyed / "extra.csv").write_text("x,y\n3,4\n", encoding="utf-8")
            with self.assertRaisesRegex(ValueError, "INPUT_FILE_COUNT_INVALID:sample"):
                worker._input_file(root, "sample")

    def test_series_are_resolved_against_each_input(self) -> None:
        input_data = {
            "first": (["time", "strength"], [[1, 10]]),
            "second": (["temperature", "value"], [[20, 30]]),
        }
        series = [
            {"input": "first", "x": "time", "y": "strength", "label": "7 d"},
            {"input": "second", "x": "temperature", "y": "value", "label": "28 d"},
        ]
        resolved, labels = worker._resolve_series(input_data, series)
        self.assertEqual(
            resolved,
            [
                {"input": "first", "label": "7 d", "x": 0, "y": 1},
                {"input": "second", "label": "28 d", "x": 0, "y": 1},
            ],
        )
        self.assertEqual(labels, {("first", 1): "7 d", ("second", 1): "28 d"})

    def test_shared_y_column_rejects_conflicting_labels(self) -> None:
        input_data = {"sample": (["x1", "x2", "y"], [[1, 2, 3]])}
        with self.assertRaisesRegex(ValueError, "SERIES_LABEL_CONFLICT"):
            worker._resolve_series(
                input_data,
                [
                    {"input": "sample", "x": "x1", "y": "y", "label": "First"},
                    {"input": "sample", "x": "x2", "y": "y", "label": "Second"},
                ],
            )

    def test_worker_owns_cross_field_semantic_validation(self) -> None:
        request = self._request()
        worker._validate_semantics(request)

        request["operation"]["plot"]["x_axis"] = {
            "scale": "log10",
            "minimum": 0,
            "maximum": 100,
        }
        with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
            worker._validate_semantics(request)

        request = self._request()
        request["inputs"].append({"key": "unused"})
        with self.assertRaisesRegex(ValueError, "INPUT_BINDINGS_MUST_BE_USED_EXACTLY"):
            worker._validate_semantics(request)

    def test_multi_panel_semantics_cover_layout_axes_and_total_series(self) -> None:
        request = {
            "inputs": [{"key": "sample"}],
            "operation": {
                "plot": {
                    "type": "multi_panel",
                    "layout": {"rows": 1, "columns": 2, "share_x": True},
                    "panels": [
                        {
                            "key": "strength",
                            "series": [
                                {
                                    "input": "sample",
                                    "x": "age",
                                    "y": "strength",
                                    "kind": "scatter",
                                    "y_error": "sd",
                                }
                            ],
                        },
                        {
                            "key": "thermal",
                            "series": [
                                {
                                    "input": "sample",
                                    "x": "temperature",
                                    "y": "tg",
                                    "kind": "line",
                                },
                                {
                                    "input": "sample",
                                    "x": "temperature",
                                    "y": "dtg",
                                    "kind": "line",
                                    "y_axis": "right",
                                },
                            ],
                            "right_y_axis": {"title": "DTG"},
                        },
                    ],
                }
            },
            "outputs": [{"key": "figure_png", "format": "png"}],
        }
        worker._validate_semantics(request)

        request["operation"]["plot"]["layout"] = {"rows": 1, "columns": 1}
        with self.assertRaisesRegex(ValueError, "PANEL_LAYOUT_TOO_SMALL"):
            worker._validate_semantics(request)

        request["operation"]["plot"]["layout"] = {
            "rows": 1,
            "columns": 2,
            "share_x": True,
        }
        request["operation"]["plot"]["panels"][0]["x_axis"] = {
            "scale": "log10",
            "minimum": 0,
        }
        with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
            worker._validate_semantics(request)

        request["operation"]["plot"]["panels"][0]["x_axis"] = {"minimum": 1}
        with self.assertRaisesRegex(ValueError, "SHARED_X_AXIS_CONFIG_MISMATCH"):
            worker._validate_semantics(request)

        request["operation"]["plot"]["panels"][0].pop("x_axis")
        request["operation"]["plot"]["x_axis"] = {
            "scale": "log10",
            "minimum": 0,
        }
        with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
            worker._validate_semantics(request)

    def test_recipe_uses_the_composition_semantics(self) -> None:
        request = {
            "inputs": [{"key": "sample"}],
            "operation": {
                "plot": {
                    "type": "recipe",
                    "recipe_version": 1,
                    "layout": {"rows": 1, "columns": 1},
                    "panels": [
                        {
                            "key": "main",
                            "series": [
                                {
                                    "input": "sample",
                                    "x": "age",
                                    "y": "strength",
                                    "kind": "line_scatter",
                                }
                            ],
                        }
                    ],
                }
            },
            "outputs": [{"key": "figure_png", "format": "png"}],
        }
        worker._validate_semantics(request)
        self.assertIn("recipe", worker.COMPOSITION_PLOT_TYPES)

    def test_panel_settings_inherit_plot_defaults_and_allow_overrides(self) -> None:
        plot = {
            "x_axis": {"title": "Age", "unit": "d"},
            "y_axis": {"title": "Value"},
            "legend": {"enabled": False, "font_size": 8},
        }
        panel = {
            "y_axis": {"title": "Strength", "unit": "MPa"},
            "legend": {"enabled": True},
        }
        self.assertEqual(
            worker._panel_setting(plot, panel, "x_axis"),
            {"title": "Age", "unit": "d"},
        )
        self.assertEqual(
            worker._panel_setting(plot, panel, "y_axis"),
            {"title": "Strength", "unit": "MPa"},
        )
        self.assertEqual(
            worker._panel_legend(plot, panel),
            {
                "enabled": True,
                "position": "top_right",
                "font_size": 8,
            },
        )

    def test_panel_y_axis_fallback_prefers_title_then_column_name(self) -> None:
        series = [{"y": "CS_20C"}]
        self.assertEqual(
            worker._panel_y_axis_fallback({"title": "抗压强度 (MPa)"}, series),
            "抗压强度 (MPa)",
        )
        self.assertEqual(
            worker._panel_y_axis_fallback({}, series),
            "CS_20C",
        )

    def test_mixed_xy_series_pass_kind_axis_and_error_columns_to_origin(self) -> None:
        class FakePlot:
            pass

        class FakeLayer:
            def __init__(self):
                self.calls = []
                self.grouped = False
                self.plots = []

            def add_plot(self, worksheet, **arguments):
                self.calls.append((worksheet, arguments))
                plot = FakePlot()
                self.plots.append(plot)
                if "colxerr" in arguments:
                    self.plots.append(FakePlot())
                if "colyerr" in arguments:
                    self.plots.append(FakePlot())
                return plot

            def plot_list(self):
                return self.plots

            def group(self, enabled=True):
                self.grouped = enabled

        layer = FakeLayer()
        series = [
            {
                "input": "sample",
                "x": "age",
                "y": "strength",
                "kind": "scatter",
                "x_error": "age_sd",
                "y_error": "strength_sd",
            },
            {"input": "sample", "x": "age", "y": "fit", "kind": "line"},
        ]
        resolved = [
            {"input": "sample", "x": 0, "y": 1, "x_error": 2, "y_error": 3},
            {"input": "sample", "x": 0, "y": 4},
        ]
        worker._add_xy_plots(layer, {"sample": "worksheet"}, series, resolved)
        self.assertEqual(
            layer.calls,
            [
                (
                    "worksheet",
                    {"coly": 1, "colx": 0, "type": "s", "colxerr": 2, "colyerr": 3},
                ),
                ("worksheet", {"coly": 4, "colx": 0, "type": "l"}),
            ],
        )
        self.assertFalse(layer.grouped)

    def test_series_support_xyz_and_error_roles(self) -> None:
        resolved, labels = worker._resolve_series(
            {"sample": (["x", "y", "z", "x_sd", "y_sd"], [[0, 1, 2, 0.1, 0.2]])},
            [
                {
                    "input": "sample",
                    "x": "x",
                    "y": "y",
                    "z": "z",
                    "x_error": "x_sd",
                    "y_error": "y_sd",
                    "label": "测量值",
                }
            ],
        )
        self.assertEqual(
            resolved,
            [
                {
                    "input": "sample",
                    "label": "测量值",
                    "x": 0,
                    "y": 1,
                    "z": 2,
                    "x_error": 3,
                    "y_error": 4,
                }
            ],
        )
        self.assertEqual(labels, {("sample", 1): "测量值"})

    def test_series_support_second_batch_roles(self) -> None:
        resolved, _ = worker._resolve_series(
            {
                "sample": (
                    ["x", "mean", "size", "lower", "upper"],
                    [[0, 1, 2, 0.5, 1.5]],
                )
            },
            [
                {
                    "input": "sample",
                    "x": "x",
                    "y": "mean",
                    "size": "size",
                    "lower": "lower",
                    "upper": "upper",
                }
            ],
        )
        self.assertEqual(
            resolved,
            [
                {
                    "input": "sample",
                    "label": None,
                    "x": 0,
                    "y": 1,
                    "size": 2,
                    "lower": 3,
                    "upper": 4,
                }
            ],
        )

    def test_second_batch_value_validation_rejects_invalid_data(self) -> None:
        input_data = {"sample": (["x", "y", "size", "lower", "upper"], [[0, 1, 0, 2, 1]])}
        resolved = [
            {
                "input": "sample",
                "x": 0,
                "y": 1,
                "size": 2,
                "lower": 3,
                "upper": 4,
            }
        ]
        with self.assertRaisesRegex(ValueError, "SIZE_VALUE_NOT_POSITIVE"):
            worker._validate_bubble_values(input_data, resolved)
        with self.assertRaisesRegex(ValueError, "BAND_BOUNDS_INVERTED"):
            worker._validate_band_values(input_data, resolved)

    def test_band_graph_adds_bounds_fill_and_center_line(self) -> None:
        class FakePlot:
            def __init__(self):
                self.fill = None

            def set_fill_area(self, **arguments):
                self.fill = arguments

        class FakeLayer:
            def __init__(self):
                self.calls = []

            def add_plot(self, worksheet, **arguments):
                plot = FakePlot()
                self.calls.append((worksheet, arguments, plot))
                return plot

        class FakeGraph:
            def __init__(self):
                self.layer = FakeLayer()

            def __getitem__(self, index):
                self.assert_index = index
                return self.layer

        class FakeOp:
            def __init__(self):
                self.graph = FakeGraph()

            def new_graph(self, *, template):
                self.template = template
                return self.graph

        op = FakeOp()
        graph, layer, centers, legend = worker._build_band_graph(
            op,
            {"sample": "worksheet"},
            [{"input": "sample", "x": "x", "y": "mean", "label": "95% CI"}],
            [{"input": "sample", "x": 0, "y": 1, "lower": 2, "upper": 3}],
        )
        self.assertIs(graph, op.graph)
        self.assertIs(layer, op.graph.layer)
        self.assertEqual(op.template, "line")
        self.assertEqual([item[1]["coly"] for item in layer.calls], [3, 2, 1])
        self.assertEqual(layer.calls[0][2].fill, {"type": 9})
        self.assertEqual(centers, [layer.calls[2][2]])
        self.assertEqual(legend, r"\l(3) 95% CI")

    def test_second_batch_plot_ids_and_templates_are_fixed(self) -> None:
        self.assertEqual(worker.PLOT_CONFIG["box"], ("box", 206))
        self.assertEqual(worker.PLOT_CONFIG["histogram"], ("hist", 219))
        self.assertEqual(worker.PLOT_CONFIG["stacked_column"], ("StackColumn", 213))
        self.assertEqual(worker.PLOT_CONFIG["bubble"], ("scatter", "s"))

    def test_bubble_execution_parameters(self) -> None:
        class FakeOp:
            @staticmethod
            def modi_col(offset):
                return ("modifier", offset)

        class FakePlot:
            symbol_size = None
            symbol_sizefactor = None

        plot = FakePlot()
        worker._configure_bubble_plot(
            FakeOp(), plot, {"style": {"symbol_size": 12}}, {"y": 1, "size": 4}
        )
        self.assertEqual(plot.symbol_size, ("modifier", 3))
        self.assertEqual(plot.symbol_sizefactor, 12)

    def test_stacked_graph_uses_one_native_xyy_range(self) -> None:
        class FakeSheet:
            def __init__(self):
                self.columns = []

            def from_list(self, index, values, *, lname):
                self.columns.append((index, values, lname))

            @staticmethod
            def lt_range(_use_name):
                return "[Book1]1"

        class FakeLayer:
            def __init__(self):
                self.calls = []
                self.obj = self

            def add_plot(self, data_range, *, type):
                self.calls.append((data_range, type))

            def activate(self):
                self.activated = True

            def group(self, enabled, begin, end):
                self.group_call = (enabled, begin, end)

            def LT_execute(self, command):
                self.command = command

            @staticmethod
            def plot_list():
                return ["a", "b"]

        class FakeGraph:
            def __init__(self):
                self.layer = FakeLayer()

            def __getitem__(self, index):
                self.index = index
                return self.layer

        class FakeOp:
            def __init__(self):
                self.sheet = FakeSheet()
                self.graph = FakeGraph()

            def new_sheet(self, kind, *, lname):
                self.sheet_call = (kind, lname)
                return self.sheet

            def new_graph(self, *, template):
                self.template = template
                return self.graph

        op = FakeOp()
        graph, layer, plots = worker._build_stacked_graph(
            op,
            {"sample": (["x", "a", "b"], [[1, 10, 5], [2, 12, 6]])},
            [
                {"input": "sample", "x": "x", "y": "a", "label": "A"},
                {"input": "sample", "x": "x", "y": "b", "label": "B"},
            ],
            [
                {"input": "sample", "x": 0, "y": 1},
                {"input": "sample", "x": 0, "y": 2},
            ],
            "StackColumn",
            213,
        )
        self.assertIs(graph, op.graph)
        self.assertIs(layer, op.graph.layer)
        self.assertEqual(plots, ["a", "b"])
        self.assertEqual(op.template, "StackColumn")
        self.assertEqual(layer.calls, [("[Book1]1!(1,2:3)", 213)])
        self.assertEqual(layer.group_call, (True, 0, 1))
        self.assertTrue(layer.activated)
        self.assertEqual(layer.command, "layer -b s 1")
        self.assertEqual(
            op.sheet.columns[1:],
            [
                (1, [10, 12], "A"),
                (2, [5, 6], "B"),
            ],
        )

    def test_stacked_graph_rejects_mismatched_x_values(self) -> None:
        with self.assertRaisesRegex(ValueError, "STACKED_PLOT_X_VALUES_MISMATCH"):
            worker._build_stacked_graph(
                object(),
                {
                    "a": (["x", "y"], [[1, 10], [2, 12]]),
                    "b": (["x", "y"], [[1, 5], [3, 6]]),
                },
                [
                    {"input": "a", "x": "x", "y": "y"},
                    {"input": "b", "x": "x", "y": "y"},
                ],
                [
                    {"input": "a", "x": 0, "y": 1},
                    {"input": "b", "x": 0, "y": 1},
                ],
                "StackColumn",
                213,
            )

    def test_third_batch_plot_ids_and_templates_are_fixed(self) -> None:
        self.assertEqual(worker.PLOT_CONFIG["area"], ("area", 204))
        self.assertEqual(worker.PLOT_CONFIG["stacked_area"], ("stackarea", 214))
        self.assertEqual(worker.PLOT_CONFIG["polar"], ("polar", 192))
        self.assertEqual(worker.PLOT_CONFIG["pie"], ("pie", 225))
        self.assertEqual(worker.PLOT_CONFIG["stacked_bar"], ("bar", 216))
        self.assertEqual(worker.PLOT_CONFIG["violin"], ("Violin", 206))

    def test_violin_graph_submits_one_multi_y_range(self) -> None:
        class FakeSheet:
            def __init__(self):
                self.columns = []
                self.axis_call = None
                self.activated = False
                self.command = None
                self.obj = self

            def from_list(self, index, values, *, lname):
                self.columns.append((index, values, lname))

            def cols_axis(self, *arguments):
                self.axis_call = arguments

            def lt_range(self, _include_sheet):
                return "[Book1]ViolinData"

            def activate(self):
                self.activated = True

            def LT_execute(self, command):
                self.command = command

        class FakeLayer:
            def __init__(self):
                self.plots = [object()]
                self.activated = False
                self.command = None
                self.obj = self
                self.values = {}

            def set_int(self, name, value):
                self.values[name] = value

            def set_float(self, name, value):
                self.values[name] = value

            def activate(self):
                self.activated = True

            def LT_execute(self, command):
                self.command = command

            def plot_list(self):
                return self.plots

        class FakeGraph:
            def __init__(self):
                self.layer = FakeLayer()

            def __getitem__(self, _index):
                return self.layer

        class FakeOp:
            def __init__(self):
                self.sheet = FakeSheet()
                self.graph = FakeGraph()

            def new_sheet(self, kind, *, lname):
                self.sheet_call = (kind, lname)
                return self.sheet

            def find_graph(self):
                return self.graph

        op = FakeOp()
        graph, layer, plots = worker._build_violin_graph(
            op,
            {
                "a": (["y1", "y2"], [[1, 10], [2, 20]]),
                "b": (["y3"], [[30], [40], [50]]),
            },
            [
                {"input": "a", "y": "y1", "label": "A"},
                {"input": "a", "y": "y2", "label": "B"},
                {"input": "b", "y": "y3", "label": "C"},
            ],
            [
                {"input": "a", "y": 0},
                {"input": "a", "y": 1},
                {"input": "b", "y": 0},
            ],
        )
        self.assertIs(graph, op.graph)
        self.assertIs(layer, op.graph.layer)
        self.assertEqual(plots, op.graph.layer.plots)
        self.assertEqual(op.sheet_call, ("w", "violin_plot_data"))
        self.assertEqual(op.sheet.axis_call, ("y", 0, 2))
        self.assertEqual(
            op.sheet.columns,
            [(0, [1, 2], "A"), (1, [10, 20], "B"), (2, [30, 40, 50], "C")],
        )
        self.assertTrue(op.sheet.activated)
        self.assertEqual(
            op.sheet.command,
            "worksheet -s 1 0 3 0; worksheet -p 206 Violin;",
        )
        self.assertEqual(
            op.graph.layer.values,
            {"unit": 1, "left": 18, "top": 14, "width": 68, "height": 58},
        )

    def test_offset_stacked_line_builds_deterministic_display_columns(self) -> None:
        class FakeSheet:
            def __init__(self):
                self.columns = []

            def from_list(self, index, values, *, lname):
                self.columns.append((index, values, lname))

        class FakeLayer:
            def __init__(self):
                self.calls = []
                self.labels = []
                self.values = {}

            def set_int(self, name, value):
                self.values[name] = value

            def set_float(self, name, value):
                self.values[name] = value

            def add_plot(self, sheet, **arguments):
                plot = (sheet, arguments)
                self.calls.append(plot)
                return plot

            def group(self, enabled):
                self.grouped = enabled

            def add_label(self, text, x, y):
                class Label:
                    def __init__(self):
                        self.name = ""
                        self.values = {}

                    def set_int(self, name, value):
                        self.values[name] = value

                label = Label()
                self.labels.append((text, x, y, label))
                return label

        class FakeGraph:
            def __init__(self):
                self.layer = FakeLayer()

            def __getitem__(self, index):
                self.index = index
                return self.layer

        class FakeOp:
            def __init__(self):
                self.sheet = FakeSheet()
                self.graph = FakeGraph()

            def new_sheet(self, kind, *, lname):
                self.sheet_call = (kind, lname)
                return self.sheet

            def new_graph(self, *, template):
                self.template = template
                return self.graph

        op = FakeOp()
        graph, layer, plots = worker._build_offset_stacked_line_graph(
            op,
            {
                "a": (["x", "y"], [[10, 20], [20, 60]]),
                "b": (["x", "y"], [[11, 100], [21, 120]]),
            },
            [
                {"input": "a", "x": "x", "y": "y", "label": "A"},
                {"input": "b", "x": "x", "y": "y", "label": "B"},
            ],
            [
                {"input": "a", "x": 0, "y": 1},
                {"input": "b", "x": 0, "y": 1},
            ],
            10,
        )
        self.assertIs(graph, op.graph)
        self.assertIs(layer, op.graph.layer)
        self.assertEqual(op.template, "line")
        self.assertEqual(op.sheet_call, ("w", "stacked_line_plot_data"))
        self.assertEqual(
            op.sheet.columns,
            [
                (0, [10.0, 20.0], "x"),
                (1, [0.0, 40.0], "A"),
                (2, [11.0, 21.0], "x"),
                (3, [44.0, 64.0], "B"),
            ],
        )
        self.assertEqual(
            [item[1] for item in plots],
            [
                {"colx": 0, "coly": 1, "type": "l"},
                {"colx": 2, "coly": 3, "type": "l"},
            ],
        )
        self.assertFalse(layer.grouped)
        self.assertEqual(
            layer.values,
            {"unit": 1, "left": 18, "top": 14, "width": 68, "height": 60},
        )
        self.assertEqual(
            [(text, x, y) for text, x, y, _label in layer.labels],
            [("A", 20.0, 40.0), ("B", 21.0, 64.0)],
        )
        self.assertEqual(
            [label.name for _text, _x, _y, label in layer.labels],
            ["ZCBOT_STACK_LABEL_001", "ZCBOT_STACK_LABEL_002"],
        )

    def test_offset_stacked_line_rejects_log_y_axis(self) -> None:
        request = self._request()
        request["operation"]["plot"].update({
            "type": "stacked_line",
            "stack_gap_percent": 8,
            "series": [
                {"input": "sample", "x": "x", "y": "a"},
                {"input": "sample", "x": "x", "y": "b"},
            ],
            "y_axis": {"scale": "log10"},
        })
        with self.assertRaisesRegex(ValueError, "STACKED_LINE_LOG_Y_UNSUPPORTED"):
            worker._validate_semantics(request)

    def test_stacked_legend_only_contains_requested_series(self) -> None:
        class Label:
            text = ""

        class Layer:
            legend = Label()

            def label(self, name):
                self.name = name
                return self.legend

        layer = Layer()
        worker._replace_series_legend(
            layer,
            [
                {"y": "a", "label": "Phase A"},
                {"y": "phase_b"},
            ],
        )
        self.assertEqual(layer.name, "Legend")
        self.assertEqual(layer.legend.text, r"\l(1) Phase A" "\n" r"\l(2) phase_b")

    def test_multi_panel_legend_uses_actual_data_plot_indices(self) -> None:
        class Label:
            text = ""

        class Layer:
            legend = Label()

            def label(self, _name):
                return self.legend

        class Plot:
            def __init__(self, value):
                self.value = value

            def index(self):
                return self.value

        layer = Layer()
        worker._replace_series_legend(
            layer,
            [
                {"y": "a", "label": "5 C"},
                {"y": "b", "label": "20 C"},
            ],
            [Plot(0), Plot(2)],
        )
        self.assertEqual(
            layer.legend.text,
            r"\l(1) 5 C" "\n" r"\l(3) 20 C",
        )

    def test_panel_title_and_legend_use_page_pixel_coordinates(self) -> None:
        class Label:
            def __init__(self):
                self.values = {}
                self.show = None

            def set_int(self, name, value):
                self.values[name] = value

            def set_float(self, name, value):
                self.values[name] = value

        class Layer:
            def __init__(self):
                self.legend = Label()
                self.title = None

            def label(self, _name):
                return self.legend

            def add_label(self, _text):
                self.title = Label()
                return self.title

            @staticmethod
            def get_float(name):
                return {
                    "left": 10,
                    "top": 20,
                    "width": 40,
                    "height": 30,
                }[name]

        class Graph:
            @staticmethod
            def get_float(name):
                return {
                    "width": 1800,
                    "height": 1450,
                    "resx": 254,
                    "resy": 254,
                }[name]

        layer = Layer()
        graph = Graph()
        worker._apply_legend(
            layer,
            {"enabled": True, "position": "top_right"},
            attach_to_layer=True,
            graph=graph,
            geometry=(10, 20, 40, 30),
        )
        worker._apply_panel_title(
            graph,
            layer,
            (10, 20, 40, 30),
            "Strength",
            {"font_size": 12},
        )
        self.assertEqual(layer.legend.values["attach"], 0)
        self.assertEqual(layer.legend.values["background"], 0)
        self.assertEqual(layer.legend.values["smartpos"], 1)
        self.assertNotIn("left", layer.legend.values)
        self.assertNotIn("top", layer.legend.values)
        self.assertEqual(layer.title.values["attach"], 1)
        self.assertEqual(layer.title.values["background"], 0)
        self.assertEqual(layer.title.values["fsize"], 12)
        self.assertEqual(layer.title.values["left"], 382)
        self.assertEqual(layer.title.values["top"], 229)

    def test_origin_session_disables_printed_text_baselines(self) -> None:
        class Origin:
            def __init__(self):
                self.variables = []

            def set_lt_var(self, name, value):
                self.variables.append((name, value))

        origin = Origin()
        worker._configure_origin_session(origin)
        self.assertEqual(origin.variables, [("@U", 1)])

    def test_svg_normalizer_removes_only_text_baseline_path(self) -> None:
        source = (
            b'<svg viewBox="0,0 100,100" width="100" height="100">'
            b'<text x="10" y="30" font-size="20">Title</text>\n'
            b'<path d="M 10,14.4 L 70,14.4" '
            b'style="stroke: black;stroke-width: 2;stroke-linecap:butt" '
            b'fill="none"/>'
            b'<path d="M 0,90 L 100,90" '
            b'style="stroke: black;stroke-width: 2" fill="none"/>'
            b'</svg>'
        )

        normalized, baselines = worker._strip_origin_svg_text_baselines(source)

        self.assertEqual(len(baselines), 1)
        self.assertNotIn(b'M 10,14.4 L 70,14.4', normalized)
        self.assertIn(b'M 0,90 L 100,90', normalized)

    def test_pdf_normalizer_preserves_stream_size_and_removes_baseline(self) -> None:
        decoded = b"4 w\n0 J\n10 20 m\n90 20 l\nS\nQ\n"
        compressed = zlib.compress(decoded)
        original = b"%PDF-1.2\nstream\n" + compressed + b"\nendstream\n%%EOF"
        with tempfile.TemporaryDirectory() as temp_dir:
            path = Path(temp_dir) / "figure.pdf"
            path.write_bytes(original)

            removed = worker._strip_origin_pdf_text_baselines(path)
            content = path.read_bytes()
            start = content.index(b"stream\n") + len(b"stream\n")
            end = content.index(b"\nendstream", start)
            normalized = zlib.decompress(content[start:end])

        self.assertEqual(removed, 1)
        self.assertNotIn(b"10 20 m", normalized)
        self.assertEqual(len(content), len(original))

    def test_png_is_rendered_from_normalized_pdf_at_requested_width(self) -> None:
        events = []

        class Pixmap:
            width = 1654
            height = 1181

            @staticmethod
            def save(path):
                events.append(("save", Path(path).name))

        class Page:
            class Rect:
                width = 371

            rect = Rect()

            @staticmethod
            def get_pixmap(*, matrix, alpha):
                events.append(("render", matrix, alpha))
                return Pixmap()

        class Document:
            page_count = 1

            def __enter__(self):
                return self

            def __exit__(self, *_):
                return None

            @staticmethod
            def __getitem__(_index):
                return Page()

        class PyMuPDF:
            @staticmethod
            def Matrix(x, y):
                return (x, y)

            @staticmethod
            def open(path):
                events.append(("open", Path(path).name))
                return Document()

        with patch.dict(sys.modules, {"pymupdf": PyMuPDF()}):
            size = worker._render_pdf_to_png(
                Path("normalized.pdf"), Path("figure.png"), 1654
            )

        self.assertEqual(size, (1654, 1181))
        self.assertEqual(
            events,
            [
                ("open", "normalized.pdf"),
                ("render", (1654 / 371, 1654 / 371), False),
                ("save", "figure.png"),
            ],
        )

    def test_origin_arranges_panel_layers_and_reports_resulting_geometry(self) -> None:
        class Layer:
            def __init__(self, geometry):
                self.geometry = geometry
                self.values = {}
                self.commands = []
                self.activated = False
                self.obj = self

            def set_int(self, name, value):
                self.values[name] = value

            def activate(self):
                self.activated = True

            def LT_execute(self, command):
                self.commands.append(command)

            def get_float(self, name):
                return self.geometry[name]

        layers = [
            Layer({"left": 9, "top": 14, "width": 38, "height": 32}),
            Layer({"left": 58, "top": 14, "width": 38, "height": 32}),
            Layer({"left": 9, "top": 56, "width": 38, "height": 32}),
            Layer({"left": 58, "top": 56, "width": 38, "height": 32}),
        ]
        geometries = worker._arrange_panel_layers(layers, (2, 2))

        self.assertTrue(layers[0].activated)
        self.assertTrue(all(layer.values["unit"] == 1 for layer in layers))
        self.assertEqual(
            layers[0].commands,
            ["layarrange row:=2 col:=2 left:=9 right:=4 top:=14 bottom:=12 xgap:=11 ygap:=10;"],
        )
        self.assertEqual(
            geometries,
            [
                (9, 14, 38, 32),
                (58, 14, 38, 32),
                (9, 56, 38, 32),
                (58, 56, 38, 32),
            ],
        )

    def test_shared_x_hides_upper_row_title_and_tick_labels(self) -> None:
        class Title:
            show = True

        class Layer:
            def __init__(self):
                self.title = Title()
                self.commands = []
                self.obj = self

            def label(self, name):
                return self.title if name == "xb" else None

            def activate(self):
                return None

            def LT_execute(self, command):
                self.commands.append(command)

        upper = Layer()
        lower = Layer()
        worker._apply_shared_x_presentation(upper, panel_index=0, rows=2, columns=2, share_x=True)
        worker._apply_shared_x_presentation(lower, panel_index=2, rows=2, columns=2, share_x=True)
        self.assertFalse(upper.title.show)
        self.assertEqual(upper.commands, ["axis -ps X L 0;"])
        self.assertTrue(lower.title.show)
        self.assertEqual(lower.commands, [])

    def test_auto_legend_delegates_positioning_to_origin(self) -> None:
        class Label:
            def __init__(self):
                self.values = {}
                self.show = None

            def set_int(self, name, value):
                self.values[name] = value

        class Layer:
            def __init__(self):
                self.legend = Label()

            def label(self, _name):
                return self.legend

        layer = Layer()
        worker._apply_legend(layer, {"enabled": True, "position": "auto"})
        self.assertEqual(layer.legend.values["smartpos"], 1)
        self.assertNotIn("left", layer.legend.values)
        self.assertNotIn("top", layer.legend.values)

        default_layer = Layer()
        worker._apply_legend(default_layer, None)
        self.assertTrue(default_layer.legend.show)
        self.assertEqual(default_layer.legend.values["smartpos"], 1)

    def test_heatmap_matrix_accepts_complete_unordered_grid(self) -> None:
        matrix, xy_map = worker._heatmap_matrix(
            [[1, 20, 4], [0, 10, 1], [1, 10, 2], [0, 20, 3]],
            {"x": 0, "y": 1, "z": 2},
        )
        self.assertEqual(matrix, [[1.0, 2.0], [3.0, 4.0]])
        self.assertEqual(xy_map, (0.0, 1.0, 10.0, 20.0))

    def test_heatmap_matrix_rejects_invalid_grid(self) -> None:
        with self.assertRaisesRegex(ValueError, "HEATMAP_GRID_INCOMPLETE"):
            worker._heatmap_matrix(
                [[0, 10, 1], [1, 10, 2], [0, 20, 3]],
                {"x": 0, "y": 1, "z": 2},
            )
        with self.assertRaisesRegex(ValueError, "HEATMAP_COORDINATES_DUPLICATED"):
            worker._heatmap_matrix([[0, 10, 1], [0, 10, 2]], {"x": 0, "y": 1, "z": 2})
        with self.assertRaisesRegex(ValueError, "HEATMAP_GRID_NOT_REGULAR"):
            worker._heatmap_matrix(
                [
                    [0, 10, 1],
                    [1, 10, 2],
                    [3, 10, 3],
                    [0, 20, 4],
                    [1, 20, 5],
                    [3, 20, 6],
                ],
                {"x": 0, "y": 1, "z": 2},
            )


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