"""
Tests for the SHDB (Simple Hardware Debugger) module.

This test file verifies:
1. DebugInfo loading and parsing
2. SymbolTable lookups and completion
3. DebugController simulation control
4. Breakpoints and watchpoints
5. Internal gate access via peek_gate
"""

import pytest
import subprocess
import json
from pathlib import Path
from typing import Optional

from SHDL.debugger import (
    SHDBCircuit,
    DebugInfo,
    SymbolTable,
    SignalRef,
    SignalType,
    SourceMap,
    SourceLocation,
    DebugController,
)
from SHDL.debugger.controller import (
    Breakpoint,
    Watchpoint,
    BreakpointType,
    StopReason,
    StopInfo,
)


# =============================================================================
# Fixtures
# =============================================================================

@pytest.fixture(scope="module")
def compiled_full_adder(tmp_path_factory) -> tuple[Path, Path]:
    """
    Compile fullAdder.shdl with debug info and return paths.
    
    Returns:
        (library_path, shdb_path)
    """
    tmp_dir = tmp_path_factory.mktemp("debug_test")
    lib_path = tmp_dir / "libfulladder_debug.dylib"
    shdb_path = tmp_dir / "libfulladder_debug.shdb"
    
    # Compile with debug info
    result = subprocess.run(
        [
            "uv", "run", "python", "-m", "SHDL.compiler.cli",
            "tests/circuits/test_half_full_adder.shdl",
            "--flatten",
            "--component", "FullAdder",
            "-g", "-c",
            "-o", str(lib_path),
        ],
        cwd=Path(__file__).parent.parent,
        capture_output=True,
        text=True,
    )
    
    if result.returncode != 0:
        pytest.fail(f"Compilation failed: {result.stderr}")
    
    if not lib_path.exists():
        pytest.fail(f"Library not created: {lib_path}")
    
    if not shdb_path.exists():
        pytest.fail(f"Debug info not created: {shdb_path}")
    
    return lib_path, shdb_path


@pytest.fixture
def debug_info(compiled_full_adder) -> DebugInfo:
    """Load debug info from compiled fullAdder."""
    _, shdb_path = compiled_full_adder
    return DebugInfo.load(shdb_path)


@pytest.fixture
def symbol_table(debug_info) -> SymbolTable:
    """Create a symbol table from debug info."""
    return SymbolTable(debug_info)


@pytest.fixture
def controller(compiled_full_adder, debug_info) -> DebugController:
    """Create a debug controller for testing."""
    lib_path, _ = compiled_full_adder
    ctrl = DebugController(lib_path, debug_info)
    ctrl.reset()
    yield ctrl
    # Cleanup handled by context manager/ctypes


# =============================================================================
# Test 1: DebugInfo Loading
# =============================================================================

