"""Tests for SWD/DAP, breakpoints, RTT, and transport mixin tools."""

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock

import pytest
from openocd.errors import OpenOCDError

from mcjtag.mixins.breakpoints import BreakpointMixin
from mcjtag.mixins.rtt import RTTMixin
from mcjtag.mixins.swd import SWDMixin
from mcjtag.mixins.transport import TransportMixin
from mcjtag.state import SessionState

# ── Fake Context harness ──────────────────────────────────────────────


def _make_ctx(state: SessionState):
    ctx = MagicMock()
    ctx.request_context.lifespan_context = state
    ctx.report_progress = AsyncMock()
    return ctx


def _make_session():
    """Build a mock Session with subsystems for new tools."""
    session = MagicMock()

    # SWD/DAP subsystem
    session.swd = MagicMock()
    session.swd.info = AsyncMock(
        return_value=SimpleNamespace(
            name="stm32f1x.dap",
            dpidr=0x2BA01477,
            ap_count=1,
            raw_info="DPIDR: 0x2ba01477\nAP # 0",
        )
    )
    session.swd.list_aps = AsyncMock(
        return_value=[
            SimpleNamespace(index=0, idr=0x04770031, base=0xE00FF003, ap_type="MEM-AP"),
        ]
    )
    session.swd.dpreg = AsyncMock(return_value=0x2BA01477)
    session.swd.apreg = AsyncMock(return_value=0x04770031)

    # Breakpoints subsystem
    session.breakpoints = MagicMock()
    session.breakpoints.add = AsyncMock()
    session.breakpoints.remove = AsyncMock()
    session.breakpoints.list = AsyncMock(
        return_value=[
            SimpleNamespace(number=0, type="hw", address=0x08001234, length=2, enabled=True),
        ]
    )
    session.breakpoints.add_watchpoint = AsyncMock()
    session.breakpoints.remove_watchpoint = AsyncMock()
    session.breakpoints.list_watchpoints = AsyncMock(
        return_value=[
            SimpleNamespace(number=0, address=0x20001000, length=4, access="rw"),
        ]
    )

    # RTT subsystem
    session.rtt = MagicMock()
    session.rtt.setup = AsyncMock()
    session.rtt.start = AsyncMock()
    session.rtt.stop = AsyncMock()
    session.rtt.channels = AsyncMock(
        return_value=[
            SimpleNamespace(index=0, name="Terminal", size=1024, direction="up"),
            SimpleNamespace(index=0, name="Terminal", size=16, direction="down"),
        ]
    )
    session.rtt.read = AsyncMock(return_value="Hello from target\n")
    session.rtt.write = AsyncMock()

    # Transport subsystem
    session.transport = MagicMock()
    session.transport.select = AsyncMock(return_value="swd")
    session.transport.list = AsyncMock(return_value=["jtag", "swd"])
    session.transport.adapter_info = AsyncMock(return_value="cmsis-dap")
    session.transport.adapter_speed = AsyncMock(return_value=1000)

    return session


# ── SWD/DAP tools ────────────────────────────────────────────────────


