"""Unit tests for the embodied BF interpreter."""

import pytest
from abiogenesis.interpreter import (
    execute, execute_traced, _build_bracket_map,
    OP_RIGHT, OP_LEFT, OP_INC, OP_DEC, OP_COPY, OP_OPEN, OP_CLOSE,
)


def make_tape(instructions: list[int], length: int = 32, fill: int = 0) -> bytearray:
    """Build a tape with instructions at the start, rest filled."""
    tape = bytearray([fill] * length)
    for i, inst in enumerate(instructions):
        if i < length:
            tape[i] = inst
    return tape


class TestBracketMap:
    def test_simple_pair(self):
        tape = make_tape([OP_OPEN, OP_CLOSE])
        bm = _build_bracket_map(tape)
        assert bm[0] == 1
        assert bm[1] == 0

    def test_nested(self):
        tape = make_tape([OP_OPEN, OP_OPEN, OP_CLOSE, OP_CLOSE])
        bm = _build_bracket_map(tape)
        assert bm[0] == 3
        assert bm[1] == 2
        assert bm[2] == 1
        assert bm[3] == 0

    def test_unmatched_open_ignored(self):
        tape = make_tape([OP_OPEN])
        bm = _build_bracket_map(tape)
        assert bm == {}  # unmatched [ not in map, becomes no-op

    def test_unmatched_close_ignored(self):
        tape = make_tape([OP_CLOSE])
        bm = _build_bracket_map(tape)
        assert bm == {}  # unmatched ] not in map, becomes no-op

    def test_partial_match(self):
        # [ [ ] — inner pair matches, outer [ is orphaned
        tape = make_tape([OP_OPEN, OP_OPEN, OP_CLOSE])
        bm = _build_bracket_map(tape)
        assert bm[1] == 2
        assert bm[2] == 1
        assert 0 not in bm  # outer [ unmatched, excluded

    def test_no_brackets(self):
        tape = make_tape([OP_INC, OP_DEC])
        bm = _build_bracket_map(tape)
        assert bm == {}