class TestDebugInfoLoading:
    """Tests for loading and parsing .shdb files."""
    
    def test_load_shdb_file(self, debug_info):
        """Test that .shdb file loads correctly."""
        assert debug_info is not None
        assert debug_info.version == "1.0"
    
    def test_component_name(self, debug_info):
        """Test component name is parsed correctly."""
        assert debug_info.component == "FullAdder"
    
    def test_inputs_parsed(self, debug_info):
        """Test input ports are parsed correctly."""
        assert len(debug_info.inputs) == 3
        
        input_names = {p.name for p in debug_info.inputs}
        assert input_names == {"A", "B", "Cin"}
    
    def test_outputs_parsed(self, debug_info):
        """Test output ports are parsed correctly."""
        assert len(debug_info.outputs) == 2
        
        output_names = {p.name for p in debug_info.outputs}
        assert output_names == {"Sum", "Cout"}
    
    def test_gates_parsed(self, debug_info):
        """Test gates are parsed correctly."""
        assert len(debug_info.gates) == 5
        
        gate_names = set(debug_info.gates.keys())
        assert gate_names == {"x1", "x2", "a1", "a2", "o1"}
    
    def test_gate_types(self, debug_info):
        """Test gate types are correct."""
        assert debug_info.gates["x1"].gate_type == "XOR"
        assert debug_info.gates["x2"].gate_type == "XOR"
        assert debug_info.gates["a1"].gate_type == "AND"
        assert debug_info.gates["a2"].gate_type == "AND"
        assert debug_info.gates["o1"].gate_type == "OR"
    
    def test_gate_hierarchy_path(self, debug_info):
        """Test gate hierarchy paths are set correctly."""
        assert debug_info.gates["x1"].hierarchy_path == "FullAdder/x1"
        assert debug_info.gates["a1"].hierarchy_path == "FullAdder/a1"
    
    def test_gate_lane_and_chunk(self, debug_info):
        """Test gate lane and chunk are set correctly."""
        # XOR gates should be in lanes 0 and 1 of chunk 0
        assert debug_info.gates["x1"].lane == 0
        assert debug_info.gates["x1"].chunk == 0
        assert debug_info.gates["x2"].lane == 1
        assert debug_info.gates["x2"].chunk == 0
    
    def test_get_input(self, debug_info):
        """Test get_input helper method."""
        a_port = debug_info.get_input("A")
        assert a_port is not None
        assert a_port.name == "A"
        assert a_port.width == 1
        
        assert debug_info.get_input("nonexistent") is None
    
    def test_get_output(self, debug_info):
        """Test get_output helper method."""
        sum_port = debug_info.get_output("Sum")
        assert sum_port is not None
        assert sum_port.name == "Sum"
        assert sum_port.width == 1
        
        assert debug_info.get_output("nonexistent") is None
    
    def test_get_gate(self, debug_info):
        """Test get_gate helper method."""
        x1_gate = debug_info.get_gate("x1")
        assert x1_gate is not None
        assert x1_gate.name == "x1"
        assert x1_gate.gate_type == "XOR"
        
        assert debug_info.get_gate("nonexistent") is None
    
    def test_num_gates(self, debug_info):
        """Test num_gates property."""
        assert debug_info.num_gates == 5
    
    def test_gate_counts(self, debug_info):
        """Test gate_counts property."""
        counts = debug_info.gate_counts
        assert counts["XOR"] == 2
        assert counts["AND"] == 2
        assert counts["OR"] == 1
    
    def test_to_json_roundtrip(self, debug_info):
        """Test JSON serialization roundtrip."""
        json_str = debug_info.to_json()
        data = json.loads(json_str)
        reloaded = DebugInfo.from_dict(data)
        
        assert reloaded.component == debug_info.component
        assert len(reloaded.inputs) == len(debug_info.inputs)
        assert len(reloaded.outputs) == len(debug_info.outputs)
        assert len(reloaded.gates) == len(debug_info.gates)


# =============================================================================
# Test 2: SymbolTable
# =============================================================================

