"""Tests for packed operation opportunity detector.

Tests the detection of scalar VALU operations that can be packed into
v_pk_* instructions for improved throughput.

Run with:
    PYTHONPATH=packages/wafer-core uv run pytest packages/wafer-core/tests/test_packed_opportunity_detector.py -v
"""

import pytest

from wafer_core.lib.kernel_scope.amdgcn.parser import parse_isa_text
from wafer_core.lib.kernel_scope.amdgcn.analyzer import analyze_isa
from wafer_core.lib.kernel_scope.amdgcn.packed_opportunity_detector import (
    detect_packable_pairs,
    detect_packed_opportunities,
    group_into_sequences,
    suggest_packed_rewrite,
    format_packed_analysis,
)
from wafer_core.lib.kernel_scope.amdgcn.types import (
    InstructionInfo,
    InstructionCategory,
    PackableOpPair,
    PackableSequence,
    PackedOpportunityAnalysis,
)


# ============================================================================
# Test ISA snippets
# ============================================================================

# Tensile-style scalar epilogue: bias + scale on each element individually
SCALAR_EPILOGUE_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
scalar_epilogue_kernel:
    ; Main MFMA loop omitted...
    v_mfma_f32_32x32x8f16 a[0:15], v[0:1], v[2:3], a[0:15]

    ; Scalar epilogue: bias + scale per element (could be packed)
    v_add_f32 v0, v0, v100
    v_mul_f32 v0, v0, s10
    v_add_f32 v1, v1, v101
    v_mul_f32 v1, v1, s10
    v_add_f32 v2, v2, v102
    v_mul_f32 v2, v2, s10
    v_add_f32 v3, v3, v103
    v_mul_f32 v3, v3, s10

    global_store_dwordx4 v[20:21], v[0:3], off
    s_endpgm

.amdhsa_kernel scalar_epilogue_kernel
    .amdhsa_next_free_vgpr 128
    .amdhsa_next_free_sgpr 32
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# Hand-tuned packed epilogue: already using v_pk_* (should report no opportunities)
PACKED_EPILOGUE_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
packed_epilogue_kernel:
    ; Main MFMA loop omitted...
    v_mfma_f32_32x32x8f16 a[0:15], v[0:1], v[2:3], a[0:15]

    ; Packed epilogue: bias + scale on 2 elements at once
    v_pk_add_f32 v[0:1], v[0:1], v[100:101]
    v_pk_mul_f32 v[0:1], v[0:1], s[10:11]
    v_pk_add_f32 v[2:3], v[2:3], v[102:103]
    v_pk_mul_f32 v[2:3], v[2:3], s[10:11]

    global_store_dwordx4 v[20:21], v[0:3], off
    s_endpgm

.amdhsa_kernel packed_epilogue_kernel
    .amdhsa_next_free_vgpr 128
    .amdhsa_next_free_sgpr 32
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# Simple repeated add pattern
REPEATED_ADD_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
repeated_add_kernel:
    v_add_f32 v0, v0, v10
    v_add_f32 v1, v1, v11
    v_add_f32 v2, v2, v12
    v_add_f32 v3, v3, v13
    v_add_f32 v4, v4, v14
    v_add_f32 v5, v5, v15
    s_endpgm

.amdhsa_kernel repeated_add_kernel
    .amdhsa_next_free_vgpr 32
    .amdhsa_next_free_sgpr 16
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# No packable opportunities (odd register pattern, or non-packable ops)
NO_OPPORTUNITY_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
no_opportunity_kernel:
    ; Different operations, can't pack
    v_add_f32 v0, v0, v10
    v_mul_f32 v1, v1, v11
    ; Compare instructions don't have packed equivalents
    v_cmp_gt_f32 vcc, v0, v1
    v_cndmask_b32 v2, v3, v4, vcc
    ; Scalar ALU
    s_add_u32 s0, s1, s2
    s_endpgm