class TestSWDTools:
    async def test_swd_info(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_info(_make_ctx(state))
        assert result.name == "stm32f1x.dap"
        assert result.dpidr == "0x2ba01477"
        assert result.ap_count == 1

    async def test_swd_info_requires_session(self):
        state = SessionState()
        mixin = SWDMixin()
        with pytest.raises(OpenOCDError, match="Not connected"):
            await mixin.swd_info(_make_ctx(state))

    async def test_swd_list_aps(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_list_aps(_make_ctx(state))
        assert result.count == 1
        assert result.aps[0].ap_type == "MEM-AP"
        assert result.aps[0].idr == "0x04770031"

    async def test_swd_read_dp(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_read_dp(_make_ctx(state), address="0x0")
        assert result.value == "0x2ba01477"
        assert result.written is False
        state.session.swd.dpreg.assert_called_once_with(0x0, dap=None)

    async def test_swd_write_dp(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_write_dp(_make_ctx(state), address="0x4", value="0x50000000")
        assert result.written is True
        assert result.value == "0x50000000"
        state.session.swd.dpreg.assert_called_once_with(0x4, 0x50000000, dap=None)

    async def test_swd_read_ap(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_read_ap(_make_ctx(state), ap=0, address="0xFC")
        assert result.value == "0x04770031"
        assert result.ap == 0
        assert result.written is False

    async def test_swd_write_ap(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        result = await mixin.swd_write_ap(
            _make_ctx(state), ap=0, address="0x00", value="0x23000052"
        )
        assert result.written is True
        state.session.swd.apreg.assert_called_once_with(0, 0x00, 0x23000052, dap=None)

    async def test_swd_read_ap_invalid_ap_number(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        with pytest.raises(OpenOCDError, match="AP number must be 0-255"):
            await mixin.swd_read_ap(_make_ctx(state), ap=256, address="0xFC")

    async def test_swd_read_dp_bad_hex(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        with pytest.raises(OpenOCDError, match="Invalid hex"):
            await mixin.swd_read_dp(_make_ctx(state), address="not_hex")

    async def test_swd_info_with_explicit_dap(self):
        state = SessionState(session=_make_session())
        mixin = SWDMixin()
        await mixin.swd_info(_make_ctx(state), dap="stm32h7xx.dap0")
        state.session.swd.info.assert_called_once_with(dap="stm32h7xx.dap0")


# ── Breakpoint tools ─────────────────────────────────────────────────


class TestBreakpointTools:
    async def test_breakpoint_add(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.breakpoint_add(_make_ctx(state), address="0x08001234")
        assert result.success is True
        assert result.action == "add"
        state.session.breakpoints.add.assert_called_once_with(0x08001234, length=2, hw=False)

    async def test_breakpoint_add_hw(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        await mixin.breakpoint_add(_make_ctx(state), address="0x08001234", length=4, hw=True)
        state.session.breakpoints.add.assert_called_once_with(0x08001234, length=4, hw=True)

    async def test_breakpoint_add_odd_address_rejected(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        with pytest.raises(OpenOCDError, match="not 2-byte aligned"):
            await mixin.breakpoint_add(_make_ctx(state), address="0x08001235")

    async def test_breakpoint_add_bad_length(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        with pytest.raises(OpenOCDError, match="must be 2.*or 4"):
            await mixin.breakpoint_add(_make_ctx(state), address="0x08001234", length=3)

    async def test_breakpoint_remove(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.breakpoint_remove(_make_ctx(state), address="0x08001234")
        assert result.action == "remove"
        assert result.success is True

    async def test_breakpoint_list(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.breakpoint_list(_make_ctx(state))
        assert result.count == 1
        assert result.breakpoints[0].type == "hw"
        assert result.breakpoints[0].address == "0x08001234"

    async def test_breakpoint_requires_session(self):
        state = SessionState()
        mixin = BreakpointMixin()
        with pytest.raises(OpenOCDError, match="Not connected"):
            await mixin.breakpoint_list(_make_ctx(state))

    async def test_watchpoint_add(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.watchpoint_add(
            _make_ctx(state), address="0x20001000", length=4, access="w"
        )
        assert result.success is True
        state.session.breakpoints.add_watchpoint.assert_called_once_with(0x20001000, 4, access="w")

    async def test_watchpoint_add_zero_length_rejected(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        with pytest.raises(OpenOCDError, match="must be positive"):
            await mixin.watchpoint_add(_make_ctx(state), address="0x20001000", length=0)

    async def test_watchpoint_remove(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.watchpoint_remove(_make_ctx(state), address="0x20001000")
        assert result.action == "remove"

    async def test_watchpoint_list(self):
        state = SessionState(session=_make_session())
        mixin = BreakpointMixin()
        result = await mixin.watchpoint_list(_make_ctx(state))
        assert result.count == 1
        assert result.watchpoints[0].access == "rw"
        assert result.watchpoints[0].address == "0x20001000"


# ── RTT tools ────────────────────────────────────────────────────────


class TestRTTTools:
    async def test_rtt_setup(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_setup(_make_ctx(state), address="0x20000000", size="0x1000")
        assert result.success is True
        assert result.address == "0x20000000"
        state.session.rtt.setup.assert_called_once_with(0x20000000, 0x1000, id_string="SEGGER RTT")

    async def test_rtt_start(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_start(_make_ctx(state))
        assert result.action == "start"
        assert result.success is True

    async def test_rtt_stop(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_stop(_make_ctx(state))
        assert result.action == "stop"
        assert result.success is True

    async def test_rtt_channels(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_channels(_make_ctx(state))
        assert result.count == 2
        assert result.channels[0].name == "Terminal"
        assert result.channels[0].direction == "up"
        assert result.channels[1].direction == "down"

    async def test_rtt_read(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_read(_make_ctx(state), channel=0)
        assert result.data == "Hello from target\n"
        assert result.channel == 0

    async def test_rtt_write(self):
        state = SessionState(session=_make_session())
        mixin = RTTMixin()
        result = await mixin.rtt_write(_make_ctx(state), data="test\n", channel=0)
        assert result.success is True
        assert result.length == 5
        state.session.rtt.write.assert_called_once_with(0, "test\n")

    async def test_rtt_requires_session(self):
        state = SessionState()
        mixin = RTTMixin()
        with pytest.raises(OpenOCDError, match="Not connected"):
            await mixin.rtt_channels(_make_ctx(state))


# ── Transport tools ──────────────────────────────────────────────────


class TestTransportTools:
    async def test_transport_info(self):
        state = SessionState(session=_make_session())
        mixin = TransportMixin()
        result = await mixin.transport_info(_make_ctx(state))
        assert result.transport == "swd"
        assert result.adapter == "cmsis-dap"
        assert result.speed_khz == 1000
        assert "jtag" in result.available_transports
        assert "swd" in result.available_transports

    async def test_adapter_speed_read(self):
        state = SessionState(session=_make_session())
        mixin = TransportMixin()
        result = await mixin.adapter_speed(_make_ctx(state))
        assert result.speed_khz == 1000
        assert result.changed is False

    async def test_adapter_speed_set(self):
        state = SessionState(session=_make_session())
        state.session.transport.adapter_speed = AsyncMock(return_value=4000)
        mixin = TransportMixin()
        result = await mixin.adapter_speed(_make_ctx(state), khz=4000)
        assert result.speed_khz == 4000
        assert result.changed is True
        state.session.transport.adapter_speed.assert_called_once_with(4000)

    async def test_transport_requires_session(self):
        state = SessionState()
        mixin = TransportMixin()
        with pytest.raises(OpenOCDError, match="Not connected"):
            await mixin.transport_info(_make_ctx(state))

    async def test_transport_info_speed_failure_handled(self):
        """If adapter speed read fails, speed_khz should be None."""
        state = SessionState(session=_make_session())
        state.session.transport.adapter_speed = AsyncMock(
            side_effect=Exception("speed not supported")
        )
        mixin = TransportMixin()
        result = await mixin.transport_info(_make_ctx(state))
        assert result.speed_khz is None
        assert result.transport == "swd"