class TestSymbolTable:
    """Tests for symbol table lookups and completion."""
    
    def test_lookup_input_port(self, symbol_table):
        """Test looking up an input port."""
        ref = symbol_table.resolve("A")
        assert ref is not None
        assert ref.signal_type == SignalType.INPUT_PORT
        assert ref.name == "A"
        assert ref.width == 1
    
    def test_lookup_output_port(self, symbol_table):
        """Test looking up an output port."""
        ref = symbol_table.resolve("Sum")
        assert ref is not None
        assert ref.signal_type == SignalType.OUTPUT_PORT
        assert ref.name == "Sum"
    
    def test_lookup_gate(self, symbol_table):
        """Test looking up a gate."""
        ref = symbol_table.resolve("x1")
        assert ref is not None
        assert ref.signal_type == SignalType.GATE_OUTPUT
        assert ref.gate_name == "x1"
    
    def test_lookup_gate_with_port(self, symbol_table):
        """Test looking up a gate with .O port."""
        ref = symbol_table.resolve("x1.O")
        assert ref is not None
        assert ref.signal_type == SignalType.GATE_OUTPUT
        assert ref.gate_port == "O"
    
    def test_lookup_nonexistent(self, symbol_table):
        """Test looking up a nonexistent signal."""
        ref = symbol_table.resolve("nonexistent")
        assert ref is None
    
    def test_completions_empty_prefix(self, symbol_table):
        """Test completions with empty prefix."""
        completions = symbol_table.get_completions("")
        # Should include inputs, outputs, and gates
        assert "A" in completions
        assert "Sum" in completions
        assert "x1" in completions
    
    def test_completions_partial_prefix(self, symbol_table):
        """Test completions with partial prefix."""
        completions = symbol_table.get_completions("x")
        assert "x1" in completions
        assert "x2" in completions
        assert "a1" not in completions
    
    def test_completions_a_prefix(self, symbol_table):
        """Test completions starting with 'a'."""
        completions = symbol_table.get_completions("a")
        assert "a1" in completions
        assert "a2" in completions
    
    def test_get_all_signals(self, symbol_table):
        """Test getting all signal names."""
        signals = symbol_table.get_all_signals()
        assert "A" in signals
        assert "B" in signals
        assert "Cin" in signals
        assert "Sum" in signals
        assert "Cout" in signals
        assert "x1" in signals
        assert "x2" in signals
        assert "a1" in signals
        assert "a2" in signals
        assert "o1" in signals
    
    def test_current_scope_at_root(self, symbol_table):
        """Test current scope at root level."""
        assert symbol_table.current_scope == "FullAdder"
    
    def test_scope_prefix_at_root(self, symbol_table):
        """Test scope prefix at root level."""
        assert symbol_table.scope_prefix == ""


# =============================================================================
# Test 3: DebugController Basic Operations
# =============================================================================

class TestDebugControllerBasic:
    """Tests for basic DebugController operations."""
    
    def test_reset(self, controller):
        """Test reset clears state."""
        controller.poke("A", 1)
        controller.step()
        controller.reset()
        
        assert controller.cycle == 0
    
    def test_poke_input(self, controller):
        """Test poking input values."""
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        
        # Values should be readable after step
        controller.step()
    
    def test_peek_output(self, controller):
        """Test peeking output values."""
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.poke("Cin", 0)
        controller.step()
        
        sum_val = controller.peek("Sum")
        cout_val = controller.peek("Cout")
        
        # 0 + 0 + 0 = 0 with carry 0
        assert sum_val == 0
        assert cout_val == 0
    
    def test_full_adder_truth_table(self, controller):
        """Test full adder truth table.
        
        Note: The SHDL simulator uses a gate-level propagation model where
        each gate type processes in sequence. For the full adder:
        - Step 1: XOR and AND gates compute from inputs
        - Step 2: OR gate computes from AND outputs
        
        So we need 2 steps for full propagation.
        """
        # Truth table for full adder:
        # A B Cin | Sum Cout
        # 0 0 0   | 0   0
        # 0 0 1   | 1   0
        # 0 1 0   | 1   0
        # 0 1 1   | 0   1
        # 1 0 0   | 1   0
        # 1 0 1   | 0   1
        # 1 1 0   | 0   1
        # 1 1 1   | 1   1
        
        test_cases = [
            (0, 0, 0, 0, 0),
            (0, 0, 1, 1, 0),
            (0, 1, 0, 1, 0),
            (0, 1, 1, 0, 1),
            (1, 0, 0, 1, 0),
            (1, 0, 1, 0, 1),
            (1, 1, 0, 0, 1),
            (1, 1, 1, 1, 1),
        ]
        
        for a, b, cin, expected_sum, expected_cout in test_cases:
            controller.reset()
            controller.poke("A", a)
            controller.poke("B", b)
            controller.poke("Cin", cin)
            # Need 5 steps for full propagation through all gate types
            controller.step(5)
            
            actual_sum = controller.peek("Sum")
            actual_cout = controller.peek("Cout")
            
            assert actual_sum == expected_sum, f"Sum mismatch for A={a}, B={b}, Cin={cin}"
            assert actual_cout == expected_cout, f"Cout mismatch for A={a}, B={b}, Cin={cin}"
    
    def test_step_advances_cycle(self, controller):
        """Test that step advances the cycle counter."""
        controller.reset()
        assert controller.cycle == 0
        
        controller.step()
        assert controller.cycle == 1
        
        controller.step(5)
        assert controller.cycle == 6
    
    def test_step_returns_stop_info(self, controller):
        """Test that step returns StopInfo."""
        result = controller.step()
        
        assert isinstance(result, StopInfo)
        assert result.reason == StopReason.STEP


