"""Tests for mcjtag.mixins.helpers — pure validation and formatting logic."""

import os
import struct

import pytest
from openocd.errors import OpenOCDError

from mcjtag.mixins.helpers import (
    check_write_address,
    format_hexdump,
    get_safe_write_ranges,
    parse_hex,
    require_session,
    values_to_bytes,
)
from mcjtag.state import SessionState

# ── parse_hex ──────────────────────────────────────────────────────────


class TestParseHex:
    def test_standard_0x_prefix(self):
        assert parse_hex("0x08000000") == 0x08000000

    def test_uppercase_0X_prefix(self):
        assert parse_hex("0X20000000") == 0x20000000

    def test_no_prefix(self):
        assert parse_hex("DEADBEEF") == 0xDEADBEEF

    def test_lowercase(self):
        assert parse_hex("0xdeadbeef") == 0xDEADBEEF

    def test_zero(self):
        assert parse_hex("0x0") == 0

    def test_strips_whitespace(self):
        assert parse_hex("  0xFF  ") == 255

    def test_rejects_decimal(self):
        with pytest.raises(OpenOCDError, match="Invalid hex"):
            parse_hex("12345xyz")

    def test_rejects_empty(self):
        with pytest.raises(OpenOCDError, match="Invalid hex"):
            parse_hex("")

    def test_rejects_float(self):
        with pytest.raises(OpenOCDError, match="Invalid hex"):
            parse_hex("3.14")

    def test_custom_name_in_error(self):
        with pytest.raises(OpenOCDError, match="register value"):
            parse_hex("not_hex", "register value")

    def test_rejects_negative(self):
        with pytest.raises(OpenOCDError, match="Negative"):
            parse_hex("-1")

    def test_rejects_negative_custom_name(self):
        with pytest.raises(OpenOCDError, match="Negative value"):
            parse_hex("-FF", "value")


# ── require_session ────────────────────────────────────────────────────


class TestRequireSession:
    def test_raises_when_no_session(self):
        state = SessionState()
        with pytest.raises(OpenOCDError, match="Not connected"):
            require_session(state)

    def test_passes_with_session(self):
        state = SessionState()
        state.session = object()  # Any truthy value
        require_session(state)  # Should not raise


# ── get_safe_write_ranges ─────────────────────────────────────────────


class TestSafeWriteRanges:
    def test_defaults_to_sram(self):
        # Ensure env var is not set
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        ranges = get_safe_write_ranges()
        assert len(ranges) == 1
        start, end = ranges[0]
        assert start == 0x20000000
        assert end == 0x20100000

    def test_none_means_unrestricted(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "none")
        assert get_safe_write_ranges() == []

    def test_none_case_insensitive(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "  None  ")
        assert get_safe_write_ranges() == []

    def test_custom_single_range(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "0x10000000-0x10010000")
        ranges = get_safe_write_ranges()
        assert ranges == [(0x10000000, 0x10010000)]

    def test_custom_multiple_ranges(self, monkeypatch):
        monkeypatch.setenv(
            "MCJTAG_SAFE_WRITE_RANGES",
            "0x20000000-0x20100000,0x10000000-0x10010000",
        )
        ranges = get_safe_write_ranges()
        assert len(ranges) == 2
        assert (0x20000000, 0x20100000) in ranges
        assert (0x10000000, 0x10010000) in ranges

    def test_invalid_entries_skipped(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "garbage,0x20000000-0x20100000")
        ranges = get_safe_write_ranges()
        assert len(ranges) == 1

    def test_empty_string_returns_empty(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "")
        assert get_safe_write_ranges() == []


# ── check_write_address ───────────────────────────────────────────────


class TestCheckWriteAddress:
    def test_sram_write_allowed(self):
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        # Should not raise — 0x20000000 is in default SRAM range
        check_write_address(0x20000000, 4)

    def test_sram_end_boundary(self):
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        # Write 4 bytes ending exactly at 0x20100000
        check_write_address(0x200FFFFC, 4)

    def test_flash_write_blocked(self):
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        with pytest.raises(OpenOCDError, match="outside safe write ranges"):
            check_write_address(0x08000000, 4)

    def test_peripheral_write_blocked(self):
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        with pytest.raises(OpenOCDError, match="outside safe write ranges"):
            check_write_address(0x40000000, 4)

    def test_write_spanning_boundary_blocked(self):
        os.environ.pop("MCJTAG_SAFE_WRITE_RANGES", None)
        # Starts in SRAM but extends past the end
        with pytest.raises(OpenOCDError, match="outside safe write ranges"):
            check_write_address(0x200FFFFE, 4)

    def test_unrestricted_mode(self, monkeypatch):
        monkeypatch.setenv("MCJTAG_SAFE_WRITE_RANGES", "none")
        # Should not raise — any address is fine
        check_write_address(0x08000000, 1024)
        check_write_address(0x00000000, 4)


# ── values_to_bytes ───────────────────────────────────────────────────


class TestValuesToBytes:
    def test_u8(self):
        result = values_to_bytes([0x41, 0x42, 0x43], 8)
        assert result == b"ABC"

    def test_u16_little_endian(self):
        result = values_to_bytes([0x0102], 16)
        assert result == struct.pack("<H", 0x0102)

    def test_u32_little_endian(self):
        result = values_to_bytes([0xDEADBEEF], 32)
        assert result == struct.pack("<I", 0xDEADBEEF)

    def test_u64_little_endian(self):
        result = values_to_bytes([0x0102030405060708], 64)
        assert result == struct.pack("<Q", 0x0102030405060708)

    def test_multiple_u32(self):
        result = values_to_bytes([0x00000001, 0x00000002], 32)
        assert result == struct.pack("<II", 1, 2)

    def test_empty_list(self):
        result = values_to_bytes([], 32)
        assert result == b""

    def test_unsupported_width(self):
        with pytest.raises(OpenOCDError, match="Unsupported width"):
            values_to_bytes([1], 24)


# ── format_hexdump ────────────────────────────────────────────────────


class TestFormatHexdump:
    def test_single_line_16_bytes(self):
        data = bytes(range(16))
        result = format_hexdump(0x08000000, data)
        assert "08000000:" in result
        assert "00 01 02 03" in result
        # ASCII should show dots for control chars, printable where applicable
        assert "|" in result

    def test_printable_ascii(self):
        data = b"Hello, World!123"
        result = format_hexdump(0x20000000, data)
        assert "|Hello, World!123|" in result

    def test_non_printable_becomes_dot(self):
        data = bytes([0x00, 0x01, 0xFF])
        result = format_hexdump(0, data)
        assert "...", result.split("|")[1]

    def test_multi_line(self):
        data = bytes(range(32))
        result = format_hexdump(0x08000000, data)
        lines = result.strip().split("\n")
        assert len(lines) == 2
        assert "08000000:" in lines[0]
        assert "08000010:" in lines[1]

    def test_partial_last_line(self):
        data = bytes(range(20))  # 16 + 4 bytes
        result = format_hexdump(0, data)
        lines = result.strip().split("\n")
        assert len(lines) == 2

    def test_empty_data(self):
        result = format_hexdump(0, b"")
        assert result == ""

    def test_gap_between_byte_7_and_8(self):
        # The hexdump should insert an extra space after byte 7 for readability
        data = bytes(range(16))
        result = format_hexdump(0, data)
        # Bytes 0-7 then gap then bytes 8-15
        assert "07  08" in result
