"""Tests for QuetzalWriter Stks chunk generation.

Field mapping:
- Quetzal return_pc → previous frame's program_counter (caller resume PC)
- Quetzal varnum → frame.return_addr (store variable for return value)
"""


class TestStksChunkGeneration:
    """Test Stks chunk generation and serialization."""

    def test_empty_stack_serialization(self):
        """Test serializing an empty stack (just the bottom sentinel)."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        assert result == b""

    def test_single_frame_no_locals_no_stack(self):
        """Test serializing a single routine frame with no locals or stack values."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)

        # Set caller resume PC on sentinel
        stack_manager._call_stack[0].program_counter = 0x1234

        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=7,  # varnum: store to local var 6
            zmem=zmachine._mem,
            args=[],
            local_vars=[],
            stack=[],
        )
        stack_manager.push_routine(routine)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                0x00,
                0x12,
                0x34,  # return_pc (from sentinel.program_counter)
                0x00,  # flags (0 locals)
                0x07,  # varnum (frame.return_addr)
                0x00,  # argflag
                0x00,
                0x00,  # eval_stack_size = 0
            ]
        )

        assert result == expected

    def test_single_frame_with_locals(self):
        """Test serializing a frame with local variables."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)
        stack_manager._call_stack[0].program_counter = 0x2000

        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=0x10,  # varnum: store to global var 0x10
            zmem=zmachine._mem,
            args=[],
            local_vars=[0x1111, 0x2222, 0x3333],
            stack=[],
        )
        stack_manager.push_routine(routine)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                0x00,
                0x20,
                0x00,  # return_pc (from sentinel.program_counter)
                0x03,  # flags (3 locals)
                0x10,  # varnum (frame.return_addr)
                0x00,  # argflag
                0x00,
                0x00,  # eval_stack_size = 0
                0x11,
                0x11,  # local_var[0]
                0x22,
                0x22,  # local_var[1]
                0x33,
                0x33,  # local_var[2]
            ]
        )

        assert result == expected

    def test_single_frame_with_stack_values(self):
        """Test serializing a frame with evaluation stack values."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)
        stack_manager._call_stack[0].program_counter = 0x3000

        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=0,  # varnum 0: push result to eval stack
            zmem=zmachine._mem,
            args=[],
            local_vars=[],
            stack=[0xABCD, 0xEF01],
        )
        stack_manager.push_routine(routine)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                0x00,
                0x30,
                0x00,  # return_pc
                0x00,  # flags (0 locals)
                0x00,  # varnum (push to stack)
                0x00,  # argflag
                0x00,
                0x02,  # eval_stack_size = 2
                0xAB,
                0xCD,  # stack[0]
                0xEF,
                0x01,  # stack[1]
            ]
        )

        assert result == expected

    def test_single_frame_full(self):
        """Test serializing a frame with both locals and stack values."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)
        stack_manager._call_stack[0].program_counter = 0x4567

        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=2,  # varnum: store to local var 1
            zmem=zmachine._mem,
            args=[],
            local_vars=[0x0001, 0x0002],
            stack=[0x1000, 0x2000, 0x3000],
        )
        stack_manager.push_routine(routine)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                0x00,
                0x45,
                0x67,  # return_pc (from sentinel.program_counter)
                0x02,  # flags (2 locals)
                0x02,  # varnum
                0x00,  # argflag
                0x00,
                0x03,  # eval_stack_size = 3
                0x00,
                0x01,  # local_var[0]
                0x00,
                0x02,  # local_var[1]
                0x10,
                0x00,  # stack[0]
                0x20,
                0x00,  # stack[1]
                0x30,
                0x00,  # stack[2]
            ]
        )

        assert result == expected

    def test_multiple_nested_frames(self):
        """Test serializing multiple nested routine frames."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)

        # Sentinel has return PC for frame 1
        stack_manager._call_stack[0].program_counter = 0x1000

        routine1 = ZRoutine(
            start_addr=0x5000,
            return_addr=0,  # varnum: push to stack
            zmem=zmachine._mem,
            args=[],
            local_vars=[0xAAAA],
            stack=[0xBBBB],
        )
        stack_manager.push_routine(routine1)
        # Frame1 has return PC for frame 2
        routine1.program_counter = 0x2000

        routine2 = ZRoutine(
            start_addr=0x6000,
            return_addr=5,  # varnum: store to local var 4
            zmem=zmachine._mem,
            args=[],
            local_vars=[0xCCCC],
            stack=[0xDDDD],
        )
        stack_manager.push_routine(routine2)

        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                # Frame 1: return_pc from sentinel (0x1000), varnum=0
                0x00,
                0x10,
                0x00,  # return_pc
                0x01,  # flags (1 local)
                0x00,  # varnum
                0x00,  # argflag
                0x00,
                0x01,  # eval_stack_size = 1
                0xAA,
                0xAA,  # local_var[0]
                0xBB,
                0xBB,  # stack[0]
                # Frame 2: return_pc from routine1 (0x2000), varnum=5
                0x00,
                0x20,
                0x00,  # return_pc
                0x01,  # flags (1 local)
                0x05,  # varnum
                0x00,  # argflag
                0x00,
                0x01,  # eval_stack_size = 1
                0xCC,
                0xCC,  # local_var[0]
                0xDD,
                0xDD,  # stack[0]
            ]
        )

        assert result == expected

    def test_bottom_frame_zero_return_pc(self):
        """Test that a bottom/dummy frame with return_pc=0 is handled correctly."""
        from unittest.mock import Mock

        from mudlib.zmachine.quetzal import QuetzalWriter
        from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5
        stack_manager = ZStackManager(zmachine._mem)
        # Sentinel PC = 0 (main routine has no caller)
        stack_manager._call_stack[0].program_counter = 0

        routine = ZRoutine(
            start_addr=0x5000,
            return_addr=0,  # varnum: push to stack
            zmem=zmachine._mem,
            args=[],
            local_vars=[0x1234],
            stack=[],
        )
        stack_manager.push_routine(routine)
        zmachine._stackmanager = stack_manager

        writer = QuetzalWriter(zmachine)
        result = writer._generate_stks_chunk()

        expected = bytes(
            [
                0x00,
                0x00,
                0x00,  # return_pc = 0
                0x01,  # flags (1 local)
                0x00,  # varnum
                0x00,  # argflag
                0x00,
                0x00,  # eval_stack_size = 0
                0x12,
                0x34,  # local_var[0]
            ]
        )

        assert result == expected