# =============================================================================
# Test 4: Internal Gate Access (peek_gate)
# =============================================================================

class TestPeekGate:
    """Tests for internal gate access via peek_gate."""
    
    def test_peek_gate_available(self, controller):
        """Test that debug API is available."""
        assert controller._has_debug_api, "Debug API should be available in debug builds"
    
    def test_peek_xor_gates(self, controller):
        """Test peeking XOR gate outputs."""
        # x1 = A XOR B
        # x2 = x1 XOR Cin
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        controller.step()
        
        x1_val = controller.peek_gate("x1")
        x2_val = controller.peek_gate("x2")
        
        # 1 XOR 1 = 0
        assert x1_val == 0
        # 0 XOR 0 = 0
        assert x2_val == 0
    
    def test_peek_and_gates(self, controller):
        """Test peeking AND gate outputs."""
        # a1 = A AND B
        # a2 = x1 AND Cin
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        controller.step()
        
        a1_val = controller.peek_gate("a1")
        a2_val = controller.peek_gate("a2")
        
        # 1 AND 1 = 1
        assert a1_val == 1
        # x1=0 AND 0 = 0
        assert a2_val == 0
    
    def test_peek_or_gate(self, controller):
        """Test peeking OR gate output.
        
        Note: Due to propagation delay, OR gate needs 2 steps to reflect
        changes from AND gates.
        """
        # o1 = a1 OR a2 = Cout
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        # Need 2 steps: 1st for XOR/AND, 2nd for OR
        controller.step(2)
        
        o1_val = controller.peek_gate("o1")
        cout_val = controller.peek("Cout")
        
        # o1 = 1 OR 0 = 1
        assert o1_val == 1
        assert o1_val == cout_val
    
    def test_all_gates_consistent(self, controller):
        """Test that all gate values are consistent with circuit logic.
        
        Note: Due to propagation delay, we need 5 steps for values to
        fully propagate through all gate levels.
        """
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 1)
        controller.poke("Cin", 1)
        # Need 5 steps for full propagation
        controller.step(5)
        
        # Expected internal values for A=0, B=1, Cin=1:
        # x1 = 0 XOR 1 = 1
        # x2 = 1 XOR 1 = 0 = Sum
        # a1 = 0 AND 1 = 0
        # a2 = 1 AND 1 = 1
        # o1 = 0 OR 1 = 1 = Cout
        
        assert controller.peek_gate("x1") == 1
        assert controller.peek_gate("x2") == 0
        assert controller.peek_gate("a1") == 0
        assert controller.peek_gate("a2") == 1
        assert controller.peek_gate("o1") == 1
        
        assert controller.peek("Sum") == 0
        assert controller.peek("Cout") == 1


# =============================================================================
# Test 5: Breakpoints
# =============================================================================

