"""Tests for multi-file project output (``ladder_to_pyrung_project``)."""

from __future__ import annotations

import importlib
import sys
from pathlib import Path

from pyrung.click import (
    TagMap,
    c,
    ct,
    ctd,
    ladder_to_pyrung_project,
    pyrung_to_ladder,
    t,
    td,
    x,
    y,
)
from pyrung.core import (
    Block,
    Bool,
    Dint,
    Int,
    Program,
    Rung,
    TagType,
    Tms,
    any_of,
)
from pyrung.core.program import (
    call,
    count_up,
    on_delay,
    out,
    reset,
    subroutine,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _project_from_program(
    program: Program,
    tag_map: TagMap,
    tmp_path: Path,
    *,
    nicknames: dict[str, str] | None = None,
) -> dict[str, str]:
    """Program -> CSV -> ladder_to_pyrung_project -> files dict."""
    bundle = pyrung_to_ladder(program, tag_map)
    csv_dir = tmp_path / "csv"
    bundle.write(csv_dir)
    return ladder_to_pyrung_project(csv_dir, nicknames=nicknames)


def _exec_project(files: dict[str, str], tmp_path: Path) -> dict:
    """Write project files to disk and import them, returning main module namespace."""
    project_dir = tmp_path / "project"
    for rel_path, content in files.items():
        fpath = project_dir / rel_path
        fpath.parent.mkdir(parents=True, exist_ok=True)
        fpath.write_text(content, encoding="utf-8")

    old_path = sys.path[:]
    sys.path.insert(0, str(project_dir))
    # Track modules to clean up
    added_modules: list[str] = []
    try:
        tags_mod = importlib.import_module("tags")
        added_modules.append("tags")

        # Import subroutine modules
        for rel_path in sorted(files):
            if (
                rel_path.startswith("subroutines/")
                and rel_path.endswith(".py")
                and rel_path != "subroutines/__init__.py"
            ):
                mod_name = rel_path[:-3].replace("/", ".")
                importlib.import_module(mod_name)
                added_modules.append(mod_name)

        # subroutines package itself
        if "subroutines/__init__.py" in files:
            if "subroutines" not in sys.modules:
                importlib.import_module("subroutines")
            added_modules.append("subroutines")

        main_mod = importlib.import_module("main")
        added_modules.append("main")

        return {
            "logic": main_mod.logic,
            "mapping": tags_mod.mapping,
        }
    finally:
        sys.path[:] = old_path
        for key in added_modules:
            sys.modules.pop(key, None)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestProjectBasic:
    """Basic project generation (no subroutines)."""

    def test_basic_files(self, tmp_path: Path):
        """Program with no subroutines produces main.py and tags.py only."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        assert "main.py" in files
        assert "tags.py" in files
        assert "subroutines/__init__.py" not in files

    def test_tags_file_has_declarations_and_mapping(self, tmp_path: Path):
        """tags.py contains tag declarations and TagMap."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        tags_py = files["tags.py"]
        assert "Generated by pyrung" in tags_py
        assert "Bool" in tags_py
        assert "TagMap" in tags_py
        assert "mapping = TagMap(" in tags_py
        assert "X001" in tags_py or "Button" in tags_py

    def test_main_file_has_program(self, tmp_path: Path):
        """main.py contains Program context and imports from tags."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        main_py = files["main.py"]
        assert "Generated by pyrung" in main_py
        assert "from pyrung import" in main_py
        assert "from tags import" in main_py
        assert "with Program() as logic:" in main_py

    def test_tags_no_inline_comments(self, tmp_path: Path):
        """tags.py suppresses inline address comments."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        # Use nicknames so that comments would normally appear
        files = (
            ladder_to_pyrung_project(
                tmp_path / "csv",  # doesn't exist yet, use direct bundle
                nicknames={"X001": "Button", "Y001": "Light"},
            )
            if False
            else _project_from_program(
                logic, mapping, tmp_path, nicknames={"X001": "Button", "Y001": "Light"}
            )
        )

        tags_py = files["tags.py"]
        assert "# X001" not in tags_py
        assert "# Y001" not in tags_py
        # But nicknames should still be used as variable names
        assert 'Button = Bool("Button")' in tags_py

    def test_exec_round_trip_no_subroutines(self, tmp_path: Path):
        """Generated project files can be imported and produce valid program."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        ns = _exec_project(files, tmp_path)
        # Re-export and verify
        bundle = pyrung_to_ladder(ns["logic"], ns["mapping"])
        assert len(bundle.main_rows) > 0

    def test_plain_block_range_project_imports_block_symbol(self, tmp_path: Path):
        """Project output should import reconstructed plain blocks, not range helpers."""
        import pyclickplc
        from pyclickplc.addresses import AddressRecord, get_addr_key
        from pyclickplc.banks import DataType

        Enable = Bool("Enable")
        Bits = Block("Bits", TagType.BOOL, 1, 3)

        with Program() as logic:
            with Rung(Enable):
                out(Bits[1])
                reset(Bits.select(1, 3))

        mapping = TagMap({Enable: x[1], Bits: c.select(1004, 1006)}, include_system=False)
        bundle = pyrung_to_ladder(logic, mapping)
        csv_dir = tmp_path / "csv"
        bundle.write(csv_dir)

        nick_path = tmp_path / "nicknames.csv"
        pyclickplc.write_csv(
            nick_path,
            {
                get_addr_key("C", 1001): AddressRecord(
                    memory_type="C",
                    address=1001,
                    nickname="",
                    comment="<CmdTagBits:block>",
                    initial_value="0",
                    retentive=False,
                    data_type=DataType.BIT,
                ),
                get_addr_key("C", 1004): AddressRecord(
                    memory_type="C",
                    address=1004,
                    nickname="Cmd_Mode_Production",
                    comment="",
                    initial_value="0",
                    retentive=False,
                    data_type=DataType.BIT,
                ),
                get_addr_key("C", 1019): AddressRecord(
                    memory_type="C",
                    address=1019,
                    nickname="",
                    comment="</CmdTagBits:block>",
                    initial_value="0",
                    retentive=False,
                    data_type=DataType.BIT,
                ),
                get_addr_key("X", 1): AddressRecord(
                    memory_type="X",
                    address=1,
                    nickname="Enable",
                    comment="",
                    initial_value="0",
                    retentive=False,
                    data_type=DataType.BIT,
                ),
            },
        )

        files = ladder_to_pyrung_project(csv_dir, nickname_csv=nick_path)

        assert 'CmdTagBits = Block("CmdTagBits", TagType.BOOL, 1, 19)' in files["tags.py"]
        assert "Cmd_Mode_Production = CmdTagBits[4]" in files["tags.py"]
        assert "from tags import mapping" in files["main.py"]
        assert "Cmd_Mode_Production" in files["main.py"]
        assert "CmdTagBits" in files["main.py"]
        assert "out(Cmd_Mode_Production)" in files["main.py"]
        assert "reset(CmdTagBits.select(4, 6))" in files["main.py"]
        assert "C1004_to_C1006" not in files["tags.py"]
        assert "C1004_to_C1006" not in files["main.py"]

    def test_plain_block_project_lifts_uniform_retentive_to_block_default(self, tmp_path: Path):
        """Uniform plain-block retentive policy should lift to the Block constructor."""
        import pyclickplc
        from pyclickplc.addresses import AddressRecord, get_addr_key
        from pyclickplc.banks import DataType

        Enable = Bool("Enable")
        Bits = Block("Bits", TagType.BOOL, 1, 3)

        with Program() as logic:
            with Rung(Enable):
                out(Bits[1])
                reset(Bits.select(1, 3))

        mapping = TagMap({Enable: x[1], Bits: c.select(1001, 1003)}, include_system=False)
        bundle = pyrung_to_ladder(logic, mapping)
        csv_dir = tmp_path / "csv"
        bundle.write(csv_dir)

        nick_path = tmp_path / "nicknames.csv"
        pyclickplc.write_csv(
            nick_path,
            {
                get_addr_key("C", 1001): AddressRecord(
                    memory_type="C",
                    address=1001,
                    nickname="AlarmCoil_PLC",
                    comment="<AlarmCoil:block>",
                    initial_value="0",
                    retentive=True,
                    data_type=DataType.BIT,
                ),
                get_addr_key("C", 1002): AddressRecord(
                    memory_type="C",
                    address=1002,
                    nickname="AlarmCoil_LostData",
                    comment="",
                    initial_value="0",
                    retentive=True,
                    data_type=DataType.BIT,
                ),
                get_addr_key("C", 1003): AddressRecord(
                    memory_type="C",
                    address=1003,
                    nickname="AlarmCoil_Watchdog",
                    comment="</AlarmCoil:block>",
                    initial_value="0",
                    retentive=True,
                    data_type=DataType.BIT,
                ),
                get_addr_key("X", 1): AddressRecord(
                    memory_type="X",
                    address=1,
                    nickname="Enable",
                    comment="",
                    initial_value="0",
                    retentive=False,
                    data_type=DataType.BIT,
                ),
            },
        )

        files = ladder_to_pyrung_project(csv_dir, nickname_csv=nick_path)

        assert (
            'AlarmCoil = Block("AlarmCoil", TagType.BOOL, 1, 3, retentive=True)' in files["tags.py"]
        )
        assert "AlarmCoil.slot(1, name='AlarmCoil_PLC')" in files["tags.py"]
        assert "AlarmCoil.slot(2, name='AlarmCoil_LostData')" in files["tags.py"]
        assert "AlarmCoil.slot(3, name='AlarmCoil_Watchdog')" in files["tags.py"]
        assert (
            "retentive=True"
            not in files["tags.py"].split(
                'AlarmCoil = Block("AlarmCoil", TagType.BOOL, 1, 3, retentive=True)', 1
            )[1]
        )


class TestProjectWithSubroutines:
    """Project generation with subroutines."""

    def _make_program_with_sub(self):
        Button = Bool("Button")
        Light = Bool("Light")
        SubLight = Bool("SubLight")

        with Program() as logic:
            with Rung(Button):
                out(Light)
                call("init")

            with subroutine("init"):
                with Rung():
                    out(SubLight)

        mapping = TagMap(
            {Button: x[1], Light: y[1], SubLight: y[2]},
            include_system=False,
        )
        return logic, mapping

    def test_file_structure(self, tmp_path: Path):
        """Project with subroutines has correct file layout."""
        logic, mapping = self._make_program_with_sub()
        files = _project_from_program(logic, mapping, tmp_path)

        assert "main.py" in files
        assert "tags.py" in files
        assert "subroutines/__init__.py" in files
        assert "subroutines/init.py" in files
        assert files["subroutines/__init__.py"] == ""

    def test_subroutine_has_decorator(self, tmp_path: Path):
        """Subroutine file uses @subroutine decorator pattern."""
        logic, mapping = self._make_program_with_sub()
        files = _project_from_program(logic, mapping, tmp_path)

        sub_py = files["subroutines/init.py"]
        assert '@subroutine("init")' in sub_py
        assert "def init():" in sub_py
        assert "from tags import" in sub_py

    def test_main_calls_func_not_string(self, tmp_path: Path):
        """main.py uses call(func) not call('name')."""
        logic, mapping = self._make_program_with_sub()
        files = _project_from_program(logic, mapping, tmp_path)

        main_py = files["main.py"]
        assert "call(init)" in main_py
        assert 'call("init")' not in main_py
        assert "from subroutines.init import init" in main_py

    def test_exec_round_trip_with_subroutine(self, tmp_path: Path):
        """Full exec round-trip with subroutine."""
        logic, mapping = self._make_program_with_sub()

        # Get original CSV
        original_bundle = pyrung_to_ladder(logic, mapping)
        original_rows = list(original_bundle.main_rows)

        files = _project_from_program(logic, mapping, tmp_path)
        ns = _exec_project(files, tmp_path)

        # Re-export
        reproduced_bundle = pyrung_to_ladder(ns["logic"], ns["mapping"])
        reproduced_rows = list(reproduced_bundle.main_rows)

        assert original_rows == reproduced_rows

    def test_subroutine_with_conditions(self, tmp_path: Path):
        """Subroutine with conditional rungs round-trips."""
        Enable = Bool("Enable")
        Cond = Bool("Cond")
        Y1 = Bool("Y1")
        Y2 = Bool("Y2")

        with Program() as logic:
            with Rung(Enable):
                call("worker")

            with subroutine("worker"):
                with Rung(Cond):
                    out(Y1)
                with Rung():
                    out(Y2)

        mapping = TagMap(
            {Enable: x[1], Cond: x[2], Y1: y[1], Y2: y[2]},
            include_system=False,
        )

        original_bundle = pyrung_to_ladder(logic, mapping)
        original_rows = list(original_bundle.main_rows)

        files = _project_from_program(logic, mapping, tmp_path)
        ns = _exec_project(files, tmp_path)

        reproduced_bundle = pyrung_to_ladder(ns["logic"], ns["mapping"])
        reproduced_rows = list(reproduced_bundle.main_rows)

        assert original_rows == reproduced_rows


class TestPerFileImports:
    """Each file should only import what it uses."""

    def test_subroutine_imports_only_its_tags(self, tmp_path: Path):
        """Sub1 uses X001/Y001, Sub2 uses X002/Y002 — imports are isolated."""
        X1 = Bool("X1")
        X2 = Bool("X2")
        Y1 = Bool("Y1")
        Y2 = Bool("Y2")
        Main = Bool("Main")

        with Program() as logic:
            with Rung(Main):
                call("sub1")
                call("sub2")

            with subroutine("sub1"):
                with Rung(X1):
                    out(Y1)

            with subroutine("sub2"):
                with Rung(X2):
                    out(Y2)

        mapping = TagMap(
            {Main: c[1], X1: x[1], X2: x[2], Y1: y[1], Y2: y[2]},
            include_system=False,
        )
        files = _project_from_program(logic, mapping, tmp_path)

        sub1 = files["subroutines/sub1.py"]
        sub2 = files["subroutines/sub2.py"]

        # sub1 imports X1, Y1 but not X2, Y2
        assert "X001" in sub1 or "X1" in sub1
        assert "Y001" in sub1 or "Y1" in sub1
        # sub2 imports X2, Y2 but not X1, Y1
        assert "X002" in sub2 or "X2" in sub2
        assert "Y002" in sub2 or "Y2" in sub2

    def test_main_imports_mapping(self, tmp_path: Path):
        """main.py always imports mapping from tags."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        main_py = files["main.py"]
        assert "mapping" in main_py
        # mapping should come from tags
        from_tags_line = [ln for ln in main_py.splitlines() if ln.startswith("from tags import")]
        assert len(from_tags_line) == 1
        assert "mapping" in from_tags_line[0]

    def test_main_omits_any_of_for_two_way_bool_or(self, tmp_path: Path):
        """Two-way BOOL OR renders with | and does not import any_of."""
        A = Bool("A")
        B = Bool("B")
        Y = Bool("Y")

        with Program() as logic:
            with Rung(any_of(A, B)):
                out(Y)

        mapping = TagMap({A: x[1], B: x[2], Y: y[1]}, include_system=False)
        files = _project_from_program(logic, mapping, tmp_path)

        main_py = files["main.py"]
        assert "with Rung(" in main_py
        assert " | " in main_py
        assert "any_of" not in main_py

    def test_subroutine_imports_click_block_for_raw_range(self, tmp_path: Path):
        """Subroutine using reset(c.select(...)) must import c from pyrung.click."""
        Enable = Bool("Enable")
        Coil = Bool("Coil")
        Bits = Block("Bits", TagType.BOOL, 1, 3)

        with Program() as logic:
            with Rung(Enable):
                call("worker")

            with subroutine("worker"):
                with Rung():
                    out(Coil)
                    reset(Bits.select(1, 3))

        mapping = TagMap(
            {Enable: x[1], Coil: y[1], Bits: c.select(1004, 1006)},
            include_system=False,
        )
        files = _project_from_program(logic, mapping, tmp_path)
        sub_py = files["subroutines/worker.py"]

        # Must have a pyrung.click import that includes 'c'
        click_import = [
            ln for ln in sub_py.splitlines() if ln.startswith("from pyrung.click import")
        ]
        assert click_import, "subroutine should have a pyrung.click import line"
        assert "c" in click_import[0].split("import")[1]

        # Verify the project can actually be imported and executed
        ns = _exec_project(files, tmp_path)
        assert ns["logic"] is not None

    def test_subroutine_imports_click_block_for_pointer(self, tmp_path: Path):
        """Subroutine using copy(DH[idx], dest) must import dh from pyrung.click."""
        from pyrung.click.codegen.collector import _collect_operands
        from pyrung.click.codegen.models import (
            _AnalyzedRung,
            _InstructionInfo,
            _SubroutineInfo,
        )
        from pyrung.click.codegen.project_emitter import _generate_project

        main_rung = _AnalyzedRung(
            comment=None,
            condition_tree=None,
            instructions=[_InstructionInfo(af_token='call("worker")', branch_tree=None, pins=[])],
        )
        sub_rung = _AnalyzedRung(
            comment=None,
            condition_tree=None,
            instructions=[
                _InstructionInfo(af_token="copy(DH[DS134],DH051)", branch_tree=None, pins=[]),
            ],
        )

        subroutines = [_SubroutineInfo(name="worker", analyzed=[sub_rung])]
        all_rungs = [main_rung, sub_rung]
        collection = _collect_operands(all_rungs, None)
        collection.has_subroutine = True

        files = _generate_project([main_rung], collection, None, subroutines)
        sub_py = files["subroutines/worker.py"]

        # Must have dh imported from pyrung.click
        click_import = [
            ln for ln in sub_py.splitlines() if ln.startswith("from pyrung.click import")
        ]
        assert click_import, "subroutine should have a pyrung.click import line"
        assert " dh" in click_import[0] or ",dh" in click_import[0]

        # Should use lowercase block variable, not uppercase Click prefix
        assert "dh[" in sub_py
        assert "DH[" not in sub_py


class TestSubroutineCallsSubroutine:
    """Subroutine that calls another subroutine.

    Note: Click hardware doesn't support nested subroutine calls, so
    we test the codegen import structure directly (not via CSV round-trip).
    """

    def test_cross_sub_import(self, tmp_path: Path):
        """sub_a calls sub_b — sub_a.py imports from subroutines.sub_b."""
        from pyrung.click.codegen.collector import _collect_operands
        from pyrung.click.codegen.models import (
            _AnalyzedRung,
            _InstructionInfo,
            _SubroutineInfo,
        )
        from pyrung.click.codegen.project_emitter import _generate_project

        # Build analyzed rungs by hand (bypassing CSV which rejects nested calls)
        main_rung = _AnalyzedRung(
            comment=None,
            condition_tree=None,
            instructions=[_InstructionInfo(af_token='call("sub_a")', branch_tree=None, pins=[])],
        )
        sub_a_rung = _AnalyzedRung(
            comment=None,
            condition_tree=None,
            instructions=[
                _InstructionInfo(af_token='call("sub_b")', branch_tree=None, pins=[]),
                _InstructionInfo(af_token="out(Y001)", branch_tree=None, pins=[]),
            ],
        )
        sub_b_rung = _AnalyzedRung(
            comment=None,
            condition_tree=None,
            instructions=[_InstructionInfo(af_token="out(Y002)", branch_tree=None, pins=[])],
        )

        subroutines = [
            _SubroutineInfo(name="sub_a", analyzed=[sub_a_rung]),
            _SubroutineInfo(name="sub_b", analyzed=[sub_b_rung]),
        ]

        all_rungs = [main_rung, sub_a_rung, sub_b_rung]
        collection = _collect_operands(all_rungs, None)
        collection.has_subroutine = True

        files = _generate_project([main_rung], collection, None, subroutines)

        sub_a = files["subroutines/sub_a.py"]
        assert "from subroutines.sub_b import sub_b" in sub_a
        assert "call(sub_b)" in sub_a


class TestNicknames:
    """Nickname variable names in project output."""

    def test_nicknames_in_all_files(self, tmp_path: Path):
        """Nicknames appear in tags.py and are used in main/sub files."""
        Button = Bool("Button")
        Light = Bool("Light")
        SubLight = Bool("SubLight")

        with Program() as logic:
            with Rung(Button):
                out(Light)
                call("init")

            with subroutine("init"):
                with Rung():
                    out(SubLight)

        mapping = TagMap(
            {Button: x[1], Light: y[1], SubLight: y[2]},
            include_system=False,
        )
        nicks = {"X001": "Button", "Y001": "Light", "Y002": "SubLight"}
        files = _project_from_program(logic, mapping, tmp_path, nicknames=nicks)

        tags_py = files["tags.py"]
        assert 'Button = Bool("Button")' in tags_py
        assert 'Light = Bool("Light")' in tags_py

        main_py = files["main.py"]
        assert "Button" in main_py
        assert "Light" in main_py

        sub_py = files["subroutines/init.py"]
        assert "SubLight" in sub_py


class TestDiskWrite:
    """Writing project to disk."""

    def test_output_dir(self, tmp_path: Path):
        """ladder_to_pyrung_project writes files to output_dir."""
        Button = Bool("Button")
        Light = Bool("Light")

        with Program() as logic:
            with Rung(Button):
                out(Light)

        mapping = TagMap({Button: x[1], Light: y[1]}, include_system=False)
        bundle = pyrung_to_ladder(logic, mapping)
        csv_dir = tmp_path / "csv"
        bundle.write(csv_dir)

        out_dir = tmp_path / "output"
        files = ladder_to_pyrung_project(csv_dir, output_dir=out_dir)

        assert (out_dir / "main.py").exists()
        assert (out_dir / "tags.py").exists()
        assert (out_dir / "main.py").read_text(encoding="utf-8") == files["main.py"]


class TestTimerCounterInstructions:
    """Project output with timer and counter instructions."""

    def test_timer_round_trip(self, tmp_path: Path):
        """Timer instructions produce correct project output."""
        Enable = Bool("Enable")
        Done = Bool("Done")
        Acc = Int("Acc")

        with Program() as logic:
            with Rung(Enable):
                on_delay(Done, Acc, preset=1000, unit=Tms)

        mapping = TagMap(
            {Enable: x[1], Done: t[1], Acc: td[1]},
            include_system=False,
        )
        files = _project_from_program(logic, mapping, tmp_path)
        ns = _exec_project(files, tmp_path)

        original_bundle = pyrung_to_ladder(logic, mapping)
        reproduced_bundle = pyrung_to_ladder(ns["logic"], ns["mapping"])
        assert list(original_bundle.main_rows) == list(reproduced_bundle.main_rows)

    def test_counter_round_trip(self, tmp_path: Path):
        """Counter instructions produce correct project output."""
        Enable = Bool("Enable")
        ResetCond = Bool("ResetCond")
        Done = Bool("Done")
        Acc = Dint("Acc")

        with Program() as logic:
            with Rung(Enable):
                count_up(Done, Acc, preset=10).reset(ResetCond)

        mapping = TagMap(
            {Enable: x[1], ResetCond: x[2], Done: ct[1], Acc: ctd[1]},
            include_system=False,
        )
        files = _project_from_program(logic, mapping, tmp_path)
        ns = _exec_project(files, tmp_path)

        original_bundle = pyrung_to_ladder(logic, mapping)
        reproduced_bundle = pyrung_to_ladder(ns["logic"], ns["mapping"])
        assert list(original_bundle.main_rows) == list(reproduced_bundle.main_rows)
