"""Integration tests for Quetzal save/restore round-trip.

Tests that verify the complete save/restore pipeline works end-to-end by
generating save data with QuetzalWriter and restoring it with QuetzalParser.

Field mapping reminder:
- Quetzal return_pc for frame N → caller (frame N-1) program_counter
- Quetzal varnum → frame.return_addr (store variable for return value)
"""


class TestQuetzalRoundTrip:
    """Test complete save/restore cycle with real zmachine state."""

    def test_basic_round_trip(self):
        """Test saving and restoring basic zmachine state."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
        from mudlib.zmachine.zmemory import ZMemory
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        # Create a minimal z-machine story file (V3)
        story_data = bytearray(8192)
        story_data[0] = 3  # Version 3
        story_data[0x02:0x04] = [0x12, 0x34]  # Release number
        story_data[0x04:0x06] = [0x10, 0x00]  # High memory start
        story_data[0x06:0x08] = [0x08, 0x00]  # Initial PC
        story_data[0x0E:0x10] = [0x04, 0x00]  # Static memory start
        story_data[0x0C:0x0E] = [0x02, 0x00]  # Global variables start
        story_data[0x12:0x18] = b"860101"  # Serial number
        story_data[0x1C:0x1E] = [0xAB, 0xCD]  # Checksum

        pristine_mem = ZMemory(bytes(story_data))
        current_mem = ZMemory(bytes(story_data))

        # Modify some bytes in dynamic memory (before static start at 0x400)
        current_mem[0x100] = 0x42
        current_mem[0x101] = 0x43
        current_mem[0x200] = 0xFF

        # Create a stack with one frame
        # return_addr=5 means "store return value in local var 4"
        stack_manager = ZStackManager(current_mem)
        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=5,
            zmem=current_mem,
            args=[],
            local_vars=[0x0001, 0x0002, 0x0003],
            stack=[0x1111, 0x2222],
        )
        stack_manager.push_routine(routine)

        # Set up mock zmachine
        zmachine = Mock()
        zmachine._pristine_mem = pristine_mem
        zmachine._mem = current_mem
        zmachine._stackmanager = stack_manager
        zmachine._cpu = Mock()
        zmachine._cpu._program_counter = 0x0850
        zmachine._opdecoder = Mock()

        # SAVE
        writer = QuetzalWriter(zmachine)
        save_data = writer.generate_save_data()

        assert save_data[:4] == b"FORM"
        assert save_data[8:12] == b"IFZS"

        # CORRUPT: Change the zmachine state
        current_mem[0x100] = 0x99
        current_mem[0x101] = 0x99
        current_mem[0x200] = 0x00
        stack_manager._call_stack.clear()
        from mudlib.zmachine.zstackmanager import ZStackBottom

        stack_manager._call_stack.append(ZStackBottom())
        zmachine._cpu._program_counter = 0x9999

        assert current_mem[0x100] == 0x99
        assert len(stack_manager._call_stack) == 1
        assert zmachine._cpu._program_counter == 0x9999

        # RESTORE
        parser = QuetzalParser(zmachine)
        parser.load_from_bytes(save_data)

        # VERIFY: Memory was restored
        assert current_mem[0x100] == 0x42
        assert current_mem[0x101] == 0x43
        assert current_mem[0x200] == 0xFF

        # VERIFY: Stack was restored
        restored_stack = zmachine._stackmanager
        assert len(restored_stack._call_stack) == 2  # Bottom + one frame
        restored_frame = restored_stack._call_stack[1]
        # return_addr is the store variable (varnum), not a PC
        assert restored_frame.return_addr == 5
        assert restored_frame.local_vars[:3] == [0x0001, 0x0002, 0x0003]
        assert restored_frame.stack == [0x1111, 0x2222]

        # VERIFY: Program counter was restored
        assert zmachine._opdecoder.program_counter == 0x0850

    def test_round_trip_with_multiple_frames(self):
        """Test save/restore with nested call frames."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
        from mudlib.zmachine.zmemory import ZMemory
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        story_data = bytearray(8192)
        story_data[0] = 3
        story_data[0x02:0x04] = [0x10, 0x00]
        story_data[0x04:0x06] = [0x10, 0x00]
        story_data[0x06:0x08] = [0x08, 0x00]
        story_data[0x0E:0x10] = [0x04, 0x00]
        story_data[0x0C:0x0E] = [0x02, 0x00]
        story_data[0x12:0x18] = b"860101"
        story_data[0x1C:0x1E] = [0x00, 0x00]

        pristine_mem = ZMemory(bytes(story_data))
        current_mem = ZMemory(bytes(story_data))

        # Create nested call frames with proper semantics:
        # return_addr = store variable (varnum)
        # program_counter = resume PC for when the next frame returns
        stack_manager = ZStackManager(current_mem)

        routine1 = ZRoutine(
            start_addr=0x5000,
            return_addr=0,  # varnum 0 = push to stack
            zmem=current_mem,
            args=[],
            local_vars=[0xAAAA, 0xBBBB],
            stack=[0x1111],
        )
        stack_manager.push_routine(routine1)
        # resume PC in routine1 after routine2 returns
        routine1.program_counter = 0x5123

        routine2 = ZRoutine(
            start_addr=0x6000,
            return_addr=3,  # varnum 3 = local var 2
            zmem=current_mem,
            args=[],
            local_vars=[0xCCCC],
            stack=[0x2222, 0x3333],
        )
        stack_manager.push_routine(routine2)

        zmachine = Mock()
        zmachine._pristine_mem = pristine_mem
        zmachine._mem = current_mem
        zmachine._stackmanager = stack_manager
        zmachine._cpu = Mock()
        zmachine._cpu._program_counter = 0x0900
        zmachine._opdecoder = Mock()

        # Save
        writer = QuetzalWriter(zmachine)
        save_data = writer.generate_save_data()

        # Clear stack
        stack_manager._call_stack.clear()
        from mudlib.zmachine.zstackmanager import ZStackBottom

        stack_manager._call_stack.append(ZStackBottom())

        # Restore
        parser = QuetzalParser(zmachine)
        parser.load_from_bytes(save_data)

        # Verify both frames restored
        restored_stack = zmachine._stackmanager
        assert len(restored_stack._call_stack) == 3  # Bottom + two frames

        frame1 = restored_stack._call_stack[1]
        assert frame1.return_addr == 0  # varnum
        assert frame1.local_vars[:2] == [0xAAAA, 0xBBBB]
        assert frame1.stack == [0x1111]
        # frame1 should have the resume PC for after frame2 returns
        assert frame1.program_counter == 0x5123

        frame2 = restored_stack._call_stack[2]
        assert frame2.return_addr == 3  # varnum
        assert frame2.local_vars[:1] == [0xCCCC]
        assert frame2.stack == [0x2222, 0x3333]

    def test_round_trip_preserves_unchanged_memory(self):
        """Test that unchanged memory bytes are preserved correctly."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
        from mudlib.zmachine.zmemory import ZMemory
        from mudlib.zmachine.zstackmanager import ZStackManager

        story_data = bytearray(8192)
        story_data[0] = 3
        story_data[0x02:0x04] = [0x10, 0x00]
        story_data[0x04:0x06] = [0x10, 0x00]
        story_data[0x06:0x08] = [0x08, 0x00]
        story_data[0x0E:0x10] = [0x04, 0x00]
        story_data[0x0C:0x0E] = [0x02, 0x00]
        story_data[0x12:0x18] = b"860101"
        story_data[0x1C:0x1E] = [0x00, 0x00]

        for i in range(0x100, 0x200):
            story_data[i] = i & 0xFF

        pristine_mem = ZMemory(bytes(story_data))
        current_mem = ZMemory(bytes(story_data))

        current_mem[0x150] = 0xFF
        current_mem[0x180] = 0xAA

        stack_manager = ZStackManager(current_mem)

        zmachine = Mock()
        zmachine._pristine_mem = pristine_mem
        zmachine._mem = current_mem
        zmachine._stackmanager = stack_manager
        zmachine._cpu = Mock()
        zmachine._cpu._program_counter = 0x0800
        zmachine._opdecoder = Mock()

        writer = QuetzalWriter(zmachine)
        save_data = writer.generate_save_data()

        for i in range(0x100, 0x200):
            current_mem[i] = 0x00

        parser = QuetzalParser(zmachine)
        parser.load_from_bytes(save_data)

        for i in range(0x100, 0x200):
            if i == 0x150:
                assert current_mem[i] == 0xFF
            elif i == 0x180:
                assert current_mem[i] == 0xAA
            else:
                assert current_mem[i] == (i & 0xFF)

    def test_round_trip_empty_stack(self):
        """Test save/restore with no routine frames (just bottom sentinel)."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
        from mudlib.zmachine.zmemory import ZMemory
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        story_data = bytearray(8192)
        story_data[0] = 3
        story_data[0x02:0x04] = [0x10, 0x00]
        story_data[0x04:0x06] = [0x10, 0x00]
        story_data[0x06:0x08] = [0x08, 0x00]
        story_data[0x0E:0x10] = [0x04, 0x00]
        story_data[0x0C:0x0E] = [0x02, 0x00]
        story_data[0x12:0x18] = b"860101"
        story_data[0x1C:0x1E] = [0x00, 0x00]

        pristine_mem = ZMemory(bytes(story_data))
        current_mem = ZMemory(bytes(story_data))
        current_mem[0x100] = 0x42

        stack_manager = ZStackManager(current_mem)

        zmachine = Mock()
        zmachine._pristine_mem = pristine_mem
        zmachine._mem = current_mem
        zmachine._stackmanager = stack_manager
        zmachine._cpu = Mock()
        zmachine._cpu._program_counter = 0x0800
        zmachine._opdecoder = Mock()

        writer = QuetzalWriter(zmachine)
        save_data = writer.generate_save_data()

        # Add a dummy frame to verify it gets cleared
        dummy = ZRoutine(
            start_addr=0x5000,
            return_addr=1,
            zmem=current_mem,
            args=[],
            local_vars=[0x9999],
            stack=[],
        )
        stack_manager.push_routine(dummy)

        parser = QuetzalParser(zmachine)
        parser.load_from_bytes(save_data)

        restored_stack = zmachine._stackmanager
        assert len(restored_stack._call_stack) == 1
        assert current_mem[0x100] == 0x42