class TestBreakpoints:
    """Tests for breakpoint functionality."""
    
    def test_add_breakpoint(self, controller):
        """Test adding a breakpoint."""
        bp = controller.add_breakpoint("Cout")
        
        assert bp is not None
        assert bp.id == 1
        assert bp.signal == "Cout"
        assert bp.bp_type == BreakpointType.CHANGE
        assert bp.enabled is True
    
    def test_breakpoint_on_change(self, controller):
        """Test breakpoint triggers on value change."""
        bp = controller.add_breakpoint("Cout")
        
        # Set up for Cout=0 initially
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.poke("Cin", 0)
        controller.step()  # Cout stays 0, initialize _last_value
        
        # Now change to cause Cout=1
        controller.poke("A", 1)
        controller.poke("B", 1)
        result = controller.step()
        
        assert result.reason == StopReason.BREAKPOINT
        assert result.signal == "Cout"
        assert result.old_value == 0
        assert result.new_value == 1
    
    def test_breakpoint_no_trigger_when_unchanged(self, controller):
        """Test breakpoint doesn't trigger when value stays same."""
        bp = controller.add_breakpoint("Cout")
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.poke("Cin", 0)
        controller.step()  # Initialize
        
        # Step again without changing inputs
        result = controller.step()
        
        assert result.reason == StopReason.STEP  # Not BREAKPOINT
    
    def test_remove_breakpoint(self, controller):
        """Test removing a breakpoint."""
        bp = controller.add_breakpoint("Cout")
        assert len(controller.get_breakpoints()) == 1
        
        success = controller.remove_breakpoint(bp.id)
        assert success is True
        assert len(controller.get_breakpoints()) == 0
    
    def test_disable_enable_breakpoint(self, controller):
        """Test disabling and enabling breakpoints."""
        bp = controller.add_breakpoint("Cout")
        
        controller.disable_breakpoint(bp.id)
        assert controller.get_breakpoints()[0].enabled is False
        
        controller.enable_breakpoint(bp.id)
        assert controller.get_breakpoints()[0].enabled is True
    
    def test_disabled_breakpoint_no_trigger(self, controller):
        """Test disabled breakpoint doesn't trigger."""
        bp = controller.add_breakpoint("Cout")
        controller.disable_breakpoint(bp.id)
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.step()  # Initialize
        
        controller.poke("A", 1)
        controller.poke("B", 1)
        result = controller.step()
        
        assert result.reason == StopReason.STEP  # Not BREAKPOINT
    
    def test_clear_breakpoints(self, controller):
        """Test clearing all breakpoints."""
        controller.add_breakpoint("Cout")
        controller.add_breakpoint("Sum")
        assert len(controller.get_breakpoints()) == 2
        
        count = controller.clear_breakpoints()
        assert count == 2
        assert len(controller.get_breakpoints()) == 0
    
    def test_breakpoint_on_gate(self, controller):
        """Test breakpoint on internal gate.
        
        Note: Gate breakpoints detect changes in the gate's output value
        between steps.
        """
        bp = controller.add_breakpoint("x1")
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.step()  # x1 = 0 XOR 0 = 0, initialize
        
        controller.poke("A", 1)
        result = controller.step()  # x1 = 1 XOR 0 = 1
        
        # x1 changed from 0 to 1
        assert result.reason == StopReason.BREAKPOINT
        assert result.signal == "x1"
        assert result.old_value == 0
        assert result.new_value == 1
    
    def test_temporary_breakpoint(self, controller):
        """Test temporary breakpoint (deleted after first hit)."""
        bp = controller.add_breakpoint("Cout", temporary=True)
        bp_id = bp.id
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.step()
        
        controller.poke("A", 1)
        controller.poke("B", 1)
        result = controller.step()
        
        assert result.reason == StopReason.BREAKPOINT
        # Breakpoint should be removed after hit
        assert controller.remove_breakpoint(bp_id) is False  # Already removed
    
    def test_value_breakpoint(self, controller):
        """Test breakpoint on specific value."""
        bp = controller.add_breakpoint("Sum", bp_type=BreakpointType.VALUE, value=1)
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.step()  # Sum=0, no trigger
        
        controller.poke("A", 1)
        controller.poke("B", 0)
        result = controller.step()  # Sum=1, should trigger
        
        assert result.reason == StopReason.BREAKPOINT


# =============================================================================
# Test 6: Watchpoints
# =============================================================================

