import hashlib
import os
from textwrap import dedent
import urllib
from zipfile import ZipFile
from io import BytesIO
from unittest.mock import patch, Mock

from skoolkittest import (SkoolKitTestCase, PZX, create_header_block,
                          create_data_block, create_tap_header_block,
                          create_tap_data_block, create_tzx_header_block,
                          create_tzx_data_block, create_tzx_turbo_data_block,
                          create_tzx_pure_data_block, mock_find_file)
from skoolkit import (components, config, tap2sna, VERSION, SkoolKitError,
                      CSimulator, CCMIOSimulator)
from skoolkit.cmiosimulator import CMIOSimulator
from skoolkit.loadtracer import LoadTracer
from skoolkit.simulator import Simulator

if CSimulator is None:
    # Pre-load the simtables module so that its (significant) load time does
    # not become part of the test run time
    from skoolkit import simtables

class MockSimulator:
    def __init__(self, memory, registers, state, config):
        global simulator
        self.memory = memory
        self.registers = [0] * 30
        self.frame_duration = config['frame_duration']
        self.config = config
        simulator = self

    def set_tracer(self, tracer, *args):
        self.tracer = tracer
        self.set_tracer_args = args

def mock_make_snapshot(*args):
    global make_snapshot_args
    make_snapshot_args = args

def mock_write_snapshot(fname, ram, registers, state):
    global s_fname, snapshot, s_banks, s_reg, s_state
    s_fname = fname
    snapshot = [0] * 65536
    if len(ram) == 8:
        s_banks = ram
        page = 0
        for spec in state:
            if spec.startswith('7ffd='):
                page = int(spec[5:]) % 8
                break
        snapshot[0x4000:0x8000] = ram[5]
        snapshot[0x8000:0xC000] = ram[2]
        snapshot[0xC000:] = ram[page]
    else:
        s_banks = None
        snapshot[0x4000:] = ram
    s_reg = registers
    s_state = state

def null_write_snapshot(fname, ram, registers, state):
    pass

class MockKeyboardTracer:
    def __init__(self, simulator, load, kb_delay, draw):
        global kbtracer
        self.simulator = simulator
        self.load = load
        self.kb_delay = kb_delay
        self.draw = draw
        self.run_called = False
        kbtracer = self

    def run(self, stop, timeout, tracefile, trace_line, prefix, byte_fmt, word_fmt):
        self.stop = stop
        self.timeout = timeout
        self.tracefile = tracefile
        self.trace_line = trace_line
        self.prefix = prefix
        self.byte_fmt = byte_fmt
        self.word_fmt = word_fmt
        self.border = id(self)
        self.out7ffd = 0
        self.outfffd = id(self) + 2
        self.ay = [id(self) + 3] * 16
        self.outfe = id(self) + 4
        self.simulator.registers[25] = 70000 * len(self.load)
        self.run_called = True

class MockLoadTracer(LoadTracer):
    def __init__(self, simulator, blocks, config, draw):
        global load_tracer
        load_tracer = self
        self.init_args = {'blocks': blocks, 'config': config}
        super().__init__(simulator, blocks, config, draw)

    def run(self, border, out7ffd, outfffd, ay, outfe):
        self.border = border
        self.out7ffd = out7ffd
        self.outfffd = outfffd
        self.ay = ay
        self.outfe = outfe

class MockScreen:
    def __init__(self, scale, fps, caption, is128k, stay_open=True):
        global screen
        screen = self
        self.scale = scale
        self.fps = fps
        self.caption = caption
        self.is128k = is128k
        self.stay_open = stay_open
        self.pygame_msg = 'Using pygame'
        self.scr = None
        self.frame = None
        self.border = None

    def draw(self, scr, frame, border):
        self.scr = scr
        self.frame = frame
        self.border = border
        return self.stay_open

def mock_get_screen(scale, fps, caption, is128k):
    return MockScreen(scale, fps, caption, is128k)

def mock_get_screen_close(scale, fps, caption, is128k):
    return MockScreen(scale, fps, caption, is128k, False)

class InterruptedTracer:
    def __init__(self, *args):
        self.border = 0
        self.out7ffd = 0
        self.outfffd = 0
        self.ay = [0] * 16
        self.outfe = 0

    def run(self, *args):
        raise KeyboardInterrupt()