class TestExecute:
    def test_empty_tape(self):
        tape, ops = execute(bytearray(), max_steps=100)
        assert tape == bytearray()
        assert ops == 0

    def test_increment(self):
        # DP at position 10, away from instructions
        tape = make_tape([OP_INC, OP_INC, OP_INC], length=32)
        tape, ops = execute(tape, max_steps=100, dp_offset=10)
        assert ops == 3
        assert tape[10] == 3

    def test_decrement_wraps(self):
        tape = make_tape([OP_DEC], length=16)
        tape, ops = execute(tape, max_steps=100, dp_offset=8)
        assert ops == 1
        assert tape[8] == 255  # 0 - 1 wraps to 255

    def test_move_right_wraps(self):
        # DP starts at position 6 in a length-8 tape
        # Two rights: 6 → 7 → 0 (wraps). Then INC tape[0].
        length = 8
        tape = make_tape([OP_RIGHT, OP_RIGHT, OP_INC], length=length)
        tape, ops = execute(tape, max_steps=100, dp_offset=6)
        assert ops == 3
        # DP wrapped to 0, INC'd tape[0] which was OP_RIGHT (62) → 63
        assert tape[0] == 63

    def test_move_left_wraps(self):
        length = 8
        tape = make_tape([OP_LEFT, OP_INC], length=length)
        tape, ops = execute(tape, max_steps=100, dp_offset=0)
        # DP goes from 0 → 7 (wrap), then INC tape[7]
        assert ops == 2
        assert tape[7] == 1  # was 0, now 1

    def test_copy_instruction(self):
        # Put data at DP, copy to WP
        tape = bytearray(16)
        tape[0] = OP_COPY  # instruction at IP=0
        tape[4] = 42       # data at DP=4
        tape, ops = execute(tape, max_steps=100, dp_offset=4, wp_offset=8)
        assert ops == 1
        assert tape[8] == 42  # copied from DP to WP

    def test_copy_advances_wp(self):
        tape = bytearray(16)
        tape[0] = OP_COPY
        tape[1] = OP_COPY
        tape[4] = 42
        tape, ops = execute(tape, max_steps=100, dp_offset=4, wp_offset=8)
        assert ops == 2
        assert tape[8] == 42
        assert tape[9] == 42  # WP advanced

    def test_simple_loop(self):
        # [+-] with tape[DP] == 0 → skip loop entirely
        tape = make_tape([OP_OPEN, OP_INC, OP_CLOSE], length=16)
        tape, ops = execute(tape, max_steps=100, dp_offset=8)
        # tape[8] starts as 0, so [ jumps past ]
        assert ops == 1  # only the [ is executed (it skips)

    def test_loop_executes(self):
        # Set tape[DP] = 3, then loop [-] decrements to 0
        tape = bytearray(16)
        tape[0] = OP_OPEN   # [
        tape[1] = OP_DEC    # -
        tape[2] = OP_CLOSE  # ]
        tape[4] = 3          # DP=4 starts at 3
        tape, ops = execute(tape, max_steps=100, dp_offset=4)
        assert tape[4] == 0
        # 3 iterations of [-]: [ - ] × 3 = 9 ops
        # (] on last iteration sees 0 and falls through)
        assert ops == 9

    def test_max_steps_limits(self):
        # Infinite loop: [+] where tape[DP] is always nonzero
        tape = bytearray(16)
        tape[0] = OP_OPEN
        tape[1] = OP_INC
        tape[2] = OP_CLOSE
        tape[4] = 1  # DP=4, nonzero
        tape, ops = execute(tape, max_steps=50, dp_offset=4)
        assert ops == 50

    def test_noop_bytes_skipped(self):
        # Bytes that aren't instructions are just skipped
        tape = bytearray(16)
        tape[0] = 0xFF  # noop
        tape[1] = 0x01  # noop
        tape[2] = OP_INC
        tape, ops = execute(tape, max_steps=100, dp_offset=5)
        assert ops == 1  # only the INC counted
        assert tape[5] == 1

    def test_unbalanced_brackets_partial_execution(self):
        # Unmatched [ is skipped as no-op, but INC still runs
        tape = bytearray(8)
        tape[0] = OP_OPEN  # unmatched — becomes no-op
        tape[1] = OP_INC
        tape, ops = execute(tape, max_steps=100, dp_offset=4)
        assert ops == 1  # only INC executes, unmatched [ skipped
        assert tape[4] == 1

    def test_deterministic(self):
        tape1 = bytearray(b'\x3e\x3c\x2b\x2d\x2e\x00' * 5 + b'\x00\x00')
        tape2 = bytearray(tape1)
        r1 = execute(tape1, max_steps=100, dp_offset=10, wp_offset=20)
        r2 = execute(tape2, max_steps=100, dp_offset=10, wp_offset=20)
        assert r1[0] == r2[0]
        assert r1[1] == r2[1]


class TestExecuteTraced:
    def test_tracks_executed_positions(self):
        tape = bytearray(16)
        tape[0] = OP_INC
        tape[1] = OP_DEC
        tape, ops, executed, written = execute_traced(
            tape, max_steps=100, dp_offset=4, wp_offset=8
        )
        assert 0 in executed
        assert 1 in executed
        assert len(written) == 0

    def test_tracks_written_positions(self):
        tape = bytearray(16)
        tape[0] = OP_COPY
        tape[4] = 99
        tape, ops, executed, written = execute_traced(
            tape, max_steps=100, dp_offset=4, wp_offset=8
        )
        assert 0 in executed
        assert 8 in written
        assert tape[8] == 99


class TestSelfReplication:
    """Test that the copy mechanism can actually replicate code."""

    def test_simple_copy_sequence(self):
        """A sequence that copies bytes from one region to another."""
        # Set up a tape where code at the start copies data from DP region
        # to WP region using a series of . > instructions
        tape = bytearray(32)
        # Code: copy, move DP right, copy, move DP right, copy
        tape[0] = OP_COPY   # copy tape[DP] → tape[WP]
        tape[1] = OP_RIGHT  # DP++
        tape[2] = OP_COPY   # copy tape[DP] → tape[WP]
        tape[3] = OP_RIGHT  # DP++
        tape[4] = OP_COPY   # copy tape[DP] → tape[WP]

        # Put data at DP start position
        dp_start = 10
        tape[10] = 0xAA
        tape[11] = 0xBB
        tape[12] = 0xCC

        wp_start = 20
        tape, ops = execute(tape, max_steps=100, dp_offset=dp_start, wp_offset=wp_start)

        assert ops == 5
        assert tape[20] == 0xAA
        assert tape[21] == 0xBB
        assert tape[22] == 0xCC