class TestWatchpoints:
    """Tests for watchpoint functionality."""
    
    def test_add_watchpoint(self, controller):
        """Test adding a watchpoint."""
        wp = controller.add_watchpoint("Sum")
        
        assert wp is not None
        assert wp.id == 1
        assert wp.signal == "Sum"
        assert wp.enabled is True
    
    def test_watchpoint_triggers_on_change(self, controller):
        """Test watchpoint triggers on value change."""
        wp = controller.add_watchpoint("Sum")
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.poke("Cin", 0)
        controller.step()  # Sum=0, initialize
        
        controller.poke("A", 1)
        result = controller.step()  # Sum=1
        
        assert result.reason == StopReason.WATCHPOINT
        assert result.signal == "Sum"
        assert result.old_value == 0
        assert result.new_value == 1
    
    def test_remove_watchpoint(self, controller):
        """Test removing a watchpoint."""
        wp = controller.add_watchpoint("Sum")
        assert len(controller.get_watchpoints()) == 1
        
        success = controller.remove_watchpoint(wp.id)
        assert success is True
        assert len(controller.get_watchpoints()) == 0
    
    def test_clear_watchpoints(self, controller):
        """Test clearing all watchpoints."""
        controller.add_watchpoint("Sum")
        controller.add_watchpoint("Cout")
        assert len(controller.get_watchpoints()) == 2
        
        count = controller.clear_watchpoints()
        assert count == 2
        assert len(controller.get_watchpoints()) == 0
    
    def test_watchpoint_on_gate(self, controller):
        """Test watchpoint on internal gate."""
        wp = controller.add_watchpoint("a1")
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 1)
        controller.step()  # a1 = 0 AND 1 = 0, initialize
        
        controller.poke("A", 1)
        result = controller.step()  # a1 = 1 AND 1 = 1
        
        assert result.reason == StopReason.WATCHPOINT
        assert result.signal == "a1"


# =============================================================================
# Test 7: Continue Until Breakpoint
# =============================================================================

class TestContinueUntilBreakpoint:
    """Tests for continue_until_breakpoint functionality."""
    
    def test_continue_hits_breakpoint(self, controller):
        """Test continue runs until breakpoint is hit."""
        controller.add_breakpoint("Cout", bp_type=BreakpointType.VALUE, value=1)
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.poke("Cin", 0)
        
        # After several steps without trigger, change inputs to trigger
        # We can't easily test "continue" without a changing input sequence
        # So just test that it returns when max_cycles is reached
        result = controller.continue_until_breakpoint(max_cycles=10)
        
        # Should stop after max_cycles
        assert result.reason == StopReason.STEP
    
    def test_continue_max_cycles(self, controller):
        """Test continue respects max_cycles limit."""
        controller.reset()
        result = controller.continue_until_breakpoint(max_cycles=100)
        
        assert result.cycle <= 100


# =============================================================================
# Test 8: Signal Inspection Helpers
# =============================================================================