class TestStksRoundTrip:
    """Test that Stks serialization/deserialization is symmetrical."""

    def test_round_trip_serialization(self):
        """Test that we can serialize and deserialize frames correctly."""
        from unittest.mock import Mock

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

        zmachine = Mock()
        zmachine._mem = Mock()
        zmachine._mem.version = 5

        original_stack = ZStackManager(zmachine._mem)
        # Sentinel has return PC for frame 1
        original_stack._call_stack[0].program_counter = 0

        routine1 = ZRoutine(
            start_addr=0x5000,
            return_addr=5,  # varnum: store to local var 4
            zmem=zmachine._mem,
            args=[],
            local_vars=[0x0001, 0x0002, 0x0003],
            stack=[0x1111, 0x2222],
        )
        original_stack.push_routine(routine1)
        routine1.program_counter = 0x5678  # resume PC for after routine2

        routine2 = ZRoutine(
            start_addr=0x6000,
            return_addr=3,  # varnum: store to local var 2
            zmem=zmachine._mem,
            args=[],
            local_vars=[0xAAAA],
            stack=[0xBBBB, 0xCCCC, 0xDDDD],
        )
        original_stack.push_routine(routine2)

        zmachine._stackmanager = original_stack

        # Serialize
        writer = QuetzalWriter(zmachine)
        stks_data = writer._generate_stks_chunk()

        # Deserialize
        parser = QuetzalParser(zmachine)
        parser._parse_stks(stks_data)

        restored_stack = zmachine._stackmanager

        assert len(restored_stack._call_stack) == 3

        # Check frame 1: return_addr is varnum
        frame1 = restored_stack._call_stack[1]
        assert frame1.return_addr == 5
        assert frame1.local_vars[:3] == [0x0001, 0x0002, 0x0003]
        assert frame1.stack == [0x1111, 0x2222]
        # Frame1's program_counter was set from frame2's return_pc
        assert frame1.program_counter == 0x5678

        # Check frame 2
        frame2 = restored_stack._call_stack[2]
        assert frame2.return_addr == 3
        assert frame2.local_vars[:1] == [0xAAAA]
        assert frame2.stack == [0xBBBB, 0xCCCC, 0xDDDD]