.amdhsa_kernel no_opportunity_kernel
    .amdhsa_next_free_vgpr 16
    .amdhsa_next_free_sgpr 16
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# Mixed: some packed, some scalar that could be packed
PARTIALLY_PACKED_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
partial_kernel:
    ; Already packed (good)
    v_pk_add_f32 v[0:1], v[0:1], v[100:101]
    v_pk_mul_f32 v[0:1], v[0:1], s[10:11]

    ; Still scalar (could be packed)
    v_add_f32 v2, v2, v102
    v_add_f32 v3, v3, v103
    v_mul_f32 v2, v2, s10
    v_mul_f32 v3, v3, s10

    s_endpgm

.amdhsa_kernel partial_kernel
    .amdhsa_next_free_vgpr 128
    .amdhsa_next_free_sgpr 32
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# Interleaved pattern: add/mul interleaved per-element (common Tensile pattern)
INTERLEAVED_EPILOGUE_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
interleaved_kernel:
    ; Interleaved: each element gets add+mul before the next
    v_add_f32 v0, v0, v100
    v_mul_f32 v0, v0, s10
    v_add_f32 v1, v1, v101
    v_mul_f32 v1, v1, s10
    v_add_f32 v2, v2, v102
    v_mul_f32 v2, v2, s10
    v_add_f32 v3, v3, v103
    v_mul_f32 v3, v3, s10
    v_add_f32 v4, v4, v104
    v_mul_f32 v4, v4, s10
    v_add_f32 v5, v5, v105
    v_mul_f32 v5, v5, s10
    v_add_f32 v6, v6, v106
    v_mul_f32 v6, v6, s10
    v_add_f32 v7, v7, v107
    v_mul_f32 v7, v7, s10
    s_endpgm

.amdhsa_kernel interleaved_kernel
    .amdhsa_next_free_vgpr 128
    .amdhsa_next_free_sgpr 32
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''

# FP16 packable operations
FP16_PACKABLE_ISA = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"

.text
fp16_kernel:
    v_add_f16 v0, v0, v10
    v_add_f16 v1, v1, v11
    v_mul_f16 v2, v2, v12
    v_mul_f16 v3, v3, v13
    s_endpgm

.amdhsa_kernel fp16_kernel
    .amdhsa_next_free_vgpr 32
    .amdhsa_next_free_sgpr 16
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''


# ============================================================================
# Tests for detect_packable_pairs
# ============================================================================


class TestDetectPackablePairs:
    """Tests for the low-level pair detection."""

    def test_scalar_epilogue_finds_add_pairs(self):
        """Scalar epilogue with v_add_f32 on v0,v1 and v2,v3 should find 2 add pairs."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)

        add_pairs = [p for p in pairs if "add" in p.packed_equivalent]
        assert len(add_pairs) >= 2

        # First pair should be v0, v1
        assert add_pairs[0].first_dest_reg == 0
        assert add_pairs[0].second_dest_reg == 1
        assert add_pairs[0].packed_equivalent == "v_pk_add_f32"

    def test_scalar_epilogue_finds_mul_pairs(self):
        """Scalar epilogue v_mul_f32 on v0,v1 and v2,v3 should find mul pairs."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)

        mul_pairs = [p for p in pairs if "mul" in p.packed_equivalent]
        assert len(mul_pairs) >= 2

    def test_packed_epilogue_no_scalar_pairs(self):
        """Already-packed epilogue should have no scalar pairs to pack."""
        parse_result = parse_isa_text(PACKED_EPILOGUE_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)
        assert len(pairs) == 0

    def test_repeated_add_finds_three_pairs(self):
        """6 consecutive v_add_f32 on v0-v5 should yield 3 pairs (v0+v1, v2+v3, v4+v5)."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)
        assert len(pairs) == 3

        # Verify register assignments
        regs = [(p.first_dest_reg, p.second_dest_reg) for p in pairs]
        assert (0, 1) in regs
        assert (2, 3) in regs
        assert (4, 5) in regs

    def test_no_opportunity_finds_nothing(self):
        """Different ops / non-packable ops should yield no pairs."""
        parse_result = parse_isa_text(NO_OPPORTUNITY_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)
        assert len(pairs) == 0

    def test_interleaved_epilogue_finds_pairs(self):
        """Interleaved add/mul per-element should still find pairs with lookahead."""
        parse_result = parse_isa_text(INTERLEAVED_EPILOGUE_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)

        # Should find add pairs and mul pairs
        add_pairs = [p for p in pairs if "add" in p.packed_equivalent]
        mul_pairs = [p for p in pairs if "mul" in p.packed_equivalent]

        assert len(add_pairs) >= 2
        assert len(mul_pairs) >= 2

    def test_fp16_packable(self):
        """FP16 operations should also be detected as packable."""
        parse_result = parse_isa_text(FP16_PACKABLE_ISA)
        assert parse_result.success
        instructions = parse_result.instructions

        pairs = detect_packable_pairs(instructions)

        add_pairs = [p for p in pairs if "add" in p.packed_equivalent]
        mul_pairs = [p for p in pairs if "mul" in p.packed_equivalent]
        assert len(add_pairs) >= 1
        assert len(mul_pairs) >= 1

    def test_pair_has_correct_packed_equivalent(self):
        """PackableOpPair should carry the correct packed mnemonic."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        assert all(p.packed_equivalent == "v_pk_add_f32" for p in pairs)

    def test_even_alignment_required(self):
        """Only even-aligned register pairs should be detected (v0+v1, not v1+v2)."""
        isa = '''
.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"
.text
test_kernel:
    v_add_f32 v1, v1, v10
    v_add_f32 v2, v2, v11
    s_endpgm
.amdhsa_kernel test_kernel
    .amdhsa_next_free_vgpr 16
    .amdhsa_next_free_sgpr 8
    .amdhsa_group_segment_fixed_size 0
    .amdhsa_private_segment_fixed_size 0
.end_amdhsa_kernel
'''
        parse_result = parse_isa_text(isa)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        # v1 is odd, so v1+v2 is NOT a valid pair for packing
        assert len(pairs) == 0