class TestSignalInspection:
    """Tests for signal inspection helper methods."""
    
    def test_get_all_inputs(self, controller):
        """Test getting all input values."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 0)
        controller.poke("Cin", 1)
        controller.step()
        
        inputs = controller.get_all_inputs()
        
        assert "A" in inputs
        assert "B" in inputs
        assert "Cin" in inputs
    
    def test_get_all_outputs(self, controller):
        """Test getting all output values.
        
        Note: Need a few steps for full propagation through all gate levels.
        """
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 0)
        controller.poke("Cin", 1)
        controller.step(5)  # 5 steps for full propagation
        
        outputs = controller.get_all_outputs()
        
        assert "Sum" in outputs
        assert "Cout" in outputs
        assert outputs["Sum"] == 0  # 1 XOR 0 XOR 1 = 0
        assert outputs["Cout"] == 1  # carry from Cin
    
    def test_get_all_gates(self, controller):
        """Test getting all gate values."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        controller.step()
        
        gates = controller.get_all_gates()
        
        assert "x1" in gates
        assert "x2" in gates
        assert "a1" in gates
        assert "a2" in gates
        assert "o1" in gates
    
    def test_resolve_and_get(self, controller):
        """Test resolve_and_get helper."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 0)
        controller.step()
        
        ref, value = controller.resolve_and_get("Sum")
        
        assert ref is not None
        assert ref.signal_type == SignalType.OUTPUT_PORT
        assert value is not None


# =============================================================================
# Test 9: Context Manager
# =============================================================================

class TestContextManager:
    """Tests for context manager support."""
    
    def test_context_manager_usage(self, compiled_full_adder, debug_info):
        """Test using DebugController as context manager.
        
        Note: Need 2 steps for full propagation through all gate levels.
        """
        lib_path, _ = compiled_full_adder
        
        with DebugController(lib_path, debug_info) as ctrl:
            ctrl.reset()
            ctrl.poke("A", 1)
            ctrl.poke("B", 1)
            ctrl.step(2)  # 2 steps for full propagation
            
            assert ctrl.peek("Sum") == 0
            assert ctrl.peek("Cout") == 1


# =============================================================================
# Test 10: Edge Cases and Error Handling
# =============================================================================

class TestEdgeCases:
    """Tests for edge cases and error handling."""
    
    def test_peek_before_step(self, controller):
        """Test peeking values before any step."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        
        # Should be able to read even before step
        # (values might be undefined/zero)
        _ = controller.peek("Sum")
    
    def test_multiple_resets(self, controller):
        """Test multiple resets work correctly."""
        for _ in range(5):
            controller.reset()
            controller.poke("A", 1)
            controller.step()
            controller.reset()
            assert controller.cycle == 0
    
    def test_many_steps(self, controller):
        """Test many steps work correctly."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        
        controller.step(1000)
        assert controller.cycle == 1000
    
    def test_breakpoint_hit_count(self, controller):
        """Test breakpoint hit count increments."""
        bp = controller.add_breakpoint("Sum")
        
        controller.reset()
        controller.poke("A", 0)
        controller.poke("B", 0)
        controller.step()  # Initialize Sum=0
        
        # Trigger a change
        controller.poke("A", 1)
        controller.step()  # Sum changes to 1
        
        assert bp.hit_count == 1
        
        # Change back
        controller.poke("A", 0)
        controller.step()  # Sum changes back to 0
        
        assert bp.hit_count == 2


# =============================================================================
# Test 11: SourceMap (basic tests)
# =============================================================================

class TestSourceMap:
    """Tests for source mapping functionality."""
    
    def test_source_map_creation(self, debug_info):
        """Test SourceMap can be created."""
        source_map = SourceMap(debug_info)
        assert source_map is not None
    
    def test_get_source_location_no_source(self, debug_info):
        """Test getting source location when not available."""
        source_map = SourceMap(debug_info)
        
        # Our test .shdb may not have source info for gates
        loc = source_map.get_source_location("x1")
        # May be None if source info not included
        # This is expected behavior


# =============================================================================
# Test 12: DebugInfo without file (programmatic creation)
# =============================================================================

class TestDebugInfoProgrammatic:
    """Tests for programmatically creating DebugInfo."""
    
    def test_create_empty(self):
        """Test creating empty DebugInfo."""
        info = DebugInfo()
        assert info.version == "1.0"
        assert info.component == ""
        assert len(info.inputs) == 0
        assert len(info.outputs) == 0
        assert len(info.gates) == 0
    
    def test_set_component_name(self):
        """Test setting component name."""
        info = DebugInfo()
        info.component = "TestComponent"
        assert info.component == "TestComponent"


# =============================================================================
# Test 13: Finish (stability detection)
# =============================================================================

class TestFinish:
    """Tests for the finish command (signal stabilization)."""
    
    def test_finish_combinational(self, controller):
        """Test finish on combinational circuit stabilizes immediately."""
        controller.reset()
        controller.poke("A", 1)
        controller.poke("B", 1)
        controller.poke("Cin", 0)
        
        result = controller.finish(max_cycles=100)
        
        # Combinational circuit should stabilize quickly
        assert result.reason == StopReason.STEP
        assert "stabilized" in result.message.lower() or result.cycle <= 100