class Tap2SnaTest(SkoolKitTestCase):
    def setUp(self):
        global kbtracer, load_tracer, simulator
        super().setUp()
        patch.object(config, 'find_file', mock_find_file).start()
        self.addCleanup(patch.stopall)
        kbtracer = load_tracer = simulator = None

    def _write_tap(self, blocks, zip_archive=False, tap_name=None):
        tap_data = []
        for block in blocks:
            tap_data.extend(block)
        if zip_archive:
            archive_fname = self.write_bin_file(suffix='.zip')
            with ZipFile(archive_fname, 'w') as archive:
                archive.writestr(tap_name or 'game.tap', bytearray(tap_data))
            return archive_fname
        return self.write_bin_file(tap_data, suffix='.tap')

    def _write_tzx(self, blocks):
        tzx_data = [ord(c) for c in "ZXTape!"]
        tzx_data.extend((26, 1, 20))
        for block in blocks:
            tzx_data.extend(block)
        return self.write_bin_file(tzx_data, suffix='.tzx')

    def _write_basic_loader(self, start, data, write=True, program='simloadbas', code='simloadbyt'):
        start_fp = (0, 0, start % 256, start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *start_fp,    # start address in floating point form
            13                # ENTER
        ]
        blocks = [
            create_tap_header_block(program, 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block(code, start, len(data)),
            create_tap_data_block(data)
        ]
        if write:
            return self._write_tap(blocks), basic_data
        return blocks, basic_data

    def _load_tape(self, start=16384, data=None, options='', load_options=None, blocks=None, tzx=False, pzx=None):
        if pzx:
            tape_file = self.write_bin_file(pzx.data, suffix='.pzx')
        else:
            if blocks is None:
                blocks = [create_tap_data_block(data)]
            if tzx:
                tape_file = self._write_tzx(blocks)
            else:
                tape_file = self._write_tap(blocks)
        if load_options is None:
            load_options = f'--ram load=1,{start}'
        output, error = self.run_tap2sna(f'{load_options} {options} {tape_file} out.z80')
        self.assertEqual(output, 'Writing out.z80\n')
        self.assertEqual(error, '')

    def _test_bad_spec(self, option, exp_error):
        tapfile = self._write_tap([create_tap_data_block([1])])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--ram load=1,16384 {option} {tapfile} test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile}: {exp_error}')

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_default_option_values(self):
        self.run_tap2sna('in.tap out.z80')
        options = make_snapshot_args[1]
        self.assertIsNone(options.output_dir)
        self.assertIsNone(options.stack)
        self.assertEqual([], options.ram_ops)
        self.assertEqual([], options.reg)
        self.assertFalse(options.screen)
        self.assertIsNone(options.start)
        self.assertEqual([], options.sim_load_config)
        self.assertEqual([], options.state)
        self.assertEqual([], options.tape_name)
        self.assertEqual(options.tape_start, 1)
        self.assertEqual(options.tape_stop, 0)
        self.assertEqual([], options.tape_sum)
        self.assertEqual(options.user_agent, '')
        self.assertEqual([], options.params)

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_config_read_from_file(self):
        ini = """
            [tap2sna]
            TraceLine={pc} - {i}
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        self.run_tap2sna('in.tap out.z80')
        options, z80, config = make_snapshot_args[1:4]
        self.assertIsNone(options.output_dir)
        self.assertIsNone(options.stack)
        self.assertEqual([], options.ram_ops)
        self.assertEqual([], options.reg)
        self.assertIsNone(options.start)
        self.assertEqual([], options.sim_load_config)
        self.assertEqual([], options.state)
        self.assertEqual([], options.tape_name)
        self.assertEqual(options.tape_start, 1)
        self.assertEqual(options.tape_stop, 0)
        self.assertEqual([], options.tape_sum)
        self.assertEqual(options.user_agent, '')
        self.assertEqual([], options.params)
        self.assertEqual(config['TraceLine'], '{pc} - {i}')

    def test_no_arguments(self):
        output, error = self.run_tap2sna(catch_exit=2)
        self.assertEqual(output, '')
        self.assertTrue(error.startswith('usage:'))

    def test_invalid_arguments(self):
        for args in ('--foo', '-k test.zip'):
            output, error = self.run_tap2sna(args, catch_exit=2)
            self.assertEqual(output, '')
            self.assertTrue(error.startswith('usage:'))

    def test_two_zip_archive_arguments(self):
        output, error = self.run_tap2sna('1.zip 2.zip', catch_exit=2)
        self.assertEqual(output, '')
        self.assertTrue(error.startswith('usage:'))

    def test_two_zip_archive_arguments_with_snapshot_argument(self):
        output, error = self.run_tap2sna('1.zip 2.zip out.z80', catch_exit=2)
        self.assertEqual(output, '')
        self.assertTrue(error.startswith('usage:'))

    def test_too_many_arguments(self):
        output, error = self.run_tap2sna('1.tap 2.tzx out.z80 out.szx', catch_exit=2)
        self.assertEqual(output, '')
        self.assertTrue(error.startswith('usage:'))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_accelerator_unrecognised(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c accelerator=nope {tapfile} out.z80')
        self.assertEqual(error, 'WARNING: Unrecognised accelerator: nope\n')

    def test_unrecognised_sim_load_configuration_parameter(self):
        blocks = [create_tap_data_block([0])]
        tapfile = self._write_tap(blocks)
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'-c foo=bar {tapfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile}: Invalid sim-load configuration parameter: foo')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_no_snapshot_argument_with_tap_file(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        exp_z80_fname = tapfile[:-4] + '.z80'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tapfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_z80_fname)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_no_snapshot_argument_with_two_tap_files(self):
        tapfile1 = self.write_bin_file(create_tap_data_block([0]), 'gamecode.tap')
        tapfile2 = self.write_bin_file(create_tap_data_block([1]), 'gamelevels.tap')
        output, error = self.run_tap2sna(f'--ram load=1,16384 --ram load=2,16385 {tapfile1} {tapfile2}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, 'game.z80')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_no_snapshot_argument_with_tzx_file(self):
        tzxfile = self._write_tzx([create_tzx_data_block([0])])
        exp_z80_fname = tzxfile[:-4] + '.z80'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tzxfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_z80_fname)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_no_snapshot_argument_with_zip_file(self):
        zipfile = self._write_tap([create_tap_data_block([0])], True, 'great_game.tap')
        exp_z80_fname = 'great_game.z80'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {zipfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_z80_fname)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_no_snapshot_argument_with_unconventionally_named_tap_file(self):
        tapfile = self.write_bin_file(create_tap_data_block([0]), suffix='.tape')
        exp_z80_fname = tapfile + '.z80'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tapfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_z80_fname)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    @patch.object(tap2sna, 'urlopen', Mock(return_value=BytesIO(bytearray(create_tap_data_block([1])))))
    def test_no_snapshot_argument_with_remote_download(self):
        url = 'http://example.com/test.tap'
        exp_z80_fname = 'test.z80'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {url}')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, exp_z80_fname)
        self.assertEqual(snapshot[16384], 1)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tape_file_with_hash_in_name(self):
        code = [1, 2, 3, 4, 5]
        start = 24576
        tap_data = create_tap_data_block(code)
        tapfile = 'game#2.tap'
        self.write_bin_file(tap_data, tapfile)
        output, error = self.run_tap2sna(f'--ram load=1,{start} {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(code, snapshot[start:start + len(code)])

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_two_tapes(self):
        tap1_data = create_tap_data_block([1, 2, 3, 4, 5])
        tap1 = self.write_bin_file(tap1_data, suffix='.tap')
        tap2_data = create_tap_data_block([6, 7, 8])
        tap2 = self.write_bin_file(tap2_data, suffix='.tap')
        output, error = self.run_tap2sna(f'{tap1} {tap2} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(len(load_tracer.blocks), 2)
        self.assertEqual(tap1_data[2:], list(load_tracer.blocks[0].data))
        self.assertEqual(tap2_data[2:], list(load_tracer.blocks[1].data))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_d(self):
        odir = 'tap2sna'
        tapfile = self._write_tap([create_tap_data_block([0])])
        z80_fname = 'test.z80'
        for option in ('-d', '--output-dir'):
            output, error = self.run_tap2sna(f'{option} {odir} --ram load=1,16384 {tapfile} {z80_fname}')
            self.assertEqual(len(error), 0)
            self.assertEqual(s_fname, os.path.join(odir, z80_fname))

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_option_I(self):
        self.run_tap2sna('-I TraceLine=Hello in.tap out.z80')
        options, z80, config = make_snapshot_args[1:4]
        self.assertEqual(['TraceLine=Hello'], options.params)
        self.assertEqual(config['TraceLine'], 'Hello')

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_option_I_overrides_config_read_from_file(self):
        ini = """
            [tap2sna]
            TraceLine=0x{pc:04x} {i}
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        self.run_tap2sna('--ini TraceLine=Goodbye in.tap out.z80')
        options, z80, config = make_snapshot_args[1:4]
        self.assertEqual(['TraceLine=Goodbye'], options.params)
        self.assertEqual(config['TraceLine'], 'Goodbye')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_press(self):
        basic_data = (
            0, 10,                # Line 10
            10, 0,                # Line length
            249, 192, 46,         # RANDOMIZE USR .
            14, 0, 0, 222, 92, 0, # 23774
            13,                   # ENTER
            0, 20,                # Line 20
            21, 0,                # Line length
            234,                  # REM
            62, 191,              # 23774 LD A,191
            219, 254,             # 23776 IN A,(254)  ; Read H-J-K-L-ENTER
            31,                   # 23778 RRA         ; Was ENTER pressed?
            56, 249,              # 23779 JR C,23774  ; Jump back if not
            221, 33, 0, 192,      # 23781 LD IX,49152
            17, 1, 0,             # 23785 LD DE,1
            55,                   # 23788 SCF
            159,                  # 23789 SBC A,A
            195, 86, 5,           # 23790 JP 1366
            13,                   # ENTER
        )
        data = [123]
        tzxfile = self._write_tzx((
            create_tzx_header_block('simldpress', 10, len(basic_data), 0),
            create_tzx_data_block(basic_data),
            create_tzx_data_block(data)
        ))
        exp_output = [
            'Program: simldpress',
            'Fast loading data block: 23755,39',
            'Tape paused',
            'Pressing keys: ENTER',
            'Resuming LOAD',
            'Fast loading data block: 49152,1',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=1343',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--press 3:ENTER --start 1343 -c finish-tape=1 {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_press_timed_out(self):
        basic_data = (
            0, 10,                # Line 10
            10, 0,                # Line length
            249, 192, 46,         # RANDOMIZE USR .
            14, 0, 0, 222, 92, 0, # 23774
            13,                   # ENTER
            0, 20,                # Line 20
            29, 0,                # Line length
            234,                  # REM
            243,                  # 23774 DI
            14, 4,                # 23775 LD C,4      ; Wait long enough
            16, 254,              # 23777 DJNZ 23777  ; for the
            13,                   # 23779 DEC C       ; KeypressTracer
            32, 251,              # 23780 JR NZ,23777 ; to time out
            62, 191,              # 23782 LD A,191
            219, 254,             # 23784 IN A,(254)  ; Read H-J-K-L-ENTER
            31,                   # 23786 RRA         ; Was ENTER pressed?
            56, 249,              # 23787 JR C,23785  ; Jump back if not
            221, 33, 0, 192,      # 23789 LD IX,49152
            17, 1, 0,             # 23793 LD DE,1
            55,                   # 23796 SCF
            159,                  # 23797 SBC A,A
            195, 86, 5,           # 23798 JP 1366
            13,                   # ENTER
        )
        tzxfile = self._write_tzx((
            create_tzx_header_block('simldpress', 10, len(basic_data), 0),
            create_tzx_data_block(basic_data),
            create_tzx_data_block([0])
        ))
        exp_output = [
            'Program: simldpress',
            'Fast loading data block: 23755,47',
            'Tape paused',
            'Pressing keys: ENTER',
            'Simulation stopped (timed out): PC=23777',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--press 3:ENTER -c timeout=8 -c first-edge=2220000 {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_options_p_stack(self):
        for option, stack in (('-p', 24576), ('--stack', 49152)):
            output, error = self.run_tap2sna(f'{option} {stack} in.tap out.z80')
            self.assertEqual(output, '')
            self.assertEqual(error, '')
            options = make_snapshot_args[1]
            self.assertEqual([f'sp={stack}'], options.reg)

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_options_p_stack_with_hex_address(self):
        for option, stack in (('-p', '0x6ff0'), ('--stack', '0x9ABC')):
            output, error = self.run_tap2sna(f'{option} {stack} in.tap out.z80')
            self.assertEqual(output, '')
            self.assertEqual(error, '')
            options = make_snapshot_args[1]
            self.assertEqual(['sp={}'.format(int(stack[2:], 16))], options.reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_p(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        stack = 32768
        output, error = self.run_tap2sna(f'-p {stack} --ram load=1,16384 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIn(f'sp={stack}', s_reg)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    def test_option_screen(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'--screen {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIsNotNone(load_tracer.draw)
        self.assertIsNotNone(screen)
        self.assertEqual(screen.scale, 2)
        self.assertEqual(screen.fps, 50)
        self.assertEqual(screen.caption, 'tap2sna.py')
        self.assertFalse(screen.is128k)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen_close)
    def test_option_screen_128k(self):
        code_start = 32768
        code = [1]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--screen -c machine=128 {tapfile} out.z80')
        self.assertEqual(error, '')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Simulation stopped (screen closed)',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertTrue(screen.is128k)

    def test_option_show_config(self):
        output, error = self.run_tap2sna('--show-config', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = r"""
            [tap2sna]
            DefaultSnapshotFormat=z80
            ScreenFps=50
            ScreenScale=2
            TraceLine=${pc:04X} {i}
            TraceOperand=$,02X,04X
            UserAgent=
        """
        self.assertEqual(dedent(exp_output).strip(), output.rstrip())

    def test_option_show_config_read_from_file(self):
        ini = """
            [tap2sna]
            TraceLine={pc:05} {i}
            UserAgent=tap2sna.py/9.5
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        output, error = self.run_tap2sna('--show-config', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            [tap2sna]
            DefaultSnapshotFormat=z80
            ScreenFps=50
            ScreenScale=2
            TraceLine={pc:05} {i}
            TraceOperand=$,02X,04X
            UserAgent=tap2sna.py/9.5
        """
        self.assertEqual(dedent(exp_output).strip(), output.rstrip())

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_options_s_start(self):
        start = 30000
        exp_reg = [f'pc={start}']
        for option in ('-s', '--start'):
            output, error = self.run_tap2sna(f'{option} {start} --ram load=1,32768 in.tap out.z80')
            self.assertEqual(output, '')
            self.assertEqual(error, '')
            options = make_snapshot_args[1]
            self.assertEqual(exp_reg, options.reg)

    @patch.object(tap2sna, 'make_snapshot', mock_make_snapshot)
    def test_options_s_start_with_hex_address(self):
        start = 30000
        exp_reg = [f'pc={start}']
        for option in ('-s', '--start'):
            output, error = self.run_tap2sna(f'{option} 0x{start:04X} --ram load=1,32768 in.tap out.z80')
            self.assertEqual(output, '')
            self.assertEqual(error, '')
            options = make_snapshot_args[1]
            self.assertEqual(exp_reg, options.reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_s(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        start = 40000
        output, error = self.run_tap2sna(f'-s {start} --ram load=1,16384 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIn(f'pc={start}', s_reg)

    def test_option_tape_analysis_with_no_tape(self):
        output, error = self.run_tap2sna('--tape-analysis', catch_exit=2)
        self.assertEqual(output, '')
        self.assertTrue(error.startswith('usage:'))

    def test_option_tape_analysis(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (8063 x 2168 T-states)
              17480584    1  Pulse (667 T-states)
              17481251    0  Pulse (735 T-states)
              17481986    1  Data (19 bytes; 855,855/1710,1710 T-states)
              17765846    1  Pause (3500000 T-states)
              21265846    1  Tone (3223 x 2168 T-states)
              28253310    0  Pulse (667 T-states)
              28253977    1  Pulse (735 T-states)
              28254712    0  Data (5 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_tape_start(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis --tape-start 2 {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (3223 x 2168 T-states)
               6987464    1  Pulse (667 T-states)
               6988131    0  Pulse (735 T-states)
               6988866    1  Data (5 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_tape_stop(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis --tape-stop 2 {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (8063 x 2168 T-states)
              17480584    1  Pulse (667 T-states)
              17481251    0  Pulse (735 T-states)
              17481986    1  Data (19 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_tape_skip(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([1, 2]),
            create_tap_data_block([3, 4, 5]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis --tape-skip 2 {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (8063 x 2168 T-states)
              17480584    1  Pulse (667 T-states)
              17481251    0  Pulse (735 T-states)
              17481986    1  Data (19 bytes; 855,855/1710,1710 T-states)
              17765846    1  Pause (3500000 T-states)
              21265846    1  Tone (3223 x 2168 T-states)
              28253310    0  Pulse (667 T-states)
              28253977    1  Pulse (735 T-states)
              28254712    0  Data (5 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_first_edge(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis -c first-edge=1 {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     1    0  Tone (8063 x 2168 T-states)
              17480585    1  Pulse (667 T-states)
              17481252    0  Pulse (735 T-states)
              17481987    1  Data (19 bytes; 855,855/1710,1710 T-states)
              17765847    1  Pause (3500000 T-states)
              21265847    1  Tone (3223 x 2168 T-states)
              28253311    0  Pulse (667 T-states)
              28253978    1  Pulse (735 T-states)
              28254713    0  Data (5 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_polarity(self):
        blocks = [
            create_tap_header_block(start=0),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis -c polarity=1 {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    1  Tone (8063 x 2168 T-states)
              17480584    0  Pulse (667 T-states)
              17481251    1  Pulse (735 T-states)
              17481986    0  Data (19 bytes; 855,855/1710,1710 T-states)
              17765846    0  Pause (3500000 T-states)
              21265846    0  Tone (3223 x 2168 T-states)
              28253310    1  Pulse (667 T-states)
              28253977    0  Pulse (735 T-states)
              28254712    1  Data (5 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_unused_bits_in_last_byte(self):
        code = [1, 2, 3, 4]
        blocks = [create_tzx_turbo_data_block(code, used_bits=4)]
        tapfile = self._write_tzx(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (3223 x 2168 T-states)
               6987464    1  Pulse (667 T-states)
               6988131    0  Pulse (735 T-states)
               6988866    1  Data (5 bytes + 4 bits; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_with_pure_tone_and_pause_and_pulse_sequence_and_pure_data(self):
        blocks = []
        blocks.append((18, 76, 4, 208, 7)) # 0x12 Pure Tone
        blocks.append((32, 1, 0))          # 0x20 Pause
        blocks.append((19, 2, 0, 1, 0, 2)) # 0x13 Pulse sequence
        blocks.append(create_tzx_pure_data_block((1, 2, 3, 4), 500, 1000))
        tapfile = self._write_tzx(blocks)
        output, error = self.run_tap2sna(f'--tape-analysis {tapfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (2000 x 1100 T-states)
               2200000    0  Pause (3500 T-states)
               2203500    0  Pulse (256 T-states)
               2203756    1  Pulse (512 T-states)
               2204268    0  Data (4 bytes; 500,500/1000,1000 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_pzx_file(self):
        pzx = PZX()
        pzx.add_puls(2)
        pzx.add_data((1, 2, 3, 4, 5))
        pzx.add_paus()
        pzx.add_puls(1)
        pzx.add_data((6, 7, 8, 9, 10))
        pzx.add_paus()
        pzx.add_puls(pulse_counts=((5000, 2168), (1, 430), (1, 870)))
        pzx.add_data((11, 12, 13, 14, 15), (570, 570), (1050, 1050), tail=0, used_bits=5, polarity=0)
        pzx.add_puls(pulse_counts=((2000, 1234), (2, 500), (1, 1000)))
        pzx.add_data((255, 0, 255, 0, 255), (80, 0), (0, 80), tail=0)
        pzx.add_puls(pulses=(0, 1000, 1000, 1000))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-analysis {pzxfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (8063 x 2168 T-states)
              17480584    1  Pulse (667 T-states)
              17481251    0  Pulse (735 T-states)
              17481986    1  Data (5 bytes; 855,855/1710,1710 T-states)
              17562356    1  Tail pulse (945 T-states)
              17563301    0  Pause (3500000 T-states)
              21063301    0  Tone (3223 x 2168 T-states)
              28050765    1  Pulse (667 T-states)
              28051432    0  Pulse (735 T-states)
              28052167    1  Data (5 bytes; 855,855/1710,1710 T-states)
              28137667    1  Tail pulse (945 T-states)
              28138612    0  Pause (3500000 T-states)
              31638612    0  Tone (5000 x 2168 T-states)
              42478612    0  Pulse (430 T-states)
              42479042    1  Pulse (870 T-states)
              42479912    0  Data (4 bytes + 5 bits; 570,570/1050,1050 T-states)
              42533612    0  Tone (2000 x 1234 T-states)
              45001612    0  Tone (2 x 500 T-states)
              45002612    0  Pulse (1000 T-states)
              45003612    1  Data (5 bytes; 80,0/0,80 T-states)
              45006812    1  Tone (3 x 1000 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_pzx_file_with_polarity_adjustments(self):
        pzx = PZX()
        pzx.add_puls(2)
        pzx.add_data((1, 2, 3, 4, 5), polarity=0)
        pzx.add_paus()
        pzx.add_puls(1)
        pzx.add_data((6, 7, 8, 9, 10), tail=0)
        pzx.add_paus(polarity=1)
        pzx.add_puls(1)
        pzx.add_data((11, 12, 13))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-analysis {pzxfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    0  Tone (8063 x 2168 T-states)
              17480584    1  Pulse (667 T-states)
              17481251    0  Pulse (735 T-states)
              17481986    1  Polarity adjustment (1->0)
              17481986    0  Data (5 bytes; 855,855/1710,1710 T-states)
              17562356    0  Tail pulse (945 T-states)
              17563301    1  Polarity adjustment (1->0)
              17563301    0  Pause (3500000 T-states)
              21063301    0  Tone (3223 x 2168 T-states)
              28050765    1  Pulse (667 T-states)
              28051432    0  Pulse (735 T-states)
              28052167    1  Data (5 bytes; 855,855/1710,1710 T-states)
              28137667    1  Pause (3500000 T-states)
              31637667    1  Polarity adjustment (1->0)
              31637667    0  Tone (3223 x 2168 T-states)
              38625131    1  Pulse (667 T-states)
              38625798    0  Pulse (735 T-states)
              38626533    1  Data (3 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    def test_option_tape_analysis_pzx_file_with_polarity_inverted(self):
        pzx = PZX()
        pzx.add_puls(2)
        pzx.add_data((1, 2, 3, 4, 5), polarity=0)
        pzx.add_paus()
        pzx.add_puls(1)
        pzx.add_data((6, 7, 8, 9, 10), tail=0)
        pzx.add_paus(polarity=1)
        pzx.add_puls(1)
        pzx.add_data((11, 12, 13))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-analysis -c polarity=1 {pzxfile}', catch_exit=0)
        self.assertEqual(error, '')
        exp_output = """
            T-states    EAR  Description
                     0    1  Tone (8063 x 2168 T-states)
              17480584    0  Pulse (667 T-states)
              17481251    1  Pulse (735 T-states)
              17481986    0  Polarity adjustment (0->1)
              17481986    1  Data (5 bytes; 855,855/1710,1710 T-states)
              17562356    1  Tail pulse (945 T-states)
              17563301    0  Polarity adjustment (0->1)
              17563301    1  Pause (3500000 T-states)
              21063301    1  Tone (3223 x 2168 T-states)
              28050765    0  Pulse (667 T-states)
              28051432    1  Pulse (735 T-states)
              28052167    0  Data (5 bytes; 855,855/1710,1710 T-states)
              28137667    0  Pause (3500000 T-states)
              31637667    0  Polarity adjustment (0->1)
              31637667    1  Tone (3223 x 2168 T-states)
              38625131    0  Pulse (667 T-states)
              38625798    1  Pulse (735 T-states)
              38626533    0  Data (3 bytes; 855,855/1710,1710 T-states)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_tape_name(self):
        code1 = [1, 2, 3, 4, 5]
        code1_start = 24576
        tap1_data = create_tap_data_block(code1)
        tap1_fname = 'code1.tap'
        code2 = [6, 7, 8]
        code2_start = 32768
        tap2_data = create_tap_data_block(code2)
        tap2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(tap1_fname, bytearray(tap1_data))
            archive.writestr(tap2_fname, bytearray(tap2_data))
        output, error = self.run_tap2sna(f'--tape-name {tap2_fname} --ram load=1,{code2_start} {zipfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(code2, snapshot[code2_start:code2_start + len(code2)])

    def test_option_tape_name_with_invalid_name(self):
        tap_data = create_tap_header_block(start=32768) + create_tap_data_block([1, 2, 3])
        zipfile = 'tape.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr('data.tap', bytearray(tap_data))
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--tape-name code.tap {zipfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting tape.zip: No file named "code.tap" in the archive')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_name_twice(self):
        tap1_data = create_tap_data_block([1, 2, 3, 4, 5])
        tap1_fname = 'code1.tap'
        tap2_data = create_tap_data_block([6, 7, 8])
        tap2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(tap1_fname, bytearray(tap1_data))
            archive.writestr(tap2_fname, bytearray(tap2_data))
        output, error = self.run_tap2sna(f'--tape-name {tap1_fname} --tape-name {tap2_fname} {zipfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(len(load_tracer.blocks), 2)
        self.assertEqual(tap1_data[2:], list(load_tracer.blocks[0].data))
        self.assertEqual(tap2_data[2:], list(load_tracer.blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_tape_name_twice_with_no_snapshot_name(self):
        tap1_data = create_tap_data_block([1, 2, 3, 4, 5])
        tap1_fname = 'code1.tap'
        tap2_data = create_tap_data_block([6, 7, 8])
        tap2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(tap1_fname, bytearray(tap1_data))
            archive.writestr(tap2_fname, bytearray(tap2_data))
        output, error = self.run_tap2sna(f'--tape-name {tap1_fname} --tape-name {tap2_fname} {zipfile}')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, zipfile + '.z80')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_skip(self):
        blocks = (
            create_tap_data_block([1, 2, 3]),
            create_tap_data_block([4, 5, 6]),
            create_tap_data_block([7, 8, 9])
        )
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-skip 2 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(len(load_tracer.blocks), 2)
        self.assertEqual(blocks[0][2:], list(load_tracer.blocks[0].data))
        self.assertEqual(blocks[2][2:], list(load_tracer.blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_skip_with_range(self):
        blocks = (
            create_tap_data_block([1, 2]),
            create_tap_data_block([3, 4]),
            create_tap_data_block([5, 6]),
            create_tap_data_block([7, 8])
        )
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--tape-skip 2-3 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(len(load_tracer.blocks), 2)
        self.assertEqual(blocks[0][2:], list(load_tracer.blocks[0].data))
        self.assertEqual(blocks[3][2:], list(load_tracer.blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_skip_with_pzx(self):
        data_blocks = [create_data_block([i]) for i in range(4)]
        pzx = PZX()
        for b in data_blocks:
            pzx.add_puls()
            pzx.add_data(b)
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-skip 4-7 {pzxfile} out.z80')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 4)
        self.assertIsNone(blocks[0].data)
        self.assertEqual(data_blocks[0], list(blocks[1].data))
        self.assertIsNone(blocks[2].data)
        self.assertEqual(data_blocks[3], list(blocks[3].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_skip_with_tzx(self):
        data_blocks = [[i] for i in range(4)]
        tzxfile = self._write_tzx(create_tzx_data_block(b) for b in data_blocks)
        output, error = self.run_tap2sna(f'--tape-skip 2-3 {tzxfile} out.z80')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual([0], list(blocks[0].data[1:-1]))
        self.assertEqual([3], list(blocks[1].data[1:-1]))

    def test_option_tape_skip_invalid(self):
        self._test_bad_spec('--tape-skip ?', 'Invalid integer(s): --tape-skip ?')
        self._test_bad_spec('--tape-skip a-b', 'Invalid integer(s): --tape-skip a-b')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_start_with_pzx(self):
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block())
        pzx.add_puls()
        data = create_data_block([4, 5, 6])
        pzx.add_data(data)
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-start 4 {pzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(data, list(blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_start_with_tap(self):
        tap_blocks = [
            create_tap_header_block(),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(tap_blocks)
        output, error = self.run_tap2sna(f'--tape-start 2 {tapfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(tap_blocks[1][2:], list(blocks[0].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_start_with_tzx(self):
        tzx_blocks = [
            create_tzx_header_block(),
            create_tzx_data_block([4, 5, 6]),
        ]
        tzxfile = self._write_tzx(tzx_blocks)
        output, error = self.run_tap2sna(f'--tape-start 2 {tzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(tzx_blocks[1][5:], list(blocks[0].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_start_with_two_tapes(self):
        t1_data = [[1, 2, 3], [4, 5, 6]]
        t1_bytes = bytearray(create_tap_data_block(t1_data[0]) + create_tap_data_block(t1_data[1]))
        t1_fname = 'code1.tap'
        t2_data = [7, 8, 9]
        t2_bytes = bytearray(create_tap_data_block(t2_data))
        t2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(t1_fname, t1_bytes)
            archive.writestr(t2_fname, t2_bytes)
        args = (
            '--tape-name', t1_fname,
            '--tape-name', t2_fname,
            '--tape-start', '2',
            zipfile,
            'out.z80'
        )
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(t1_bytes[6 + len(t1_data[0]):], blocks[0].data)
        self.assertEqual(t2_bytes[2:], blocks[1].data)

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_stop_with_pzx(self):
        pzx = PZX()
        pzx.add_puls()
        data = create_header_block()
        pzx.add_data(data)
        pzx.add_puls()
        pzx.add_data(create_data_block([4, 5, 6]))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'--tape-stop 4 {pzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(data, list(blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_stop_with_tap(self):
        tap_blocks = [
            create_tap_header_block(),
            create_tap_data_block([4, 5, 6]),
        ]
        tapfile = self._write_tap(tap_blocks)
        output, error = self.run_tap2sna(f'--tape-stop 2 {tapfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(tap_blocks[0][2:], list(blocks[0].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_stop_with_tzx(self):
        tzx_blocks = [
            create_tzx_header_block(),
            create_tzx_data_block([4, 5, 6]),
        ]
        tzxfile = self._write_tzx(tzx_blocks)
        output, error = self.run_tap2sna(f'--tape-stop 2 {tzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(tzx_blocks[0][5:], list(blocks[0].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_option_tape_stop_with_two_tapes(self):
        t1_data = [1, 2, 3]
        t1_bytes = bytearray(create_tap_data_block(t1_data))
        t1_fname = 'code1.tap'
        t2_data = [[4, 5, 6], [7, 8, 9]]
        t2_bytes = bytearray(create_tap_data_block(t2_data[0]) + create_tap_data_block(t2_data[1]))
        t2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(t1_fname, t1_bytes)
            archive.writestr(t2_fname, t2_bytes)
        args = (
            '--tape-name', t1_fname,
            '--tape-name', t2_fname,
            '--tape-stop', '3',
            zipfile,
            'out.z80'
        )
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(t1_bytes[2:], blocks[0].data)
        self.assertEqual(t2_bytes[2:2 + len(t2_data[0]) + 2], blocks[1].data)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_tape_sum(self):
        code = [1, 2, 3]
        code_start = 49152
        tap_data = create_tap_data_block(code)
        md5sum = hashlib.md5(bytearray(tap_data)).hexdigest()
        zipfile = 'tape.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr('data.tap', bytearray(tap_data))
        output, error = self.run_tap2sna(f'--tape-sum {md5sum} --ram load=1,{code_start} {zipfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_option_tape_sum_twice(self):
        t1_start = 24576
        t1_data = [1, 2, 3]
        t1_bytes = bytearray(create_tap_data_block(t1_data))
        t1_md5 = hashlib.md5(t1_bytes).hexdigest()
        t1_fname = 'code1.tap'
        t2_start = 49152
        t2_data = [4, 5, 6]
        t2_bytes = bytearray(create_tap_data_block(t2_data))
        t2_md5 = hashlib.md5(t2_bytes).hexdigest()
        t2_fname = 'code2.tap'
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(t1_fname, t1_bytes)
            archive.writestr(t2_fname, t2_bytes)
        args = (
            '--tape-name', t1_fname,
            '--tape-name', t2_fname,
            '--tape-sum', t1_md5,
            '--tape-sum', t2_md5,
            '--ram', f'load=1,{t1_start}',
            '--ram', f'load=2,{t2_start}',
            zipfile,
            'out.z80'
        )
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertEqual(t1_data, snapshot[t1_start:t1_start + len(t1_data)])
        self.assertEqual(t2_data, snapshot[t2_start:t2_start + len(t2_data)])

    def test_option_tape_sum_with_incorrect_value(self):
        code = [1, 2, 3]
        code_start = 49152
        tap_data = create_tap_header_block(start=code_start) + create_tap_data_block(code)
        md5sum = hashlib.md5(bytearray(tap_data)).hexdigest()
        zipfile = 'tape.zip'
        wrongsum = '0' * 32
        tapname = 'data.tap'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(tapname, bytearray(tap_data))
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--tape-sum {wrongsum} {zipfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting tape.zip: Checksum mismatch ({tapname}): Expected {wrongsum}, actually {md5sum}')
        self.assertEqual(self.err.getvalue(), '')

    def test_option_tape_sum_twice_with_one_incorrect_value(self):
        t1_data = bytearray(create_tap_data_block([1, 2, 3]))
        t1_md5 = hashlib.md5(t1_data).hexdigest()
        t1_f = 'code1.tap'
        t2_data = bytearray(create_tap_data_block([4, 5, 6]))
        t2_md5 = hashlib.md5(t2_data).hexdigest()
        t2_f = 'code2.tap'
        wrongsum = '0' * 32
        zipfile = 'tapes.zip'
        with ZipFile(zipfile, 'w') as archive:
            archive.writestr(t1_f, t1_data)
            archive.writestr(t2_f, t2_data)
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--tape-name {t1_f} --tape-name {t2_f} --tape-sum {t1_md5} --tape-sum {wrongsum} {zipfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {zipfile}: Checksum mismatch ({t2_f}): Expected {wrongsum}, actually {t2_md5}')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    @patch.object(tap2sna, 'urlopen')
    def test_option_u(self, mock_urlopen):
        tap_data = bytes(create_tap_data_block([1]))
        url = 'http://example.com/test.tap'
        for option, user_agent in (('-u', 'Wget/1.18'), ('--user-agent', 'SkoolKit/6.3')):
            mock_urlopen.return_value = BytesIO(tap_data)
            output, error = self.run_tap2sna(f'{option} {user_agent} --ram load=1,23296 {url} test.z80')
            self.assertTrue(output.startswith(f'Downloading {url}\n'))
            self.assertEqual(error, '')
            request = mock_urlopen.call_args[0][0]
            self.assertEqual({'User-agent': user_agent}, request.headers)
            self.assertEqual(snapshot[23296], 1)

    def test_option_V(self):
        for option in ('-V', '--version'):
            output, error = self.run_tap2sna(option, catch_exit=0)
            self.assertEqual(output, f'SkoolKit {VERSION}\n')

    def test_nonexistent_tap_file(self):
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'non-existent.tap test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting non-existent.tap: non-existent.tap: file not found')

    def test_nonexistent_tap_file_two_of_two(self):
        tapfile1 = self._write_tap([create_tap_data_block([1])])
        tapfile2 = 'non-existent.tap'
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--ram load=1,16384 {tapfile1} {tapfile2} test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile1} and {tapfile2}: non-existent.tap: file not found')

    def test_load_nonexistent_block(self):
        tapfile = self._write_tap([create_tap_data_block([1])])
        block_num = 2
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'--ram load={block_num},16384 {tapfile} test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile}: Block {block_num} not found')

    def test_zip_archive_with_no_tape_file(self):
        archive_fname = self.write_bin_file(suffix='.zip')
        with ZipFile(archive_fname, 'w') as archive:
            archive.writestr('game.txt', bytearray((1, 2)))
        z80_fname = 'test.z80'
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{archive_fname} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {archive_fname}: No PZX, TAP or TZX file found')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_call(self):
        ram_module = """
            def fix(snapshot):
                snapshot[65280:] = list(range(256))
        """
        module_dir = self.make_directory()
        module_path = os.path.join(module_dir, 'ram.py')
        module = self.write_text_file(dedent(ram_module).strip(), path=module_path)
        blocks = [create_tap_data_block([0])]
        load_options = f'--ram load=1,16384 --ram call={module_dir}:ram.fix'
        self._load_tape(load_options=load_options, blocks=blocks)
        self.assertEqual(list(range(256)), snapshot[65280:])

    def test_ram_call_nonexistent_module(self):
        self._test_bad_spec('--ram call=no:nope.never', "Failed to import object nope.never: No module named 'nope'")

    def test_ram_call_nonexistent_function(self):
        module_dir = self.make_directory()
        module_path = os.path.join(module_dir, 'ram.py')
        module = self.write_text_file(path=module_path)
        self._test_bad_spec(f'--ram call={module_dir}:ram.never', "No object named 'never' in module 'ram'")

    def test_ram_call_uncallable(self):
        ram_module = "fix = None"
        module_dir = self.make_directory()
        module_path = os.path.join(module_dir, 'uncallable.py')
        module = self.write_text_file(ram_module, path=module_path)
        self._test_bad_spec(f'--ram call={module_dir}:uncallable.fix', "'NoneType' object is not callable")

    def test_ram_call_function_with_no_arguments(self):
        ram_module = "def fix(): pass"
        module_dir = self.make_directory()
        module_path = os.path.join(module_dir, 'noargs.py')
        module = self.write_text_file(ram_module, path=module_path)
        self._test_bad_spec(f'--ram call={module_dir}:noargs.fix', "fix() takes 0 positional arguments but 1 was given")

    def test_ram_call_function_with_two_positional_arguments(self):
        ram_module = "def fix(snapshot, what): pass"
        module_dir = self.make_directory()
        module_path = os.path.join(module_dir, 'twoargs.py')
        module = self.write_text_file(ram_module, path=module_path)
        self._test_bad_spec(f'--ram call={module_dir}:twoargs.fix', "fix() missing 1 required positional argument: 'what'")

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load(self):
        start = 16384
        data = [237, 1, 1, 1, 1, 1]
        self._load_tape(start, data)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_pzx(self):
        start = 32768
        data = [1, 4, 9, 16, 25]
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_data_block(data))
        self._load_tape(start, load_options=f'--ram load=3,{start}', pzx=pzx)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_with_length(self):
        start = 16384
        data = [1, 2, 3, 4]
        length = 2
        self._load_tape(start, data, load_options=f'--ram load=1,{start},{length}')
        self.assertEqual(data[:length], snapshot[start:start + length])
        self.assertEqual([0] * (len(data) - length), snapshot[start + length:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_with_step(self):
        start = 16385
        data = [5, 4, 3]
        step = 2
        self._load_tape(start, data, load_options=f'--ram load=1,{start},,{step}')
        self.assertEqual(data, snapshot[start:start + len(data) * step:step])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_with_offset(self):
        start = 16384
        data = [15, 16, 17]
        offset = 5
        self._load_tape(start, data, load_options=f'--ram load=1,{start},,,{offset}')
        self.assertEqual(data, snapshot[start + offset:start + offset + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_with_increment(self):
        start = 65534
        data = [8, 9, 10]
        inc = 65533
        self._load_tape(start, data, load_options=f'--ram load=1,{start},,,,{inc}')
        self.assertEqual([data[2]] + data[:2], snapshot[65533:])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_wraparound_with_step(self):
        start = 65535
        data = [23, 24, 25]
        step = 8193
        self._load_tape(start, data, load_options=f'--ram load=1,{start},,{step}')
        self.assertEqual(snapshot[start], data[0])
        self.assertEqual(snapshot[(start + 2 * step) & 65535], data[2])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_with_hexadecimal_parameters(self):
        start = 30000
        data = [1, 2, 3]
        step = 2
        offset = 3
        inc = 0
        self._load_tape(start, data, load_options=f'--ram load=1,0x{start:04x},0x{len(data):04x},0x{step:04x},0x{offset:04x},0x{inc:04x}')
        self.assertEqual(data, snapshot[30003:30008:2])

    def test_ram_load_bad_address(self):
        self._test_bad_spec('--ram load=1,abcde', 'Invalid integer in load spec: 1,abcde')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_patch(self):
        start = 16384
        data = [4, 5, 6]
        patch = [21, 22]
        patchfile = self.write_bin_file(patch)
        exp_data = patch + data[len(patch):]
        self._load_tape(start, data, f'--ram patch={start},{patchfile}')
        self.assertEqual(exp_data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_patch_with_page_number(self):
        data = [0]
        basic_data = [
            0, 10,  # Line 10
            2, 0,   # Line length
            234,    # REM
            13      # ENTER
        ]
        tapfile = self._write_tap((
            create_tap_header_block('basicprog', 10, len(basic_data), 0),
            create_tap_data_block(basic_data)
        ))
        patches = (
            (0, 0x0000, [255, 7]),
            (1, 0x4000, [254, 33]),
            (2, 0x8000, [253, 14]),
            (3, 0xC000, [252, 8]),
            (4, 0x3FFF, [251]),
            (5, 0x7FFE, [250, 44]),
            (6, 0xBFFF, [249]),
            (7, 0xFFFF, [248]),
        )
        patch_opts= ' '.join(f'--ram patch={p}:{a},{self.write_bin_file(d)}' for p, a, d in patches)
        output, error = self.run_tap2sna(f'-c machine=128 {patch_opts} {tapfile} out.z80')
        self.assertTrue(output.endswith(f'\nWriting out.z80\n'))
        self.assertEqual(error, '')
        for p, a, d in patches:
            addr = a % 0x4000
            self.assertEqual(d, list(s_banks[p][addr:addr + len(d)]))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_single_address(self):
        start = 16384
        data = [4, 5, 6]
        poke_addr = 16386
        poke_val = 255
        self._load_tape(start, data, f'--ram poke={poke_addr},{poke_val}')
        self.assertEqual(data[:2], snapshot[start:start + 2])
        self.assertEqual(snapshot[poke_addr], poke_val)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_address_range(self):
        start = 16384
        data = [1, 1, 1]
        poke_addr_start = 16384
        poke_addr_end = 16383 + len(data)
        poke_val = 254
        self._load_tape(start, data, f'--ram poke={poke_addr_start}-{poke_addr_end},{poke_val}')
        self.assertEqual([poke_val] * len(data), snapshot[poke_addr_start:poke_addr_end + 1])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_address_range_with_xor(self):
        start = 30000
        data = [1, 2, 3]
        end = start + len(data) - 1
        xor_val = 129
        self._load_tape(start, data, f'--ram poke={start}-{end},^{xor_val}')
        exp_data = [b ^ xor_val for b in data]
        self.assertEqual(exp_data, snapshot[start:end + 1])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_address_range_with_add(self):
        start = 40000
        data = [100, 200, 32]
        end = start + len(data) - 1
        add_val = 130
        self._load_tape(start, data, f'--ram poke={start}-{end},+{add_val}')
        exp_data = [(b + add_val) & 255 for b in data]
        self.assertEqual(exp_data, snapshot[start:end + 1])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_address_range_with_step(self):
        self._load_tape(16384, [2, 9, 2], '--ram poke=16384-16386-2,253')
        self.assertEqual([253, 9, 253], snapshot[16384:16387])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_hex_address(self):
        address, value = 16385, 253
        self._load_tape(16384, [1], f'--ram poke=${address:X},{value}')
        self.assertEqual(snapshot[address], value)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_0x_hex_values(self):
        self._load_tape(16384, [2, 1, 2], '--ram poke=0x4000-0x4002-0x02,0x2a')
        self.assertEqual([42, 1, 42], snapshot[16384:16387])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_poke_with_page_number(self):
        data = [0]
        basic_data = [
            0, 10,  # Line 10
            2, 0,   # Line length
            234,    # REM
            13      # ENTER
        ]
        blocks = [
            create_tap_header_block('basicprog', 10, len(basic_data), 0),
            create_tap_data_block(basic_data)
        ]
        tapfile = self._write_tap(blocks)
        pokes = (
            (0, 0x0000, 255),
            (1, 0x4000, 254),
            (2, 0x8000, 253),
            (3, 0xC000, 252),
            (4, 0x3FFF, 251),
            (5, 0x7FFF, 250),
            (6, 0xBFFF, 249),
            (7, 0xFFFF, 248),
        )
        poke_opts = ' '.join(f'--ram poke={p}:{a},{v}' for p, a, v in pokes)
        output, error = self.run_tap2sna(f'-c machine=128 {poke_opts} {tapfile} out.z80')
        self.assertTrue(output.endswith(f'\nWriting out.z80\n'))
        self.assertEqual(error, '')
        for p, a, v in pokes:
            self.assertEqual(s_banks[p][a % 0x4000], v)

    def test_ram_poke_bad_value(self):
        self._test_bad_spec('--ram poke=1', 'Value missing in poke spec: 1')
        self._test_bad_spec('--ram poke=q', 'Value missing in poke spec: q')
        self._test_bad_spec('--ram poke=p:1,1', 'Invalid page number in poke spec: p:1,1')
        self._test_bad_spec('--ram poke=1,x', 'Invalid value in poke spec: 1,x')
        self._test_bad_spec('--ram poke=x,1', 'Invalid address range in poke spec: x,1')
        self._test_bad_spec('--ram poke=1-y,1', 'Invalid address range in poke spec: 1-y,1')
        self._test_bad_spec('--ram poke=1-3-z,1', 'Invalid address range in poke spec: 1-3-z,1')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_move(self):
        start = 16384
        data = [5, 6, 7]
        src = start
        size = len(data)
        dest = 16387
        self._load_tape(start, data, f'--ram move={src},{size},{dest}')
        self.assertEqual(data, snapshot[start:start + len(data)])
        self.assertEqual(data, snapshot[dest:dest + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_move_hex_address(self):
        src, size, dest = 16384, 1, 16385
        value = 3
        self._load_tape(src, [value], f'--ram move=${src:X},{size},${dest:x}')
        self.assertEqual(snapshot[dest], value)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_move_0x_hex_values(self):
        src, size, dest = 16385, 1, 16384
        value = 2
        self._load_tape(src, [value], f'--ram move=0x{src:X},0x{size:X},0x{dest:x}')
        self.assertEqual(snapshot[dest], value)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_move_with_page_number(self):
        data = [0]
        basic_data = [
            0, 10,  # Line 10
            2, 0,   # Line length
            234,    # REM
            13      # ENTER
        ]
        blocks = [
            create_tap_header_block('basicprog', 10, len(basic_data), 0),
            create_tap_data_block(basic_data)
        ]
        tapfile = self._write_tap(blocks)
        moves = (
            (5, 22528, 8, 1, 0),
            (5, 17440, 8, 3, 0),
            (5, 17440, 8, None, 8),
            (0, 16376, 8, 4, 49152),
        )
        move_opts = []
        for src_page, src, length, dest_page, dest in moves:
            if dest_page:
                move_opts.append(f'--ram move={src_page}:{src},{length},{dest_page}:{dest}')
            else:
                move_opts.append(f'--ram move={src_page}:{src},{length},{dest}')
        move_opts = ' '.join(move_opts)
        output, error = self.run_tap2sna(f'-c machine=128 {move_opts} {tapfile} out.z80')
        self.assertTrue(output.endswith(f'\nWriting out.z80\n'))
        self.assertEqual(error, '')
        for src_page, src, length, dest_page, dest in moves:
            s = src % 0x4000
            d = dest % 0x4000
            if dest_page is None:
                dest_page = src_page
            self.assertEqual(s_banks[src_page][s:s + length], s_banks[dest_page][d:d + length])

    def test_ram_move_bad_address(self):
        self._test_bad_spec('--ram move=1', 'Not enough arguments in move spec (expected 3): 1')
        self._test_bad_spec('--ram move=1,2', 'Not enough arguments in move spec (expected 3): 1,2')
        self._test_bad_spec('--ram move=x,2,3', 'Invalid integer in move spec: x,2,3')
        self._test_bad_spec('--ram move=1,y,3', 'Invalid integer in move spec: 1,y,3')
        self._test_bad_spec('--ram move=1,2,z', 'Invalid integer in move spec: 1,2,z')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_sysvars(self):
        self._load_tape(23552, [0], '--ram sysvars')
        self.assertEqual(sum(snapshot[23552:23755]), 7911)
        self.assertEqual(len(snapshot), 65536)

    def test_ram_invalid_operation(self):
        self._test_bad_spec('--ram foo=bar', 'Invalid operation: foo=bar')

    def test_ram_help(self):
        output, error = self.run_tap2sna('--ram help')
        self.assertTrue(output.startswith('Usage: --ram call=[/path/to/moduledir:]module.function\n'))
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tap_file_in_zip_archive(self):
        data = [1]
        block = create_tap_data_block(data)
        tap_name = 'game.tap'
        zip_fname = self._write_tap([block], zip_archive=True, tap_name=tap_name)
        start = 16385
        output, error = self.run_tap2sna(f'--ram load=1,{start} {zip_fname} out.z80')
        self.assertEqual(output, f'Extracting {tap_name}\nWriting out.z80\n')
        self.assertEqual(error, '')
        self.assertEqual(data, snapshot[start:start + len(data)])

    def test_invalid_tzx_file(self):
        tzxfile = self.write_bin_file([1, 2, 3], suffix='.tzx')
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: Not a TZX file')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_stops_at_pzx_48k_stop_block_in_48k_mode(self):
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block())
        pzx.add_puls()
        pzx.add_data(create_data_block([1, 2, 3]))
        pzx.add_stop(False)
        pzx.add_puls()
        pzx.add_data(create_data_block([4, 5, 6]))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(pzxfile)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 4)
        self.assertEqual([1, 2, 3], list(blocks[3].data[1:-1]))

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_does_not_stop_at_pzx_48k_stop_block_in_128k_mode(self):
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block())
        pzx.add_puls()
        pzx.add_data(create_data_block([1, 2, 3]))
        pzx.add_stop(False)
        pzx.add_puls()
        pzx.add_data(create_data_block([4, 5, 6]))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'-c machine=128 {pzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 6)

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_stops_at_pzx_always_stop_block_in_48k_mode(self):
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block())
        pzx.add_puls()
        pzx.add_data(create_data_block([1, 2, 3]))
        pzx.add_stop(True)
        pzx.add_puls()
        pzx.add_data(create_data_block([4, 5, 6]))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(pzxfile)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 4)
        self.assertEqual([1, 2, 3], list(blocks[3].data[1:-1]))

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_stops_at_pzx_always_stop_block_in_128k_mode(self):
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block())
        pzx.add_puls()
        pzx.add_data(create_data_block([1, 2, 3]))
        pzx.add_stop(True)
        pzx.add_puls()
        pzx.add_data(create_data_block([4, 5, 6]))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        output, error = self.run_tap2sna(f'-c machine=128 {pzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 4)
        self.assertEqual([1, 2, 3], list(blocks[3].data[1:-1]))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_stops_at_pause_block_with_duration_0(self):
        tzx_blocks = [
            create_tzx_header_block(),
            create_tzx_data_block([1, 2, 3]),
            (0x20, 0, 0), # 0x20 Pause 0ms
            create_tzx_data_block([4, 5, 6]),
        ]
        output, error = self.run_tap2sna(self._write_tzx(tzx_blocks))
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(tzx_blocks[1][5:], list(blocks[1].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tzx_loop(self):
        tzx_blocks = [
            (0x24, 2, 0), # 0x24 Loop start
            create_tzx_header_block(),
            create_tzx_data_block([1, 2, 3]),
            (0x25,), # 0x25 Loop end
        ]
        output, error = self.run_tap2sna(self._write_tzx(tzx_blocks))
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 4)
        self.assertEqual(tzx_blocks[1][5:], list(blocks[0].data))
        self.assertEqual(tzx_blocks[2][5:], list(blocks[1].data))
        self.assertEqual(tzx_blocks[1][5:], list(blocks[2].data))
        self.assertEqual(tzx_blocks[2][5:], list(blocks[3].data))

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_stops_at_tzx_block_type_0x2A_in_48k_mode(self):
        tzx_blocks = [
            create_tzx_header_block(),
            create_tzx_data_block([1, 2, 3]),
            (0x2A, 0, 0, 0, 0), # 0x2A Stop the tape if in 48K mode
            create_tzx_data_block([4, 5, 6]),
        ]
        output, error = self.run_tap2sna(self._write_tzx(tzx_blocks))
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 2)
        self.assertEqual(tzx_blocks[1][5:], list(blocks[1].data))

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_tape_does_not_stop_at_tzx_block_type_0x2A_in_128k_mode(self):
        tzx_blocks = [
            create_tzx_header_block(),
            create_tzx_data_block([1, 2, 3]),
            (0x2A, 0, 0, 0, 0), # 0x2A Stop the tape if in 48K mode
            create_tzx_data_block([4, 5, 6]),
        ]
        tzxfile = self._write_tzx(tzx_blocks)
        output, error = self.run_tap2sna(f'-c machine=128 {tzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 3)
        self.assertEqual(tzx_blocks[3][5:], list(blocks[2].data))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tzx_block_type_0x10(self):
        data = [1, 2, 4]
        start = 16386
        block = create_tzx_data_block(data)
        self._load_tape(start, blocks=[block], tzx=True)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tzx_block_type_0x11(self):
        data = [2, 3, 5]
        block = [17] # Block ID
        block.extend([0] * 15)
        data_block = create_data_block(data)
        length = len(data_block)
        block.extend((length % 256, length // 256, 0))
        block.extend(data_block)
        start = 16387
        self._load_tape(start, blocks=[block], tzx=True)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tzx_block_type_0x14(self):
        data = [7, 11, 13]
        block = [20] # Block ID
        block.extend([0] * 7)
        data_block = create_data_block(data)
        length = len(data_block)
        block.extend((length % 256, length // 256, 0))
        block.extend(data_block)
        start = 16388
        self._load_tape(start, blocks=[block], tzx=True)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_load_first_byte_of_block(self):
        data = [1, 2, 3, 4, 5]
        block = [20] # Block ID
        block.extend([0] * 7)
        length = len(data)
        block.extend((length % 256, length // 256, 0))
        block.extend(data)
        start = 16389
        load_options = f'--ram load=+1,{start}'
        self._load_tape(load_options=load_options, blocks=[block], tzx=True)
        exp_data = data[:-1]
        self.assertEqual(exp_data, snapshot[start:start + len(exp_data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_load_last_byte_of_block(self):
        data = [1, 2, 3, 4, 5]
        block = [20] # Block ID
        block.extend([0] * 7)
        length = len(data)
        block.extend((length % 256, length // 256, 0))
        block.extend(data)
        start = 16390
        load_options = f'--ram load=1+,{start}'
        self._load_tape(load_options=load_options, blocks=[block], tzx=True)
        exp_data = data[1:]
        self.assertEqual(exp_data, snapshot[start:start + len(exp_data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_load_first_and_last_bytes_of_block(self):
        data = [1, 2, 3, 4, 5]
        block = [20] # Block ID
        block.extend([0] * 7)
        length = len(data)
        block.extend((length % 256, length // 256, 0))
        block.extend(data)
        start = 16391
        load_options = f'--ram load=+1+,{start}'
        self._load_tape(load_options=load_options, blocks=[block], tzx=True)
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_ram_load_tzx_with_unsupported_blocks(self):
        blocks = []
        blocks.append((18, 0, 0, 0, 0)) # 0x12 Pure Tone
        blocks.append((19, 2, 0, 0, 0, 0)) # 0x13 Pulse sequence
        blocks.append([21] + [0] * 5 + [1, 0, 0, 0]) # 0x15 Direct Recording
        blocks.append([24, 11] + [0] * 14) # 0x18 CSW Recording
        blocks.append([25, 20] + [0] * 23) # 0x19 Generalized Data Block
        blocks.append((32, 0, 0)) # 0x20 Pause (silence) or 'Stop the Tape' command
        blocks.append((33, 1, 32)) # 0x21 Group start
        blocks.append((34,)) # 0x22 - Group end
        blocks.append((35, 0, 0)) # 0x23 Jump to block
        blocks.append((36, 2, 0)) # 0x24 Loop start
        blocks.append((37,)) # 0x25 Loop end
        blocks.append((38, 1, 0, 0, 0)) # 0x26 Call sequence
        blocks.append((39,)) # 0x27 Return from sequence
        blocks.append((40, 5, 0, 1, 0, 0, 1, 32)) # 0x28 Select block
        blocks.append((42, 0, 0, 0, 0)) # 0x2A Stop the tape if in 48K mode
        blocks.append((43, 1, 0, 0, 0, 1)) # 0x2B Set signal level
        blocks.append((48, 1, 65)) # 0x30 Text description
        blocks.append((49, 0, 1, 66)) # 0x31 Message block
        blocks.append((50, 4, 0, 1, 0, 1, 33)) # 0x32 Archive info
        blocks.append((51, 1, 0, 0, 0)) # 0x33 Hardware type
        blocks.append([53] + [32] * 16 + [1] + [0] * 4) # 0x35 Custom info block
        blocks.append([90] + [0] * 9) # 0x5A "Glue" block
        data = [2, 4, 6]
        blocks.append(create_tzx_data_block(data))
        start = 16388
        load_options = f'--ram load={len(blocks)},{start}'
        self._load_tape(load_options=load_options, blocks=blocks, tzx=True)
        self.assertEqual(data, snapshot[start:start + len(data)])

    def test_tzx_with_unknown_block(self):
        block_id = 0x29
        block = [block_id, 0]
        tzxfile = self._write_tzx([block])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} test.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: Unknown TZX block ID: 0x{block_id:X}')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_default_register_values(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual([], s_reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_reg(self):
        block = create_tap_data_block([1])
        tapfile = self._write_tap([block])
        reg_dicts = (
            {'^a': 1, '^b': 2, '^c': 3, '^d': 4, '^e': 5, '^f': 6, '^h': 7, '^l': 8},
            {'a': 9, 'b': 10, 'c': 11, 'd': 12, 'e': 13, 'f': 14, 'h': 15, 'l': 16, 'r': 129},
            {'^bc': 258, '^de': 515, '^hl': 65534, 'bc': 259, 'de': 516, 'hl': 65533},
            {'i': 13, 'ix': 1027, 'iy': 1284, 'pc': 1541, 'r': 23, 'sp': 32769, 'memptr': 1112}
        )
        for reg_dict in reg_dicts:
            exp_reg = [f'{r}={v}' for r, v in reg_dict.items()]
            reg_options = ' '.join([f'--reg {spec}' for spec in exp_reg])
            output, error = self.run_tap2sna(f'--ram load=1,16384 {reg_options} {tapfile} out.z80')
            self.assertEqual(error, '')
            self.assertEqual(exp_reg, s_reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_reg_hex_value(self):
        tapfile = self._write_tap([create_tap_data_block([1])])
        reg_value = 35487
        output, error = self.run_tap2sna(f'--ram load=1,16384 --reg bc=${reg_value:x} {tapfile} test.z80')
        self.assertEqual(error, '')
        self.assertIn(f'bc=${reg_value:x}', s_reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_reg_0x_hex_value(self):
        tapfile = self._write_tap([create_tap_data_block([1])])
        reg_value = 54873
        output, error = self.run_tap2sna(f'--ram load=1,16384 --reg hl=0x{reg_value:x} {tapfile} test.z80')
        self.assertEqual(error, '')
        self.assertIn(f'hl=0x{reg_value:x}', s_reg)

    def test_reg_bad_value(self):
        self._test_bad_spec('--reg bc=A2', 'Cannot parse register value: bc=A2')

    def test_ram_invalid_register(self):
        self._test_bad_spec('--reg iz=1', 'Invalid register: iz=1')

    def test_reg_help(self):
        output, error = self.run_tap2sna('--reg help')
        self.assertTrue(output.startswith('Usage: --reg name=value\n'))
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load(self):
        code_start = 32768
        code = [4, 5]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_initial_code_block(self):
        code_start = 65360 # Overwrite return address on stack with...
        code = [128, 128]  # ...32896
        blocks = [
            create_tap_header_block("\xafblock", code_start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Bytes: CODE block    ',
            'Fast loading data block: 65360,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32896',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65362', 'IX=65362', 'IY=23610', 'PC=32896'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_given_start_address(self):
        code_start = 32768
        start = 32769
        code = [175, 201]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--start {start} {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=32769',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32769'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_character_array(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            23, 0,            # Line length
            239, 34, 34, 228, # LOAD "" DATA
            97, 36, 40, 41,   # a$()
            58,               # :
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        ca_name = "characters"
        ca_data = [193, 5, 0, 1, 2, 0, 97, 98]
        code = [4, 5]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block(ca_name, length=len(ca_data), data_type=2),
            create_tap_data_block(ca_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,28',
            'Character array: characters',
            'Fast loading data block: 23786,8',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(ca_data, snapshot[23786:23786 + len(ca_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_number_array(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            23, 0,            # Line length
            239, 34, 34, 228, # LOAD "" DATA
            97, 40, 41,       # a()
            58,               # :
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        na_name = "numbers"
        na_data = [129, 13, 0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0]
        code = [4, 5]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block(na_name, length=len(na_data), data_type=1),
            create_tap_data_block(na_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,27',
            'Number array: numbers   ',
            'Fast loading data block: 23785,16',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(na_data, snapshot[23785:23785 + len(na_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_headerless_block(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            16, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        code = [
            221, 33, 0, 192,  # LD IX,49152
            17, 2, 0,         # LD DE,2
            55,               # SCF
            159,              # SBC A,A
            221, 229,         # PUSH IX
            195, 86, 5        # JP 1366
        ]
        code2 = [128, 129]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code),
            create_tap_data_block(code2)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,14',
            'Fast loading data block: 49152,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=49152',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        self.assertEqual(code2, snapshot[49152:49152 + len(code2)])
        exp_reg = set(('^F=187', 'SP=65344', 'IX=49154', 'IY=23610', 'PC=49152'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_overlong_blocks(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        code = [4, 5]
        basic_header = create_tap_header_block("simloadbas", 10, len(basic_data), 0)
        basic_header[0] += 1
        basic_header.append(1)
        basic_data_block = create_tap_data_block(basic_data)
        basic_data_block[0] += 1
        basic_data_block.append(2)
        code_header = create_tap_header_block("simloadbyt", code_start, len(code))
        code_header[0] += 1
        code_header.append(3)
        code_data_block = create_tap_data_block(code)
        code_data_block[0] += 1
        code_data_block.append(4)
        blocks = [
            basic_header,
            basic_data_block,
            code_header,
            code_data_block
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data + [128], snapshot[23755:23755 + len(basic_data) + 1])
        self.assertEqual(code + [0], snapshot[code_start:code_start + len(code) + 1])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_undersize_block(self):
        code2 = [201]
        code2_start = 49152
        code2_end = code2_start + len(code2)
        code = [
            221, 33, 0, 192,  # LD IX,49152
            221, 229,         # PUSH IX
            17, 5, 0,         # LD DE,5
            55,               # SCF
            159,              # SBC A,A
            195, 86, 5,       # JP 1366
        ]
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        code2_data_block = create_tap_data_block(code2)
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code),
            code2_data_block
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')

        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        self.assertEqual(code2, snapshot[code2_start:code2_end])
        self.assertEqual(snapshot[code2_end], code2_data_block[-1])
        exp_reg = {'^F=187', 'SP=65344', f'IX={code2_end+1}', 'DE=3', 'IY=23610', 'PC=49152', 'F=64'}
        self.assertLessEqual(exp_reg, set(s_reg))

        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,14',
            'Fast loading data block: 49152,5',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=49152',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_skips_blocks_with_wrong_flag_byte(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        code = [
            221, 33, 0, 0,    # 32768 LD IX,0
            17, 2, 0,         # 32772 LD DE,2
            55,               # 32775 SCF
            159,              # 32776 SBC A,A
            205, 86, 5,       # 32777 CALL 1366
            221, 33, 0, 192,  # 32780 LD IX,49152
            48, 245,          # 32784 JR NC,32775
        ]
        code2 = [128, 129]
        blocks = [
            create_tap_data_block(code), # Skipped
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code),
            create_tap_header_block("IGNORE ME", 49152, len(code2)), # Skipped
            create_tap_data_block(code2)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Data block (18 bytes) [skipped]',
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,18',
            'Bytes: IGNORE ME  [skipped]',
            'Fast loading data block: 49152,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32780',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        self.assertEqual(code2, snapshot[49152:49152 + len(code2)])
        exp_reg = set(('^F=187', 'SP=65344', 'IX=49154', 'IY=23610', 'PC=32780', 'F=1'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_ignores_extra_byte_at_end_of_tape(self):
        code_start = 32768
        code = [4, 5]
        blocks, basic_data = self._write_basic_loader(code_start, code, False)
        tapfile = self._write_tap(blocks + [[0]])
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_preserves_border_colour(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,             # Line 10
            24, 0,             # Line length
            239, 34, 34, 175,  # LOAD ""CODE
            58,                # :
            231, 51,           # BORDER 3
            14, 0, 0, 3, 0, 0, # 3 in floating point form
            58,                # :
            249, 192, 46,      # RANDOMIZE USR .
            14, *c_start_fp,   # start address
            13                 # ENTER
        ]
        code = [201]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,28',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,1',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertIn('border=3', s_state)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_preserves_interrupt_mode_and_flip_flop(self):
        code_start = 32768
        code = [
            243,              # 32768 DI
            237, 94,          # 32769 IM 2
            201,              # 32771 RET
        ]
        start = 32771
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--start {start} {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,4',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=32771',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertIn('im=2', s_state)
        self.assertIn('iff=0', s_state)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_fast_load_does_not_overwrite_rom(self):
        start_fp = [0, 0, 0, 64, 0] # 16384
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *start_fp,    # start address
            13                # ENTER
        ]
        start = 16380
        code = [
            0x01, 0x02, 0x03, 0x04, # 16380 DEFB 1,2,3,4
            0x21, 0xFC, 0x3F,       # 16384 LD HL,16380
            0x11, 0x00, 0x80,       # 16387 LD DE,32768
            0x01, 0x04, 0x00,       # 16390 LD BC,4
            0xED, 0xB0,             # 16393 LDIR
            0xC9,                   # 16395 RET
        ]
        blocks = [
            create_tap_header_block('simloadbas', 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block('simloadrom', start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--start 16395 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadrom',
            'Fast loading data block: 16380,16',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=16395',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual([161, 153, 66, 60], snapshot[32768:32772])
        self.assertEqual(code[4:], snapshot[start + 4:start + len(code)])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=16396', 'IY=23610', 'PC=16395'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_fast_load_checks_parity(self):
        code_start = 32768
        c_start_fp = (0, 0, code_start % 256, code_start // 256, 0)
        basic_data = [
            0, 10,            # Line 10
            15, 0,            # Line length
            239, 34, 34, 175, # LOAD ""CODE
            58,               # :
            249, 192, 46,     # RANDOMIZE USR .
            14, *c_start_fp,  # start address
            13                # ENTER
        ]
        code = [
            221, 33, 0, 192,  # 32768 LD IX,49152
            17, 2, 0,         # 32772 LD DE,2
            55,               # 32775 SCF
            159,              # 32776 SBC A,A
            205, 86, 5,       # 32777 CALL 1366
            48, 2,            # 32780 JR NC,32787 ; Jump if load failed as expected
            22, 128,          # 32782 LD D,128    ; Signal test failure
            201               # 32784 RET
        ]
        code2 = [128, 129]
        code2_block = create_tap_data_block(code2)
        code2_block[-1] ^= 255 # Break parity check
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
            create_tap_header_block("simloadbyt", code_start, len(code)),
            create_tap_data_block(code),
            code2_block
        ]
        tapfile = self._write_tap(blocks)
        output, error = self.run_tap2sna(f'--start 32784 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,17',
            'Fast loading data block: 49152,2',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=32784',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        self.assertEqual(code2, snapshot[49152:49152 + len(code2)])
        exp_reg = {'^F=187', 'DE=0', 'SP=65344', 'IX=49154', 'IY=23610', 'PC=32784'}
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_ram_call(self):
        ram_module = """
            def fix(snapshot):
                snapshot[32768:32772] = [1, 2, 3, 4]
        """
        module_dir = self.make_directory()
        module_name = 'simloadram'
        module_path = os.path.join(module_dir, f'{module_name}.py')
        module = self.write_text_file(dedent(ram_module).strip(), path=module_path)
        code_start = 32768
        code = [4, 5]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--ram call={module_dir}:{module_name}.fix {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual([1, 2, 3, 4], snapshot[32768:32772])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_ram_move(self):
        code_start = 32768
        code = [4, 5]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--ram move=32768,2,32770 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual([4, 5, 4, 5], snapshot[32768:32772])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_ram_patch(self):
        code_start = 32768
        code = [255, 128, 1]
        tapfile = self._write_tap((
            create_tap_header_block('simloadbyt', code_start, len(code)),
            create_tap_data_block(code)
        ))
        patch = [54, 33]
        patchfile = self.write_bin_file(patch)
        exp_data = patch + code[len(patch):]
        output, error = self.run_tap2sna(f'--ram patch={code_start},{patchfile} --start 1343 -c finish-tape=1 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,3',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=1343',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(exp_data, snapshot[code_start:code_start + len(exp_data)])
        exp_reg = set(('SP=65360', 'IX=32771', 'IY=23610', 'PC=1343'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_ram_poke(self):
        code_start = 32768
        code = [4, 5]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--ram poke=32768-32770-2,1 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual([1, 5, 1], snapshot[32768:32771])
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_ram_sysvars(self):
        code_start = 32768
        code = [4, 5]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--ram sysvars {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Fast loading data block: 32768,2',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        self.assertEqual([203, 92], snapshot[23627:23629]) # VARS=23755
        self.assertEqual([204, 92], snapshot[23641:23643]) # E-LINE=23756
        self.assertEqual([206, 92], snapshot[23649:23651]) # WORKSP=23758
        self.assertEqual([206, 92], snapshot[23651:23653]) # STKBOT=23758
        self.assertEqual([206, 92], snapshot[23653:23655]) # STKEND=23758
        exp_reg = set(('^F=129', 'SP=65344', 'IX=32770', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_initial_pause_block(self):
        pause_block = [
            32,    # Block ID
            1, 0,  # Pause (1ms)
        ]
        tzxfile = self._write_tzx([
            pause_block,
            create_tzx_header_block("simloadbas", 10, 1, 0),
        ])
        output, error = self.run_tap2sna(f'--start 1343 {tzxfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=1343',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_unexpected_end_of_tape(self):
        basic_data = [
            0, 10,       # Line 10
            4, 0,        # Line length
            239, 34, 34, # LOAD ""
            13           # ENTER
        ]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ]
        tapfile = self._write_tap(blocks)
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tapfile} out.z80')
        out_lines = self.out.getvalue().strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,8',
            'Tape finished'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile}: Failed to fast load block: unexpected end of tape')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x15_first_bit_0(self):
        direct_recording_block = (
            21,          # Block ID
            50, 0,       # T-states per sample
            0, 0,        # Pause
            8,           # Used bits in last byte
            3, 0, 0,     # Data length
            0b00000001,  # Data...
            0b00000010,
            0b00000011
        )
        tzxfile = self._write_tzx([direct_recording_block])
        output, error = self.run_tap2sna(tzxfile)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        block = blocks[0]
        self.assertEqual([], block.data)
        self.assertEqual([(1, 350), (1, 50), (1, 300), (1, 50), (1, 350), (1, 100)], block.timings.pulses)

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x15_first_bit_1(self):
        direct_recording_block = (
            21,          # Block ID
            10, 0,       # T-states per sample
            0, 0,        # Pause
            8,           # Used bits in last byte
            3, 0, 0,     # Data length
            0b10000000,  # Data...
            0b00000010,
            0b00000011
        )
        tzxfile = self._write_tzx([direct_recording_block])
        output, error = self.run_tap2sna(tzxfile)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        block = blocks[0]
        self.assertEqual([], block.data)
        self.assertEqual([(1, 0), (1, 10), (1, 130), (1, 10), (1, 70), (1, 20)], block.timings.pulses)

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x15_unused_bits_in_last_byte(self):
        direct_recording_block = (
            21,          # Block ID
            100, 0,      # T-states per sample
            0, 0,        # Pause
            4,           # Used bits in last byte
            3, 0, 0,     # Data length
            0b00000001,  # Data...
            0b00000010,
            0b11110000
        )
        tzxfile = self._write_tzx([direct_recording_block])
        output, error = self.run_tap2sna(tzxfile)
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        block = blocks[0]
        self.assertEqual([], block.data)
        self.assertEqual([(1, 700), (1, 100), (1, 600), (1, 100), (1, 100), (1, 400)], block.timings.pulses)

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x15_followed_by_bytes(self):
        direct_recording_block = (
            21,          # Block ID
            50, 0,       # T-states per sample
            0, 0,        # Pause
            8,           # Used bits in last byte
            1, 0, 0,     # Data length
            0,           # Data
        )
        blocks = [
            direct_recording_block,
            create_tzx_header_block(data_type=3),
            create_tzx_data_block([4, 5, 6]),
        ]
        tzxfile = self._write_tzx(blocks)
        output, error = self.run_tap2sna(tzxfile)
        self.assertEqual(error, '')
        load_cmd = [239, 34, 34, 13] # LOAD "" ENTER
        self.assertEqual(load_cmd, list(load_tracer.simulator.memory[23756:23760]))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x16(self):
        block = [
            22,         # Block ID
            4, 0, 0, 0, # Block length
            0, 0, 0, 0, # Contents
        ]
        tzxfile = self._write_tzx([block])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: TZX C64 ROM type data (0x16) not supported')
        self.assertEqual(self.out.getvalue(), '')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x17(self):
        block = [
            23,         # Block ID
            4, 0, 0, 0, # Block length
            0, 0, 0, 0, # Contents
        ]
        tzxfile = self._write_tzx([block])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: TZX C64 turbo tape data (0x17) not supported')
        self.assertEqual(self.out.getvalue(), '')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x18(self):
        block = [
            24,          # Block ID
            10, 0, 0, 0, # Block length
            0, 0,        # Pause
            68, 172,     # Sampling rate
            1,           # Compression type
            1, 0, 0, 0,  # Number of stored pulses
            1,           # CSW Data
        ]
        tzxfile = self._write_tzx([block])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: TZX CSW Recording (0x18) not supported')
        self.assertEqual(self.out.getvalue(), '')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_can_ignore_tzx_block_type_0x18(self):
        csw_recording_block = (
            24,          # Block ID
            10, 0, 0, 0, # Block length
            0, 0,        # Pause
            68, 172,     # Sampling rate
            1,           # Compression type
            1, 0, 0, 0,  # Number of stored pulses
            1,           # CSW Data
        )
        header_block = create_tzx_header_block()
        tzxfile = self._write_tzx((csw_recording_block, header_block))
        output, error = self.run_tap2sna(f'--tape-start 2 {tzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(header_block[5:], list(blocks[0].data))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_with_tzx_block_type_0x19(self):
        block = [
            25,          # Block ID
            14, 0, 0, 0, # Block length
            0, 0,        # Pause
            0, 0, 0, 0,  # Number of symbols in pilot/sync block
            1,           # Maximum number of pulses per pilot/sync symbol
            1,           # Number of pilot/sync symbols in alphabet table
            0, 0, 0, 0,  # Number of symbols in data stream
            1,           # Maximum number of pulses per data symbol
            1,           # Number of data symbols in alphabet table
        ]
        tzxfile = self._write_tzx([block])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'{tzxfile} out.z80')
        self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: TZX Generalized Data Block (0x19) not supported')
        self.assertEqual(self.out.getvalue(), '')
        self.assertEqual(self.err.getvalue(), '')

    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_can_ignore_tzx_block_type_0x19(self):
        generalized_data_block = (
            25,          # Block ID
            14, 0, 0, 0, # Block length
            0, 0,        # Pause
            0, 0, 0, 0,  # Number of symbols in pilot/sync block
            1,           # Maximum number of pulses per pilot/sync symbol
            1,           # Number of pilot/sync symbols in alphabet table
            0, 0, 0, 0,  # Number of symbols in data stream
            1,           # Maximum number of pulses per data symbol
            1,           # Number of data symbols in alphabet table
        )
        header_block = create_tzx_header_block()
        tzxfile = self._write_tzx((generalized_data_block, header_block))
        output, error = self.run_tap2sna(f'--tape-start 2 {tzxfile}')
        self.assertEqual(error, '')
        blocks = load_tracer.init_args['blocks']
        self.assertEqual(len(blocks), 1)
        self.assertEqual(header_block[5:], list(blocks[0].data))

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_trace(self):
        basic_data = [
            0, 10,               # Line 10
            14, 0,               # Line length
            249, 192,            # RANDOMIZE USR
            51, 50, 55, 54, 56,  # 32768 in ASCII
            14, 0, 0, 0, 128, 0, # 32768 in floating point form
            13                   # ENTER
        ]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ]
        tapfile = self._write_tap(blocks)
        tracefile = 'sim-load.trace'
        output, error = self.run_tap2sna(f'-c trace={tracefile} {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,18',
            'Tape finished',
            'Simulation stopped (PC in RAM): PC=32768',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        exp_reg = set(('^F=69', 'SP=65344', 'IX=23773', 'IY=23610', 'PC=32768'))
        self.assertLessEqual(exp_reg, set(s_reg))
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 8101)
        self.assertEqual(trace_lines[0], '$0605 POP AF')
        self.assertEqual(trace_lines[8100], '$34BB RET')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_trace_and_self_modifying_code(self):
        code = [
            33, 55, 128,  # 32820 LD HL,32823
            117,          # 32823 LD (HL),L   ; -> 32823 SCF
            48, 253,      # 32824 JR NC,32823
        ]
        tapfile, basic_data = self._write_basic_loader(32820, code)
        tracefile = 'sim-load.trace'
        output, error = self.run_tap2sna(f'--start 32826 -c trace={tracefile} {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        self.assertIn('PC=32826', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 13428)
        self.assertEqual(trace_lines[13423], '$8034 LD HL,$8037')
        self.assertEqual(trace_lines[13424], '$8037 LD (HL),L')
        self.assertEqual(trace_lines[13425], '$8038 JR NC,$8037')
        self.assertEqual(trace_lines[13426], '$8037 SCF')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_trace_and_interrupts_and_timestamps(self):
        basic_data = [
            0, 10,             # Line 10
            9, 0,              # Line length
            242,               # PAUSE
            48,                # 0 in ASCII
            14, 0, 0, 0, 0, 0, # 0 in floating point form
            13                 # ENTER
        ]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ]
        tapfile = self._write_tap(blocks)
        tracefile = 'sim-load.trace'
        trace_line = 'TraceLine={t} ${pc:04X} {i}'
        output, error = self.run_tap2sna(('-I', trace_line, '-c', f'trace={tracefile}', '--start', '57', tapfile, 'out.z80'))
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,13',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=57',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        self.assertEqual(basic_data, snapshot[23755:23755 + len(basic_data)])
        exp_reg = set(('IX=23768', 'IY=23610', 'PC=57'))
        self.assertLessEqual(exp_reg, set(s_reg))
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 11189)
        self.assertEqual(trace_lines[11187], '17891324 $1F3D HALT')
        self.assertEqual(trace_lines[11188], '17891341 $0038 PUSH AF')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_sim_load_with_trace_logs_time_correctly_after_fast_load(self):
        data = [0] * 20
        blocks = (
            create_tap_header_block("justheader", 10, len(data), 0),
            create_tap_data_block(data),
        )
        tapfile = self._write_tap(blocks)
        tracefile = 'sim-load.trace'
        args = ('-c', f'trace={tracefile}', '-I', 'TraceLine={t:>8} ${pc:04X} {i}', '--start', '0x053F', '-c', 'finish-tape=1', tapfile, 'out.z80')
        output, error = self.run_tap2sna(args)
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: justheader',
            'Fast loading data block: 23755,20',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=1343',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')
        exp_reg = {'SP=65360', 'IX=23775', 'IY=23610', 'PC=1343'}
        self.assertLessEqual(exp_reg, set(s_reg))
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 7281)
        self.assertEqual(trace_lines[2263], '   27206 $076E CALL $0556') # LoadTracer.next_block()
        self.assertEqual(trace_lines[2264], '17823986 $05E2 RET')        # advances the clock
        self.assertEqual(trace_lines[7279], '17865775 $0802 CALL $0556') # LoadTracer.stop_tape()
        self.assertEqual(trace_lines[7280], '17865792 $05E2 RET')        # does not

    def test_sim_load_config_help(self):
        for option in ('-c', '--sim-load-config'):
            output, error = self.run_tap2sna(f'{option} help')
            self.assertTrue(output.startswith('Usage: --sim-load-config accelerate-dec-a=0/1/2/3\n'))
            self.assertEqual(error, '')

    def test_sim_load_config_help_specific_parameter(self):
        for param in (
                'accelerate-dec-a', 'accelerator', 'cmio', 'fast-load',
                'finish-tape', 'first-edge', 'in-flags', 'load', 'machine',
                'pause', 'polarity', 'python', 'timeout', 'trace'
        ):
            for option in ('-c', '--sim-load-config'):
                output, error = self.run_tap2sna(f'{option} help-{param}')
                self.assertTrue(output.startswith(f'--sim-load-config {param}='))
                self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_timed_out(self):
        code_start = 32768
        code = [2]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'-c timeout=1 {tapfile} out.z80')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Simulation stopped (timed out): PC=1343',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen_close)
    def test_sim_load_screen_closed(self):
        code_start = 32768
        code = [1]
        tapfile, basic_data = self._write_basic_loader(code_start, code)
        output, error = self.run_tap2sna(f'--screen {tapfile} out.z80')
        self.assertEqual(error, '')
        out_lines = output.strip().split('\n')
        exp_out_lines = [
            'Program: simloadbas',
            'Fast loading data block: 23755,19',
            'Bytes: simloadbyt',
            'Simulation stopped (screen closed)',
            'Writing out.z80'
        ]
        self.assertEqual(exp_out_lines, out_lines)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_tape_ended_1_second_ago(self):
        basic_data = [
            0, 10,               # Line 10
            10, 0,               # Line length
            249, 192, 46,        # RANDOMIZE USR .
            14, 0, 0, 194, 4, 0, # 1218 (SA-BYTES)
            13,                  # ENTER
        ]
        tapfile = self._write_tap((
            create_tap_header_block('tapeend-1s', 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ))
        exp_out_lines = [
            'Program: tapeend-1s',
            'Fast loading data block: 23755,14',
            'Tape finished',
            'Simulation stopped (tape ended 1 second ago): PC=1240',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'{tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_out_lines, output.strip().split('\n'))

    def test_sim_load_config_help_invalid_parameter(self):
        for option in ('-c', '--sim-load-config'):
            output, error = self.run_tap2sna(f'{option} help-foo')
            self.assertTrue(output.startswith('Usage: --sim-load-config accelerate-dec-a=0/1/2/3\n'))
            self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_default_state(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual([], s_state)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_state_iff(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 --state iff=0 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(['iff=0'], s_state)

    def test_state_iff_bad_value(self):
        self._test_bad_spec('--state iff=fa', 'Cannot parse integer: iff=fa')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_state_im(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 --state im=2 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(['im=2'], s_state)

    def test_state_im_bad_value(self):
        self._test_bad_spec('--state im=Q', 'Cannot parse integer: im=Q')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_state_border(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 --state border=4 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(['border=4'], s_state)

    def test_state_border_bad_value(self):
        self._test_bad_spec('--state border=x!', 'Cannot parse integer: border=x!')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_state_tstates(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        output, error = self.run_tap2sna(f'--ram load=1,16384 --state tstates=31445 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(['tstates=31445'], s_state)

    def test_state_tstates_bad_value(self):
        self._test_bad_spec('--state tstates=?', 'Cannot parse integer: tstates=?')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_state_issue2(self):
        block = create_tap_data_block([0])
        tapfile = self._write_tap([block])
        for issue2 in (0, 1):
            output, error = self.run_tap2sna(f'--ram load=1,16384 --state issue2={issue2} {tapfile} out.z80')
            self.assertEqual(error, '')
            self.assertEqual([f'issue2={issue2}'], s_state)

    def test_state_issue2_bad_value(self):
        self._test_bad_spec('--state issue2=*', 'Cannot parse integer: issue2=*')

    def test_state_help(self):
        output, error = self.run_tap2sna('--state help')
        self.assertEqual(error, '')
        exp_output = """
            Usage: --state name=value

            Set a hardware state attribute. Recognised names and their default values are:

              7ffd    - last OUT to port 0x7ffd (128K only)
              ay[N]   - contents of AY register N (N=0-15; 128K only)
              border  - border colour (default=0)
              fe      - last OUT to port 0xfe (SZX only)
              fffd    - last OUT to port 0xfffd (128K only)
              iff     - interrupt flip-flop: 0=disabled, 1=enabled (default=1)
              im      - interrupt mode (default=1)
              issue2  - issue 2 emulation: 0=disabled, 1=enabled (default=0)
              tstates - T-states elapsed since start of frame (default=34943)
        """
        self.assertEqual(dedent(exp_output).lstrip(), output)

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_config_parameter_default_values(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(tapfile)
        self.assertEqual(error, '')
        self.assertIsNone(kbtracer)
        self.assertIs(load_tracer.simulator.__class__, CSimulator or Simulator)
        self.assertEqual(len(load_tracer.accelerators), 53)
        self.assertTrue(load_tracer.pause)
        config = load_tracer.init_args['config']
        self.assertEqual(config['first_edge'], 0)
        self.assertEqual(config['polarity'], 0)
        self.assertEqual(load_tracer.finish_tape, 0)
        self.assertEqual(load_tracer.in_min_addr, 0x8000)
        self.assertEqual(load_tracer.accel_dec_a, 3)
        self.assertFalse(load_tracer.list_accelerators)
        self.assertEqual(load_tracer.border, 7)
        self.assertEqual(load_tracer.out7ffd, 0)
        self.assertEqual(load_tracer.outfffd, 0)
        self.assertEqual([0] * 16, load_tracer.ay)
        self.assertEqual(load_tracer.outfe, 0)
        self.assertIsNone(load_tracer.stop)
        self.assertEqual(load_tracer.flash_load, 1)
        self.assertEqual(load_tracer.timeout, 3150000000)
        self.assertIsNone(load_tracer.tracefile)
        self.assertIsNone(load_tracer.trace_line)
        self.assertIsNone(load_tracer.prefix)
        self.assertIsNone(load_tracer.byte_fmt)
        self.assertIsNone(load_tracer.word_fmt)

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_config_parameters(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        trace_log = 'trace.log'
        params = (
            'accelerate-dec-a=2',
            'accelerator=speedlock',
            'cmio=0',
            'fast-load=0',
            'finish-tape=1',
            'first-edge=1234',
            'in-flags=0',
            'load=RUN',
            'machine=128',
            'pause=0',
            'polarity=1',
            'timeout=1000',
            f'trace={trace_log}'
        )
        options = ' '.join(f'-c {p}' for p in params)
        output, error = self.run_tap2sna(f'{options} {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(['RUN', 'ENTER'], kbtracer.load)
        self.assertEqual(kbtracer.kb_delay, 13)
        self.assertTrue(kbtracer.run_called)
        self.assertEqual(kbtracer.stop, 0x13BE)
        self.assertEqual(kbtracer.timeout, 3500000000)
        self.assertEqual(kbtracer.tracefile.name, trace_log)
        self.assertEqual(kbtracer.trace_line, '${pc:04X} {i}\n')
        self.assertEqual(kbtracer.prefix, '$')
        self.assertEqual(kbtracer.byte_fmt, '02X')
        self.assertEqual(kbtracer.word_fmt, '04X')
        self.assertIs(load_tracer.simulator.__class__, CSimulator or Simulator)
        self.assertEqual(['speedlock'], [a.name for a in load_tracer.accelerators])
        self.assertEqual(load_tracer.pause, 0)
        config = load_tracer.init_args['config']
        self.assertEqual(config['first_edge'], 1234)
        self.assertEqual(config['polarity'], 1)
        self.assertEqual(load_tracer.finish_tape, 1)
        self.assertEqual(load_tracer.in_min_addr, 0x8000)
        self.assertEqual(load_tracer.accel_dec_a, 2)
        self.assertFalse(load_tracer.list_accelerators)
        self.assertEqual(load_tracer.border, kbtracer.border)
        self.assertEqual(load_tracer.out7ffd, kbtracer.out7ffd)
        self.assertEqual(load_tracer.outfffd, kbtracer.outfffd)
        self.assertEqual(load_tracer.ay, kbtracer.ay)
        self.assertEqual(load_tracer.outfe, kbtracer.outfe)
        self.assertIsNone(load_tracer.stop)
        self.assertEqual(load_tracer.flash_load, 0)
        self.assertTrue(load_tracer.timeout < 3500000000)
        self.assertEqual(load_tracer.tracefile.name, trace_log)
        self.assertEqual(load_tracer.trace_line, '${pc:04X} {i}\n')
        self.assertEqual(load_tracer.prefix, '$')
        self.assertEqual(load_tracer.byte_fmt, '02X')
        self.assertEqual(load_tracer.word_fmt, '04X')

    @patch.object(tap2sna, 'CSimulator', MockSimulator)
    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_disables_fast_djnz_ldir(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(tapfile)
        self.assertEqual(error, '')
        self.assertFalse(simulator.config.get('fast_djnz'))
        self.assertFalse(simulator.config.get('fast_ldir'))

    @patch.object(tap2sna, 'CSimulator', MockSimulator)
    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_sim_load_disables_fast_djnz_ldir_when_tracing(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c trace=load.log {tapfile}')
        self.assertEqual(error, '')
        self.assertFalse(simulator.config.get('fast_djnz'))
        self.assertFalse(simulator.config.get('fast_ldir'))

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_cmio_1_disables_acceleration(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        trace_log = 'trace.log'
        params = (
            'accelerate-dec-a=2',
            'accelerator=speedlock',
            'cmio=1'
        )
        options = ' '.join(f'-c {p}' for p in params)
        output, error = self.run_tap2sna(f'{options} {tapfile}')
        self.assertEqual(error, '')
        self.assertIs(load_tracer.simulator.__class__, CCMIOSimulator or CMIOSimulator)
        self.assertEqual(set(), load_tracer.accelerators)
        self.assertEqual(load_tracer.accel_dec_a, 0)

    @patch.object(tap2sna, 'CSimulator', MockSimulator)
    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_in_flags_parameter_bit_0(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c in-flags=1 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(load_tracer.in_min_addr, 0x4000)
        set_tracer_args = load_tracer.simulator.set_tracer_args
        self.assertFalse(set_tracer_args[0])
        self.assertFalse(set_tracer_args[1])

    @patch.object(tap2sna, 'CSimulator', MockSimulator)
    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_in_flags_parameter_bit_1(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c in-flags=2 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(load_tracer.in_min_addr, 0x10000)
        set_tracer_args = load_tracer.simulator.set_tracer_args
        self.assertFalse(set_tracer_args[0])
        self.assertFalse(set_tracer_args[1])

    @patch.object(tap2sna, 'CSimulator', MockSimulator)
    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_in_flags_parameter_bit_2(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c in-flags=4 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(load_tracer.in_min_addr, 0x8000)
        set_tracer_args = load_tracer.simulator.set_tracer_args
        self.assertTrue(set_tracer_args[0])
        self.assertFalse(set_tracer_args[1])

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_load_parameter_defaults_to_enter_for_128k(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c machine=128 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(['ENTER'], kbtracer.load)

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_load_parameter_with_pc(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c load=PC=16384 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(['ENTER'], kbtracer.load)
        self.assertTrue(kbtracer.run_called)
        self.assertEqual(kbtracer.stop, 16384)

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_keyboard_tracer_config_for_48k(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c load=RUN {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(len(kbtracer.simulator.memory), 65536)
        self.assertEqual(['RUN', 'ENTER'], kbtracer.load)
        self.assertEqual(kbtracer.kb_delay, 4)
        self.assertTrue(kbtracer.run_called)
        self.assertEqual(kbtracer.stop, 0x0605)

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_keyboard_tracer_config_for_128k(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c machine=128 {tapfile}')
        self.assertEqual(error, '')
        self.assertEqual(len(kbtracer.simulator.memory), 0x20000)
        self.assertEqual(['ENTER'], kbtracer.load)
        self.assertEqual(kbtracer.kb_delay, 13)
        self.assertTrue(kbtracer.run_called)
        self.assertEqual(kbtracer.stop, 0x13BE)

    @patch.object(tap2sna, 'Simulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_python_parameter(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c python=1 {tapfile}')
        self.assertEqual(error, '')
        self.assertIs(load_tracer.simulator.__class__, MockSimulator)

    @patch.object(tap2sna, 'CMIOSimulator', MockSimulator)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_python_parameter_with_cmio(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c python=1 -c cmio=1 {tapfile}')
        self.assertEqual(error, '')
        self.assertIs(load_tracer.simulator.__class__, MockSimulator)

    def test_empty_tape(self):
        tapfile = self._write_tap([])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(tapfile)
        self.assertEqual(cm.exception.args[0], f"Error while converting {tapfile}: Tape is empty")

    def test_load_parameter_with_invalid_pc(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        with self.assertRaises(SkoolKitError) as cm:
            self.run_tap2sna(f'-c load=PC=? {tapfile}')
        self.assertEqual(cm.exception.args[0], f"Error while converting {tapfile}: Invalid integer in 'load' parameter: PC=?")

    @patch.object(tap2sna, 'KeyboardTracer', MockKeyboardTracer)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_keyboard_tracer_timed_out(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        load = ' '.join(['a'] * 50)
        output, error = self.run_tap2sna(('-c', 'timeout=1', '-c', f'load={load}', tapfile))
        self.assertEqual(error, '')
        self.assertTrue(kbtracer.run_called)
        self.assertEqual(kbtracer.timeout, 3500000)
        self.assertIn('Simulation stopped (timed out): PC=4608\n', output)

    @patch.object(tap2sna, 'KeyboardTracer', InterruptedTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_interrupted_keyboard_tracer(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'-c load=RUN {tapfile}')
        self.assertEqual(error, '')
        self.assertIn('Simulation stopped (interrupted): PC=4608\n', output)

    @patch.object(tap2sna, 'LoadTracer', InterruptedTracer)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_interrupted_load_tracer(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(tapfile)
        self.assertEqual(error, '')
        self.assertIn('Simulation stopped (interrupted): PC=1541\n', output)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_DefaultSnapshotFormat_read_from_file(self):
        ini = """
            [tap2sna]
            DefaultSnapshotFormat=szx
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        tapfile = self._write_tap([create_tap_data_block([0])])
        exp_outfile = tapfile[:-4] + '.szx'
        output, error = self.run_tap2sna(f'--ram load=1,16384 {tapfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_outfile)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_DefaultSnapshotFormat_set_on_command_line(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        exp_outfile = tapfile[:-4] + '.szx'
        output, error = self.run_tap2sna(f'--ram load=1,16384 -I DefaultSnapshotFormat=szx {tapfile}')
        self.assertEqual(len(error), 0)
        self.assertEqual(s_fname, exp_outfile)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    def test_config_ScreenFps_read_from_file(self):
        ini = """
            [tap2sna]
            ScreenFps=10
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'--screen {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIsNotNone(load_tracer.draw)
        self.assertIsNotNone(screen)
        self.assertEqual(screen.scale, 2)
        self.assertEqual(screen.fps, 10)
        self.assertEqual(screen.caption, 'tap2sna.py')
        self.assertFalse(screen.is128k)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    def test_config_ScreenFps_set_on_command_line(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'--screen -I ScreenFps=0 -c machine=128 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIsNotNone(load_tracer.draw)
        self.assertIsNotNone(screen)
        self.assertEqual(screen.scale, 2)
        self.assertEqual(screen.fps, 0)
        self.assertEqual(screen.caption, 'tap2sna.py')
        self.assertTrue(screen.is128k)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    def test_config_ScreenScale_read_from_file(self):
        ini = """
            [tap2sna]
            ScreenScale=3
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'--screen {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIsNotNone(load_tracer.draw)
        self.assertIsNotNone(screen)
        self.assertEqual(screen.scale, 3)
        self.assertEqual(screen.fps, 50)
        self.assertEqual(screen.caption, 'tap2sna.py')
        self.assertFalse(screen.is128k)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    @patch.object(tap2sna, 'get_screen', mock_get_screen)
    @patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
    def test_config_ScreenScale_set_on_command_line(self):
        tapfile = self._write_tap([create_tap_data_block([0])])
        output, error = self.run_tap2sna(f'--screen -I ScreenScale=1 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIsNotNone(load_tracer.draw)
        self.assertIsNotNone(screen)
        self.assertEqual(screen.scale, 1)
        self.assertEqual(screen.fps, 50)
        self.assertEqual(screen.caption, 'tap2sna.py')
        self.assertFalse(screen.is128k)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_read_from_file(self):
        ini = """
            [tap2sna]
            TraceLine={pc:05}: {i}
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        basic_data = [
            0, 10,               # Line 10
            2, 0,                # Line length
            234,                 # REM
            13                   # ENTER
        ]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ]
        tapfile = self._write_tap(blocks)
        tracefile = 'sim-load.trace'
        output, error = self.run_tap2sna(f'-c trace={tracefile} --start 1343 -c finish-tape=1 {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertIn('PC=1343', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 7281)
        self.assertEqual(trace_lines[0], '01541: POP AF')
        self.assertEqual(trace_lines[7280], '01506: RET')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_set_on_command_line(self):
        basic_data = [
            0, 10,               # Line 10
            2, 0,                # Line length
            234,                 # REM
            13                   # ENTER
        ]
        blocks = [
            create_tap_header_block("simloadbas", 10, len(basic_data), 0),
            create_tap_data_block(basic_data),
        ]
        tapfile = self._write_tap(blocks)
        tracefile = 'sim-load.trace'
        trace_line = '{pc:>5}:{i}'
        args = f'-I TraceLine={trace_line} -c trace={tracefile} --start 1343 -c finish-tape=1 {tapfile} out.z80'
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertIn('PC=1343', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 7281)
        self.assertEqual(trace_lines[0], ' 1541:POP AF')
        self.assertEqual(trace_lines[7280], ' 1506:RET')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_register_values(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "{t:>3} ${pc:04X} {i:<13} AFBCDEHL={r[a]:02X}{r[f]:02X}{r[b]:02X}{r[c]:02X}{r[d]:02X}{r[e]:02X}{r[h]:02X}{r[l]:02X}"
        trace_line += " AFBCDEHL'={r[^a]:02X}{r[^f]:02X}{r[^b]:02X}{r[^c]:02X}{r[^d]:02X}{r[^e]:02X}{r[^h]:02X}{r[^l]:02X}"
        trace_line += " IX={r[ixh]:02X}{r[ixl]:02X} IY={r[iyh]:02X}{r[iyl]:02X} SP={r[sp]:04X} IR={r[i]:02X}{r[r]:02X}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x250E', tapfile, 'out.z80')
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=9486', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 31)
        self.assertEqual(trace_lines[0], "  0 $0605 POP AF        AFBCDEHL=1B52000000000000 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF52 IR=3F01")
        self.assertEqual(trace_lines[1], " 10 $0606 LD A,($5C74)  AFBCDEHL=E152000000000000 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF52 IR=3F02")
        self.assertEqual(trace_lines[2], " 23 $0609 SUB $E0       AFBCDEHL=0102000000000000 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF52 IR=3F03")
        self.assertEqual(trace_lines[28], "267 $250A LD B,$00      AFBCDEHL=2261002200002597 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF4C IR=3F1D")
        self.assertEqual(trace_lines[29], "274 $250C LD C,(HL)     AFBCDEHL=2261001C00002597 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF4C IR=3F1E")
        self.assertEqual(trace_lines[30], "281 $250D ADD HL,BC     AFBCDEHL=2260001C000025B3 AFBCDEHL'=0000000000000000 IX=0000 IY=5C3A SP=FF4C IR=3F1F")

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_register_pairs(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} BCDEHL={r[bc]:04X}{r[de]:04X}{r[hl]:04X}"
        trace_line += " BCDEHL'={r[^bc]:04X}{r[^de]:04X}{r[^hl]:04X}"
        trace_line += " IX={r[ix]:04X} IY={r[iy]:04X}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x250E', tapfile, 'out.z80')
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=9486', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 31)
        self.assertEqual(trace_lines[0], "$0605 POP AF        BCDEHL=000000000000 BCDEHL'=000000000000 IX=0000 IY=5C3A")
        self.assertEqual(trace_lines[1], "$0606 LD A,($5C74)  BCDEHL=000000000000 BCDEHL'=000000000000 IX=0000 IY=5C3A")
        self.assertEqual(trace_lines[2], "$0609 SUB $E0       BCDEHL=000000000000 BCDEHL'=000000000000 IX=0000 IY=5C3A")
        self.assertEqual(trace_lines[28], "$250A LD B,$00      BCDEHL=002200002597 BCDEHL'=000000000000 IX=0000 IY=5C3A")
        self.assertEqual(trace_lines[29], "$250C LD C,(HL)     BCDEHL=001C00002597 BCDEHL'=000000000000 IX=0000 IY=5C3A")
        self.assertEqual(trace_lines[30], "$250D ADD HL,BC     BCDEHL=001C000025B3 BCDEHL'=000000000000 IX=0000 IY=5C3A")

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_config_TraceLine_with_memptr(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} MEMPTR={r[memptr]:04X}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}',
                '-c', 'cmio=1', '--start', '0x0018', tapfile, 'out.z80')
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        exp_trace = """
            $0605 POP AF        MEMPTR=0000
            $0606 LD A,($5C74)  MEMPTR=5C75
            $0609 SUB $E0       MEMPTR=5C75
            $060B LD ($5C74),A  MEMPTR=0175
            $060E CALL $1C8C    MEMPTR=1C8C
            $1C8C CALL $24FB    MEMPTR=24FB
            $24FB RST $18       MEMPTR=0018
        """
        with open(tracefile, 'r') as f:
            self.assertEqual(dedent(exp_trace).strip(), f.read().rstrip())

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_memory_contents(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} (23668)={m[23668]}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x060E', tapfile, 'out.z80')
        exp_trace = """
            $0605 POP AF        (23668)=225
            $0606 LD A,($5C74)  (23668)=225
            $0609 SUB $E0       (23668)=225
            $060B LD ($5C74),A  (23668)=1
        """
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=1550', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip()
        self.assertEqual(dedent(exp_trace).strip(), trace_lines)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_memory_contents_using_hex_address(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} ($5C74)=${m[$5c74]:02X}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x060E', tapfile, 'out.z80')
        exp_trace = """
            $0605 POP AF        ($5C74)=$E1
            $0606 LD A,($5C74)  ($5C74)=$E1
            $0609 SUB $E0       ($5C74)=$E1
            $060B LD ($5C74),A  ($5C74)=$01
        """
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=1550', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip()
        self.assertEqual(dedent(exp_trace).strip(), trace_lines)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_memory_contents_using_0x_prefixed_hex_address(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} (0x5C74)=0x{m[0x5c74]:02X}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x060E', tapfile, 'out.z80')
        exp_trace = """
            $0605 POP AF        (0x5C74)=0xE1
            $0606 LD A,($5C74)  (0x5C74)=0xE1
            $0609 SUB $E0       (0x5C74)=0xE1
            $060B LD ($5C74),A  (0x5C74)=0x01
        """
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=1550', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip()
        self.assertEqual(dedent(exp_trace).strip(), trace_lines)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceLine_with_memory_contents_using_escaped_braces(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        trace_line = "${pc:04X} {i:<13} {{m[0x5C74]}}={{{m[0x5c74]:02X}}}"
        args = ('-I', f'TraceLine={trace_line}', '-c', f'trace={tracefile}', '--start', '0x060E', tapfile, 'out.z80')
        exp_trace = """
            $0605 POP AF        {m[0x5C74]}={E1}
            $0606 LD A,($5C74)  {m[0x5C74]}={E1}
            $0609 SUB $E0       {m[0x5C74]}={E1}
            $060B LD ($5C74),A  {m[0x5C74]}={01}
        """
        tap2sna.main(args)
        self.assertEqual(self.err.getvalue(), '')
        self.assertIn('PC=1550', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip()
        self.assertEqual(dedent(exp_trace).strip(), trace_lines)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceOperand(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 10, 1, 0)])
        tracefile = 'sim-load.trace'
        args = f'-I TraceOperand=&,02x,04x -c trace={tracefile} --start 1343 {tapfile} out.z80'
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertIn('PC=1343', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 2265)
        self.assertEqual(trace_lines[1], '$0606 LD A,(&5c74)')
        self.assertEqual(trace_lines[2], '$0609 SUB &e0')
        self.assertEqual(trace_lines[2264], '$05E2 RET')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceOperand_with_no_commas(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 1, 1, 0)])
        tracefile = 'sim-load.trace'
        args = f'-I TraceOperand=# -c trace={tracefile} --start 1547 {tapfile} out.z80'
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertIn('PC=1547', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 3)
        self.assertEqual(trace_lines[1], '$0606 LD A,(#23668)')
        self.assertEqual(trace_lines[2], '$0609 SUB #224')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceOperand_with_one_comma(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 1, 1, 0)])
        tracefile = 'sim-load.trace'
        args = f'-I TraceOperand=+,02x -c trace={tracefile} --start 1547 {tapfile} out.z80'
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertIn('PC=1547', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 3)
        self.assertEqual(trace_lines[1], '$0606 LD A,(+23668)')
        self.assertEqual(trace_lines[2], '$0609 SUB +e0')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_config_TraceOperand_with_three_commas(self):
        tapfile = self._write_tap([create_tap_header_block("prog", 1, 1, 0)])
        tracefile = 'sim-load.trace'
        args = f'-I TraceOperand=0x,02x,04x,??? -c trace={tracefile} --start 1547 {tapfile} out.z80'
        output, error = self.run_tap2sna(args)
        self.assertEqual(error, '')
        self.assertIn('PC=1547', s_reg)
        with open(tracefile, 'r') as f:
            trace_lines = f.read().rstrip().split('\n')
        self.assertEqual(len(trace_lines), 3)
        self.assertEqual(trace_lines[1], '$0606 LD A,(0x5c74)')
        self.assertEqual(trace_lines[2], '$0609 SUB 0xe0')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    @patch.object(tap2sna, 'urlopen')
    def test_config_UserAgent_read_from_file(self, mock_urlopen):
        user_agent = 'SkoolKit/9.5'
        ini = f"""
            [tap2sna]
            UserAgent={user_agent}
        """
        self.write_text_file(dedent(ini).strip(), 'skoolkit.ini')
        tap_data = bytes(create_tap_data_block([1]))
        url = 'http://example.com/test.tap'
        mock_urlopen.return_value = BytesIO(tap_data)
        output, error = self.run_tap2sna(f'--ram load=1,23296 {url}')
        self.assertTrue(output.startswith(f'Downloading {url}\n'))
        self.assertEqual(error, '')
        request = mock_urlopen.call_args[0][0]
        self.assertEqual({'User-agent': user_agent}, request.headers)
        self.assertEqual(snapshot[23296], 1)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    @patch.object(tap2sna, 'urlopen')
    def test_config_UserAgent_set_on_command_line(self, mock_urlopen):
        tap_data = bytes(create_tap_data_block([2]))
        url = 'http://example.com/test.tap'
        user_agent = 'tap2sna.py/9.5'
        mock_urlopen.return_value = BytesIO(tap_data)
        output, error = self.run_tap2sna(f'-I UserAgent={user_agent} --ram load=1,23296 {url}')
        self.assertTrue(output.startswith(f'Downloading {url}\n'))
        self.assertEqual(error, '')
        request = mock_urlopen.call_args[0][0]
        self.assertEqual({'User-agent': user_agent}, request.headers)
        self.assertEqual(snapshot[23296], 2)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_args_from_file(self):
        data = [1, 2, 3, 4]
        start = 49152
        args = f"""
            ; Comment
            # Another comment
            --ram load=1,{start} # Load first block
            --ram poke=32768,255 ; POKE 32768,255
        """
        args_file = self.write_text_file(dedent(args).strip(), suffix='.t2s')
        self._load_tape(start, data, f'@{args_file}')
        self.assertEqual(data, snapshot[start:start + len(data)])
        self.assertEqual(snapshot[32768], 255)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_quoted_args_from_file(self):
        code = [1, 2, 3]
        code_start = 24576
        tap_data = create_tap_data_block(code)
        tapfile = self.write_bin_file(tap_data, 'all the data.tap')
        z80file = 'my snapshot.z80'
        args = f"""
            --ram load=1,{code_start}
            --ram poke=32768,255
            "{tapfile}"
            '{z80file}'
        """
        args_file = self.write_text_file(dedent(args).strip(), suffix='.t2s')
        output, error = self.run_tap2sna(f'@{args_file}')
        self.assertEqual(error, '')
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])
        self.assertEqual(snapshot[32768], 255)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_t2s_file_with_default_snapshot_filename(self):
        code = [3, 2, 1]
        code_start = 32768
        tap_data = create_tap_data_block(code)
        tapfile = self.write_bin_file(tap_data)
        args = f"""
            "{tapfile}"
            --ram load=1,{code_start}
        """
        args_file = self.write_text_file(dedent(args).strip(), suffix='.t2s')
        exp_z80 = f'{args_file[:-4]}.z80'
        output, error = self.run_tap2sna(f'@{args_file}')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, exp_z80)
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_t2s_file_in_subdirectory_with_default_snapshot_filename(self):
        code = [3, 2, 1]
        code_start = 32768
        tap_data = create_tap_data_block(code)
        tapfile = self.write_bin_file(tap_data)
        args = f"""
            "{tapfile}"
            --ram load=1,{code_start}
        """
        subdir = 'foo/bar/baz'
        t2s_name = 'game'
        os.makedirs(subdir)
        args_file = self.write_text_file(dedent(args).strip(), f'{subdir}/{t2s_name}.t2s')
        exp_z80 = f'{t2s_name}.z80'
        output, error = self.run_tap2sna(f'@{args_file}')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, exp_z80)
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_t2s_file_with_default_snapshot_filename_in_szx_format(self):
        code = [3, 2, 1]
        code_start = 32768
        tap_data = create_tap_data_block(code)
        tapfile = self.write_bin_file(tap_data)
        args = f"""
            "{tapfile}"
            --ram load=1,{code_start}
            --ini DefaultSnapshotFormat=szx
        """
        args_file = self.write_text_file(dedent(args).strip(), suffix='.t2s')
        exp_szx = f'{args_file[:-4]}.szx'
        output, error = self.run_tap2sna(f'@{args_file}')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, exp_szx)
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_t2s_file_with_snapshot_filename_given_on_command_line(self):
        code = [3, 2, 1]
        code_start = 32768
        tap_data = create_tap_data_block(code)
        tapfile = self.write_bin_file(tap_data)
        args = f"""
            "{tapfile}"
            --ram load=1,{code_start}
        """
        args_file = self.write_text_file(dedent(args).strip(), suffix='.t2s')
        output, error = self.run_tap2sna(f'@{args_file} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(s_fname, 'out.z80')
        self.assertEqual(code, snapshot[code_start:code_start + len(code)])

    @patch.object(tap2sna, 'urlopen', Mock(return_value=BytesIO(bytearray(create_tap_data_block([2, 3])))))
    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_remote_download(self):
        data = [2, 3]
        start = 17000
        url = 'http://example.com/test.tap'
        output, error = self.run_tap2sna(f'--ram load=1,{start} {url} test.z80')
        self.assertTrue(output.startswith(f'Downloading {url}\n'))
        self.assertEqual(error, '')
        self.assertEqual(data, snapshot[start:start + len(data)])

    @patch.object(tap2sna, 'urlopen', Mock(side_effect=urllib.error.HTTPError('', 403, 'Forbidden', None, None)))
    def test_http_error_on_remote_download(self):
        with self.assertRaisesRegex(SkoolKitError, '^Error while converting test.zip: HTTP Error 403: Forbidden$'):
            self.run_tap2sna('http://example.com/test.zip test.z80')

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_dec_a(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = (
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xF3,             # $FF52 DI
            0xAF,             # $FF53 XOR A
            0x3D,             # $FF54 DEC A
            0x20, 0xFD,       # $FF55 JR NZ,$FF54
            0x3D,             # $FF57 DEC A
            0xC2, 0x57, 0xFF, # $FF58 JP NZ,$FF57
        )
        tapfile = self._write_tap((
            create_tap_header_block('deca-jr-jp', code_start, len(code)),
            create_tap_data_block(code)
        ))
        exp_output = [
            'Bytes: deca-jr-jp',
            'Fast loading data block: 65360,11',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=65371',
            'Accelerators: none; misses: 0; dec-a: 1/1/23',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 0xff5b -c accelerator=list {tapfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_list_accelerators(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = (
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xF3,             # $FF52 DI
            0x01, 0x40, 0xFC, # $FF53 LD BC,$FC40
            0xC5,             # $FF56 PUSH BC
            0x05,             # $FF57 DEC B
            0xC8,             # $FF58 RET Z
            0xDB, 0xFE,       # $FF59 IN A,($FE)
            0xA9,             # $FF5B XOR C
            0xE6, 0x40,       # $FF5C AND $40
            0xCA, 0x57, 0xFF, # $FF5E JP Z,$FF57
            0x18, 0xF4,       # $FF61 JR $FF57
        )
        tzxfile = self._write_tzx((
            create_tzx_header_block('listaccels', code_start, len(code)),
            create_tzx_data_block(code),
            (18, 50, 0, 3, 0), # 0x12 Pure Tone (3x50 T-states)
        ))
        exp_output = [
            'Bytes: listaccels',
            'Fast loading data block: 65360,19',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Tape finished',
            'Simulation stopped (PC at start address): PC=64576',
            'Accelerators: digital-integration: 1; misses: 0; dec-a: 0/0/23',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 0xfc40 -c accelerator=list {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_list_accelerators_polarity_sensitive(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = (
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xF3,             # $FF52 DI
            0xDB, 0xFE,       # $FF53 IN A,($FE)  ; Resume tape
            0x26, 0x40,       # $FF55 LD H,$40
            0x2C,             # $FF57 INC L
            0xDB, 0xFE,       # $FF58 IN A,($FE)
            0xA4,             # $FF5A AND H
            0xCA, 0x57, 0xFF, # $FF5B JP Z,$FF57
            0x2C,             # $FF5E INC L
            0xDB, 0xFE,       # $FF5F IN A,($FE)
            0xA4,             # $FF61 AND H
            0xC2, 0x5E, 0xFF, # $FF62 JP NZ,$FF5E
        )
        tzxfile = self._write_tzx((
            create_tzx_header_block('listaccels', code_start, len(code)),
            create_tzx_data_block(code),
            (18, 60, 0, 3, 0), # 0x12 Pure Tone (3x60 T-states)
        ))
        exp_output = [
            'Bytes: listaccels',
            'Fast loading data block: 65360,21',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Simulation stopped (PC at start address): PC=65381',
            'Accelerators: gremlin2-0: 1; gremlin2-1: 1; misses: 0; dec-a: 0/0/23',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 0xff65 -c accelerator=list {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_list_accelerators_tsl_miss(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = (
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xF3,             # $FF52 DI
            0xDB, 0xFE,       # $FF53 IN A,($FE)  ; Resume tape
            0xDB, 0xFE,       # $FF55 IN A,($FE)  ; Miss
        )
        tzxfile = self._write_tzx((
            create_tzx_header_block('listaccels', code_start, len(code)),
            create_tzx_data_block(code),
            (18, 20, 0, 3, 0), # 0x12 Pure Tone (3x20 T-states)
        ))
        exp_output = [
            'Bytes: listaccels',
            'Fast loading data block: 65360,7',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Simulation stopped (PC at start address): PC=65367',
            'Accelerators: none; misses: 1; dec-a: 0/0/23',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 0xff57 -c accelerator=list {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(components, 'SK_CONFIG', None)
    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_custom_screen(self):
        custom_screen = """
            class CustomScreen:
                def __init__(self, scale, fps, caption, is128k):
                    pass
                def draw(self, scr, frame, border, keyboard=None):
                    print(f'Frame {frame}: ATTR(0,1)={scr[6145]}; BORDER={border[-1][1]}')
                    return True
        """
        self.write_component_config('Screen', '*.CustomScreen', custom_screen)

        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = [
            0x52, 0xFF,             # $FF50 DEFW $FF52
            0x3E, 0x01,             # $FF52 LD A,$01
            0xD3, 0xFE,             # $FF54 OUT ($FE),A
            0x21, 0x01, 0x58,       # $FF56 LD HL,$5801
            0x36, 0x04,             # $FF59 LD (HL),$04
            0x76,                   # $FF5B HALT
            0x00,                   # $FF5C NOP         ; Force screen draw
        ]
        blocks = [
            create_tap_header_block("code", code_start, len(code)),
            create_tap_data_block(code)
        ]
        tapfile = self._write_tap(blocks)
        start = code_start + len(code)
        exp_out_lines = [
            'Bytes: code      ',
            'Frame 255: ATTR(0,1)=56; BORDER=7',
            'Fast loading data block: 65360,13',
            'Tape finished',
            'Frame 256: ATTR(0,1)=4; BORDER=1',
            f'Simulation stopped (PC at start address): PC={start}',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--screen --start {start} {tapfile} out.z80')
        self.assertEqual(exp_out_lines, output.strip().split('\n'))
        self.assertEqual(error, '')

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_tape_stops_1ms_after_final_edge(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = [
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xDB, 0xFE,       # $FF52 IN A,($FE)  ; Resume tape
            0x01, 0x00, 0x00, # $FF54 LD BC,$0000
            0x03,             # $FF57 INC BC      ; 18 T-states
            0x18, 0xFD,       # $FF58 JR $FF57    ; per loop iteration
        ]
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block('tapstop1ms', code_start, len(code)))
        pzx.add_puls()
        pzx.add_data(create_data_block(code))
        pzx.add_data([0], s0=(1, 1))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        exp_output = [
            'Bytes: tapstop1ms',
            'Fast loading data block: 65360,10',
            'Data (1 bytes)',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Tape finished',
            'Simulation stopped (end of tape): PC=65368',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'{pzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))
        self.assertIn('BC=195', s_reg)

    @patch.object(tap2sna, 'write_snapshot', mock_write_snapshot)
    def test_128k_read_ay_reg_14(self):
        basic_data = [
            0, 10,                # Line 10
            10, 0,                # Line length
            249, 192, 46,         # RANDOMIZE USR .
            14, 0, 0, 222, 92, 0, # 23774
            13,                   # ENTER
            0, 20,                # Line 20
            27, 0,                # Line length
            234,                  # REM
            219, 254,             # 23774 IN A,(254)    ; Resume tape
            243,                  # 23776 DI
            33, 3, 0,             # 23777 LD HL,3       ; BAUD=3 minimises time
            34, 95, 91,           # 23780 LD (23391),HL ; spent in ROM routine
            1, 253, 127,          # 23783 LD BC,32765
            175,                  # 23786 XOR A
            237, 121,             # 23787 OUT (C),A     ; Page in ROM 0
            205, 163, 8,          # 23789 CALL 2211     ; Read AY register 14
            243,                  # 23792 DI
            1, 0, 0,              # 23793 LD BC,0
            3,                    # 23796 INC BC        ; 18 T-states
            24, 253,              # 23797 JR 23796      ; per loop iteration
            13,                   # ENTER
        ]
        pzx = PZX()
        pzx.add_puls()
        pzx.add_data(create_header_block('tapayreg14', 10, len(basic_data), 0))
        pzx.add_puls()
        pzx.add_data(create_data_block(basic_data))
        pzx.add_data([0], s0=(1, 1))
        pzxfile = self.write_bin_file(pzx.data, suffix='.pzx')
        exp_output = [
            'Program: tapayreg14',
            'Fast loading data block: 23755,45',
            'Data (1 bytes)',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Tape finished',
            'Simulation stopped (end of tape): PC=23796',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'-c machine=128 -c in-flags=5 {pzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))
        self.assertIn('BC=79', s_reg)

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_trailing_tone_unread(self):
        blocks = [
            create_tzx_header_block('trailingtn', 32768, 1),
            create_tzx_data_block([1]),
            (18, 20, 0, 2, 0), # 0x12 Pure Tone (2x20 T-states)
            (32, 1, 0)         # 0x20 Pause (1ms)
        ]
        tzxfile = self._write_tzx(blocks)
        exp_output = [
            'Bytes: trailingtn',
            'Fast loading data block: 32768,1',
            'Tape finished',
            'Simulation stopped (PC at start address): PC=7030',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 7030 {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_trailing_tone_read_by_rom(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = [0x56, 0x05] # $FF50 DEFW $0556
        blocks = [
            create_tzx_header_block('trtone-rom', code_start, len(code)),
            create_tzx_data_block(code),
            (18, 120, 8, 151, 12), # 0x12 Pure Tone (3223x2168 T-states)
        ]
        tzxfile = self._write_tzx(blocks)
        exp_output = [
            'Bytes: trtone-rom',
            'Fast loading data block: 65360,2',
            '[ 78.1%]\x08\x08\x08\x08\x08\x08\x08\x08Simulation stopped (PC at start address): PC=1393',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 1393 {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))

    @patch.object(tap2sna, 'write_snapshot', null_write_snapshot)
    def test_trailing_tone_read_by_custom_rom_tsl(self):
        # A code start address of 0xFF50 ensures that the return address on the
        # stack will be overwritten by the first two bytes of the code block
        code_start = 0xFF50
        code = [
            0x52, 0xFF,       # $FF50 DEFW $FF52
            0xF3,             # $FF52 DI
            0x01, 0x00, 0xFC, # $FF53 LD BC,$FC00
            0xC5,             # $FF56 PUSH BC
            0x04,             # $FF57 INC B
            0xC8,             # $FF58 RET Z       ; To $FC00
            0x3E, 0xFE,       # $FF59 LD A,$FE
            0xDB, 0xFE,       # $FF5B IN A,($FE)
            0x1F,             # $FF5D RRA
            0xD0,             # $FF5E RET NC
            0xA9,             # $FF5F XOR C
            0xE6, 0x20,       # $FF60 AND $20
            0x28, 0xF3,       # $FF62 JR Z,$FF57
            0x18, 0xF1,       # $FF64 JR $FF57
        ]
        blocks = [
            create_tzx_header_block('toneromtsl', code_start, len(code)),
            create_tzx_data_block(code),
            (18, 100, 0, 3, 0), # 0x12 Pure Tone (3x100 T-states)
        ]
        tzxfile = self._write_tzx(blocks)
        exp_output = [
            'Bytes: toneromtsl',
            'Fast loading data block: 65360,22',
            '[100.0%]\x08\x08\x08\x08\x08\x08\x08\x08Simulation stopped (PC at start address): PC=64512',
            'Accelerators: rom: 1; misses: 0; dec-a: 0/0/23',
            'Writing out.z80'
        ]
        output, error = self.run_tap2sna(f'--start 0xfc00 -c accelerator=list {tzxfile} out.z80')
        self.assertEqual(error, '')
        self.assertEqual(exp_output, output.strip().split('\n'))