# ============================================================================
# Tests for group_into_sequences
# ============================================================================


class TestGroupIntoSequences:
    """Tests for grouping pairs into contiguous sequences."""

    def test_scalar_epilogue_one_sequence(self):
        """All pairs in a scalar epilogue should be in one sequence."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        sequences = group_into_sequences(pairs)

        # All pairs are close together, should be one sequence
        assert len(sequences) == 1
        seq = sequences[0]
        assert seq.scalar_instruction_count == len(pairs) * 2
        assert seq.packed_instruction_count == len(pairs)
        assert seq.instruction_savings == len(pairs)

    def test_sequence_has_correct_pattern_type(self):
        """Sequence pattern should be classified correctly."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        sequences = group_into_sequences(pairs)

        assert len(sequences) == 1
        assert sequences[0].pattern_type == "repeated_add"

    def test_epilogue_pattern_classified(self):
        """Add+mul epilogue should be classified as epilogue_bias_scale_f32."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        sequences = group_into_sequences(pairs)

        assert len(sequences) >= 1
        # Should contain an epilogue pattern
        pattern_types = [s.pattern_type for s in sequences]
        has_epilogue = any("epilogue" in pt for pt in pattern_types)
        assert has_epilogue, f"Expected epilogue pattern, got: {pattern_types}"


# ============================================================================
# Tests for detect_packed_opportunities (high-level)
# ============================================================================


class TestDetectPackedOpportunities:
    """Tests for the high-level analysis function."""

    def test_scalar_epilogue_has_opportunities(self):
        """Scalar epilogue should report packable opportunities."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert analysis.total_packable_pairs > 0
        assert analysis.total_scalar_ops_packable > 0
        assert analysis.instruction_savings > 0
        assert analysis.savings_pct_of_valu > 0.0

    def test_packed_epilogue_no_opportunities(self):
        """Packed epilogue should report 0 packable pairs and count existing packed ops."""
        parse_result = parse_isa_text(PACKED_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert analysis.total_packable_pairs == 0
        assert analysis.already_packed_count > 0
        assert analysis.packing_ratio == 1.0

    def test_partially_packed_reports_both(self):
        """Partially packed kernel should report both packed and packable."""
        parse_result = parse_isa_text(PARTIALLY_PACKED_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert analysis.already_packed_count > 0
        assert analysis.total_packable_pairs > 0
        assert 0.0 < analysis.packing_ratio < 1.0

    def test_no_opportunities_empty_recommendations(self):
        """No opportunities should give informational recommendation."""
        parse_result = parse_isa_text(NO_OPPORTUNITY_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert analysis.total_packable_pairs == 0
        assert len(analysis.recommendations) > 0
        assert any("No packable" in r or "no" in r.lower() for r in analysis.recommendations)

    def test_recommendations_generated(self):
        """Opportunities should generate actionable recommendations."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert len(analysis.recommendations) > 0
        assert any("OPPORTUNITY" in r for r in analysis.recommendations)

    def test_sequences_populated(self):
        """Analysis should contain packable sequences."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        assert len(analysis.packable_sequences) > 0
        for seq in analysis.packable_sequences:
            assert seq.start_line > 0
            assert seq.end_line >= seq.start_line
            assert seq.scalar_instruction_count > 0
            assert seq.packed_instruction_count > 0
            assert seq.instruction_savings > 0

    def test_savings_math_correct(self):
        """Instruction savings should be scalar_ops - packed_ops."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        expected_savings = analysis.total_scalar_ops_packable - analysis.total_packed_ops_needed
        assert analysis.instruction_savings == expected_savings
        # Each pair saves exactly 1 instruction (2 scalar -> 1 packed)
        assert analysis.instruction_savings == analysis.total_packable_pairs


# ============================================================================
# Tests for integration with analyze_isa
# ============================================================================


class TestAnalyzerIntegration:
    """Tests that packed opportunity analysis integrates into the full pipeline."""

    def test_isa_analysis_has_packed_opportunity(self):
        """ISAAnalysis should contain packed_opportunity field."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = analyze_isa(parse_result)

        assert analysis.packed_opportunity is not None
        assert analysis.packed_opportunity.total_packable_pairs > 0

    def test_packed_epilogue_analysis(self):
        """Already-packed kernel should report good packing ratio."""
        parse_result = parse_isa_text(PACKED_EPILOGUE_ISA)
        assert parse_result.success

        analysis = analyze_isa(parse_result)

        assert analysis.packed_opportunity is not None
        assert analysis.packed_opportunity.total_packable_pairs == 0
        assert analysis.packed_opportunity.already_packed_count > 0

    def test_warnings_include_packing_opportunity(self):
        """Warnings should mention packing opportunity when pairs exist."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = analyze_isa(parse_result)

        has_packing_warning = any("packed" in w.lower() or "v_pk_" in w for w in analysis.warnings)
        assert has_packing_warning, f"Expected packing warning, got: {analysis.warnings}"


# ============================================================================
# Tests for suggest_packed_rewrite
# ============================================================================


class TestSuggestPackedRewrite:
    """Tests for the rewrite suggestion generator."""

    def test_rewrite_shows_before_after(self):
        """Rewrite suggestion should show both before and after code."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        assert len(pairs) > 0

        rewrite = suggest_packed_rewrite(pairs[0])
        assert "Before" in rewrite
        assert "After" in rewrite
        assert "v_pk_add_f32" in rewrite

    def test_rewrite_shows_register_pair(self):
        """Rewrite should show register pair notation v[N:N+1]."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        rewrite = suggest_packed_rewrite(pairs[0])

        assert "v[0:1]" in rewrite


# ============================================================================
# Tests for format_packed_analysis
# ============================================================================


class TestFormatPackedAnalysis:
    """Tests for the human-readable output formatter."""

    def test_format_with_opportunities(self):
        """Formatter should produce readable output with opportunities."""
        parse_result = parse_isa_text(SCALAR_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)
        output = format_packed_analysis(analysis)

        assert "Packed Operation Opportunities" in output
        assert "Packable scalar pairs" in output
        assert "Instruction savings" in output
        assert "OPPORTUNITY" in output

    def test_format_without_opportunities(self):
        """Formatter should handle no-opportunity case gracefully."""
        parse_result = parse_isa_text(NO_OPPORTUNITY_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)
        output = format_packed_analysis(analysis)

        assert "Packed Operation Opportunities" in output
        assert "Packable scalar pairs found: 0" in output

    def test_format_fully_packed(self):
        """Formatter should report good status for fully packed kernels."""
        parse_result = parse_isa_text(PACKED_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)
        output = format_packed_analysis(analysis)

        assert "Already packed" in output
        assert "100%" in output or "GOOD" in output


# ============================================================================
# Tests for interleaved pattern (the most common Tensile case)
# ============================================================================


class TestInterleavedPattern:
    """Tests specifically for the interleaved epilogue pattern.

    This is the most common case in Tensile-generated code:
        v_add_f32 v0, v0, v100    ; element 0 bias
        v_mul_f32 v0, v0, s10     ; element 0 scale
        v_add_f32 v1, v1, v101    ; element 1 bias
        v_mul_f32 v1, v1, s10     ; element 1 scale
        ...

    The detector needs lookahead to pair v_add_f32 v0 with v_add_f32 v1
    (skipping the v_mul_f32 in between).
    """

    def test_interleaved_detects_both_add_and_mul_pairs(self):
        """Should detect both add pairs and mul pairs despite interleaving."""
        parse_result = parse_isa_text(INTERLEAVED_EPILOGUE_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        add_pairs = [p for p in pairs if "add" in p.packed_equivalent]
        mul_pairs = [p for p in pairs if "mul" in p.packed_equivalent]

        # 8 elements = 4 add pairs and 4 mul pairs possible
        assert len(add_pairs) >= 2
        assert len(mul_pairs) >= 2

    def test_interleaved_full_analysis(self):
        """Full analysis of interleaved epilogue should show significant savings."""
        parse_result = parse_isa_text(INTERLEAVED_EPILOGUE_ISA)
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        # 16 scalar ops -> 8 packed ops = 50% VALU savings
        assert analysis.total_packable_pairs >= 4
        assert analysis.instruction_savings >= 4
        assert analysis.savings_pct_of_valu > 10.0

    def test_large_interleaved_epilogue(self):
        """Larger epilogue with 16 elements should find many pairs."""
        lines = [
            '.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"',
            '.text',
            'large_epilogue_kernel:',
        ]
        # Generate 16-element interleaved epilogue (v0-v15)
        for i in range(16):
            lines.append(f'    v_add_f32 v{i}, v{i}, v{100 + i}')
            lines.append(f'    v_mul_f32 v{i}, v{i}, s10')
        lines.extend([
            '    s_endpgm',
            '.amdhsa_kernel large_epilogue_kernel',
            '    .amdhsa_next_free_vgpr 256',
            '    .amdhsa_next_free_sgpr 32',
            '    .amdhsa_group_segment_fixed_size 0',
            '    .amdhsa_private_segment_fixed_size 0',
            '.end_amdhsa_kernel',
        ])

        parse_result = parse_isa_text('\n'.join(lines))
        assert parse_result.success

        analysis = detect_packed_opportunities(parse_result.instructions)

        # 16 elements: 8 add pairs + 8 mul pairs = 16 pairs
        # Saves 16 instructions out of 32
        assert analysis.total_packable_pairs >= 8
        assert analysis.instruction_savings >= 8


# ============================================================================
# Tests for type immutability
# ============================================================================


class TestTypeImmutability:
    """Ensure all new types are properly frozen."""

    def test_packable_op_pair_frozen(self):
        """PackableOpPair should be immutable."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        assert len(pairs) > 0

        with pytest.raises(AttributeError):
            pairs[0].packed_equivalent = "modified"  # type: ignore[misc]

    def test_packed_opportunity_analysis_frozen(self):
        """PackedOpportunityAnalysis should be immutable."""
        analysis = PackedOpportunityAnalysis()
        with pytest.raises(AttributeError):
            analysis.total_packable_pairs = 99  # type: ignore[misc]

    def test_packable_sequence_frozen(self):
        """PackableSequence should be immutable."""
        parse_result = parse_isa_text(REPEATED_ADD_ISA)
        assert parse_result.success

        pairs = detect_packable_pairs(parse_result.instructions)
        sequences = group_into_sequences(pairs)
        assert len(sequences) > 0

        with pytest.raises(AttributeError):
            sequences[0].pattern_type = "modified"  # type: ignore[misc]
