#![cfg_attr(
    not(test),
    allow(
        clippy::indexing_slicing,
        clippy::string_slice,
        reason = "PROOF: emit_dex serializes a validated, parser-accepted DexFile back to bytes. Every pool index (StringIdx/TypeIdx/MethodIdx/FieldIdx/ProtoIdx) is < pool.len() by parse-time validation. Every byte slice in the output is built up via Vec::push / extend_from_slice into emit-internal buffers (no parser-controlled offsets touch the indexing here). UTF-8 encoding goes through MUTF-8 helpers that produce well-formed bytes. v1.x refinement candidate (~18 sites)."
    )
)]
#![cfg_attr(not(test), allow(clippy::map_err_ignore, reason = "every `.map_err(|_| DexEmitError::...)` here converts an opaque `TryFromIntError` (numeric overflow on emit-side cast) or `NonDecreasing::from_verified` precondition error into a typed DexEmitError variant whose context (got/section/cap) is captured in the variant fields. The discarded source adds no actionable info beyond what the typed error already carries."))]
//! DEX byte emitter — the inverse of `parser::DexFile::parse`.
//!
//! `emit_dex(dex)` serializes a parsed `DexFile` back to bytes such that
//! `parse(emit_dex(parse(bytes))) == parse(bytes)` at the IR level
//! (structural equivalence). Byte-identity is the deterministic-path
//! consequence of canonical IR ordering + symmetric emit — tracked as
//! an opt-in property tracked separately, not enforced by default.
//!
//! **Trust boundary.** The output retains the same trust level as its
//! input. Round-trip consistency proves emit is consistent with our
//! parser; it does not prove emit output is safe to hand to
//! ART / Dalvik. Downstream consumers must treat emit output as
//! adversarial-equivalent to its input DEX.
//!
//! **Canonical ordering (gauge-fix).** The DEX spec requires five id
//! pools to be lexicographically sorted: `string_ids`, `type_ids`,
//! `proto_ids`, `field_ids`, `method_ids`. The [`NonDecreasing`]
//! newtype makes this invariant structural: emit code can only obtain
//! a `NonDecreasing<T>` by calling [`NonDecreasing::from_sorted`],
//! which sorts on construction. The type system forbids an emit path
//! that skips or reorders after wrapping. Byte-identity becomes a
//! theorem of the sort function + emit symmetry, not a test result.
//!
//! Analogously, [`StrictlyAscending`] carries the stricter invariant
//! `inner[i] < inner[j]` for `i < j` and is used for `sparse-switch`
//! keys (spec §6 — VM does binary-search dispatch; duplicates would
//! be non-deterministic).
//!
//! **Emit domain ⊊ Parse domain.** The parser is defensive: it accepts
//! a wider set of byte sequences than emit can produce. Emit rejects
//! IR shapes that are structurally ill-formed even if parse-acceptable
//! (e.g., an `encoded_catch_handler` with zero typed catches and no
//! `catch_all_addr` — spec-representable byte-wise, but a handler with
//! no runtime effect). This asymmetry is by design:
//!
//! - `parse → IR` tolerates anomalies so analysis can proceed on
//!   adversarial input without panicking.
//! - `IR → emit` rejects the same anomalies so we never produce bytes
//!   that round-trip an ill-formed shape onward.
//!
//! Hand-built IR (test fixtures, synthesized transformations) must
//! satisfy emit's stricter invariants. Parser-produced IR already does
//! on the paths that matter (e.g., `parse_code_item` never emits
//! `CatchHandler { catches: [], catch_all_addr: None }`: `size_raw = 0`
//! sets `has_catch_all = true`).
#![allow(missing_docs, reason = "internal")]

use std::fmt;
use thiserror::Error;

use droidsaw_common::encoding::{write_sleb128, write_uleb128};

use crate::annotation::{EncodedAnnotation, EncodedValue};
use crate::decode::{
    insn_format, CatchHandler, EncodedField, EncodedMethod, InsnFormat, Instruction, PayloadData,
    PoolIndex, TryItem,
};
use crate::ids::{ClassDefItem, FieldIdItem, MethodIdItem, ProtoIdItem, StringIdx, TypeIdx};
use crate::parser::DexFile;

/// Sentinel meaning "no such index" in a DEX file — used for optional
/// `TypeIdx` / `StringIdx` fields in `class_def_item` (superclass of
/// `java.lang.Object`, classes without a source-file attribute, etc.).
const NO_INDEX: u32 = 0xFFFFFFFF;

/// Failure classes for `emit_dex`. Every variant names a real failure
/// mode — none is a catch-all. Adding a new failure class requires a
/// new variant; panicking on attacker-controlled IR values is a bug.
#[derive(Debug, Clone, Error)]
pub enum DexEmitError {
    /// A width-bounded field (file_size / data_size / header_size / any
    /// `*_size` in the DEX header) cannot represent the computed size.
    /// Typically triggered by attacker-controlled IR with an oversized
    /// section or count.
    #[error("emit: size overflow in {layer}: {context}")]
    SizeOverflow {
        layer: &'static str,
        context: &'static str,
    },

    /// An offset (`*_off` in the header or a payload) cannot be
    /// represented within the DEX file's 32-bit offset space. Usually
    /// means the emit is producing a file larger than the DEX format
    /// allows (>4 GiB), or that an intra-section offset computation
    /// narrowed incorrectly.
    #[error("emit: offset overflow in section {section}: {context}")]
    OffsetOverflow {
        section: &'static str,
        context: &'static str,
    },

    /// Adler-32 or SHA-1 recompute produced a value that does not
    /// match the byte-range we wrote — almost always indicates a
    /// byte-range bug, not a hash-fn bug.
    #[error("emit: checksum recompute failed: {0}")]
    ChecksumRecomputeFailed(&'static str),

    /// The caller handed us an IR shape emit cannot round-trip:
    /// non-canonical ordering, undefined references, etc. If the
    /// canonical-ordering newtype fully fixed the gauge, this variant
    /// would be unreachable — it exists for the retrofit window where
    /// some orderings are still runtime-asserted rather than
    /// type-level.
    #[error("emit: unrepresentable IR: {why}")]
    UnrepresentableIR { why: &'static str },

    /// Internal invariant violated during emit assembly (a layout
    /// arithmetic bug or unreachable-branch-reached condition). Not
    /// an input error — emit has its own contract broken. Returned
    /// instead of `debug_assert!` so release builds don't ship
    /// silently-corrupt output on internal bugs.
    #[error("emit: internal invariant violated: {why}")]
    InvariantViolated { why: &'static str },

    /// Placeholder during the staged implementation. Returned by the
    /// v1-bootstrap while Directive 6 section emit is being built up.
    /// Will be removed when all sections are implemented.
    #[error("emit: not yet implemented (staged impl)")]
    NotImplemented,

    /// The input `DexFile` has non-empty `parse_errors` — some
    /// subsection(s) were silently dropped by the tolerant parser,
    /// and emit would produce an output that is a PARTIAL IMAGE of
    /// the original input bytes. The evasion primitive: an attacker
    /// hides bytes in malformed subsections; parse drops them; emit
    /// produces a corpus-ready clean DEX that lacks the hidden
    /// content.
    ///
    /// Default `EmitConfig` rejects this shape. Callers who want
    /// partial-IR round-trip (e.g. normalization / deduplication
    /// workflows that deliberately accept lossy emit) must opt in
    /// via [`EmitConfig::permit_partial_ir`].
    #[error(
        "emit: input IR has {count} parse error(s); first = {first_kind:?} at offset {first_offset:#x}; \
         set EmitConfig::permit_partial_ir = true to proceed"
    )]
    PartialIR {
        count: usize,
        first_kind: crate::parser::ParseFailureKind,
        first_offset: u32,
    },
}

/// Per-call emit configuration. `EmitConfig::default()` = strict
/// mode: rejects partial IR; applies every canonicalization emit
/// currently knows about. Today the only field is `permit_partial_ir`;
/// additional per-canonicalization toggles will land as the
/// preservation paths are implemented. Adding a toggle before the
/// emit path actually observes it is a lying API — don't.
#[derive(Debug, Clone, Default)]
pub struct EmitConfig {
    /// When `true`, `emit_dex` proceeds even if the input
    /// `DexFile.parse_errors` is non-empty. Normally `false` (strict
    /// mode rejects with [`DexEmitError::PartialIR`]) — the attacker
    /// evasion primitive requires this gate to default-strict.
    ///
    /// Opt in when you genuinely want to round-trip a DEX whose
    /// tolerant parse lost subsections: e.g. corpus normalization
    /// pipelines that deliberately canonicalize lossy parses.
    pub permit_partial_ir: bool,

    /// When `true`, retain the input `DexFile`'s `map_entries` order in
    /// the emitted `map_list` instead of canonicalizing into
    /// ascending-offset order. Default `false` — preserves the
    /// established canonicalizing behaviour. When `true`,
    /// [`CanonicalTransform::MapListReordered`] is never pushed
    /// because emit did not reorder.
    ///
    /// Spec note: DEX §7.18 says map_items "must be in ascending
    /// order by offset." Most well-formed inputs already are, so this
    /// toggle is a no-op for canonical input. For non-canonical
    /// inputs (e.g. d8-emitted DEX whose map_list order is consumed
    /// by a downstream tool that expects byte-identical round-trip),
    /// flipping this on preserves the section ordering at the
    /// expense of spec-strict map ordering.
    pub preserve_map_list_order: bool,

    /// When `true`, emit each `encoded_value` in `encoded_arrays` at
    /// the input's on-disk byte width (read from
    /// `dex.encoded_array_widths`) instead of always picking the
    /// minimum representable width. Default `false` — preserves the
    /// established min-width canonicalization. When `true`,
    /// [`CanonicalTransform::EncodedValueReencoded`] is never pushed
    /// because emit did not re-encode.
    ///
    /// Spec note: DEX §VII.1 encodes the value's payload byte width
    /// in the upper 3 bits of the header (`value_arg`), which the
    /// reader uses verbatim — both min-width and wider widths are
    /// spec-valid as long as the encoded value fits. When the
    /// requested input width cannot represent the IR value (rare;
    /// implies IR was mutated after parse), emit falls back to
    /// min-width per-value to avoid silent truncation. Applies to
    /// encoded_array top-level values only; nested values
    /// (Array/Annotation children) and Byte/Boolean/Null/Float/Double
    /// (fixed-width by spec) are unaffected.
    pub preserve_encoded_value_width: bool,

    /// When `true`, emit places each data-section subsection at the
    /// offset recorded in the input's `dex.map_entries` instead of
    /// laying sections out sequentially in `emit_dex_inner`'s canonical
    /// order. Default `false` — preserves the established canonical
    /// layout. When `true`, [`CanonicalTransform::DataSectionLayoutReordered`]
    /// is never pushed because emit did not reorder.
    ///
    /// This is the dominant byte-identity gauge knob: corpus analysis
    /// found that 99.9% of DEX have an input data-section subsection
    /// order that differs from emit's canonical sequential order, and
    /// that ~0% of those achieve byte-identity by default. Under
    /// preserve mode, the predicted byte-identity rate with the guard
    /// is ≥95% (target 100% modulo parse/emit failures).
    ///
    /// Sections present in the input's `map_entries` but not produced
    /// by emit (e.g., input had an `ANNOTATION_SET_ITEM` section the
    /// IR no longer holds) are silently skipped. Sections produced by
    /// emit but not in input (rare; implies IR was mutated post-parse)
    /// are appended at the end of the data segment in canonical order
    /// — fallback. Strict 1:1 round-trip is the common case.
    pub preserve_data_section_layout: bool,
    /// When `true`, after `finalize_checksums` recomputes the Adler-32
    /// and SHA-1 over the output bytes, OVERWRITE `bytes[8..32]` with
    /// `dex.header.checksum` + `dex.header.signature` (the values
    /// parsed from the input header).
    ///
    /// **Trap.** This produces DEX bytes that **Android's loader will
    /// reject** if the input had non-canonical checksums (the case
    /// this flag exists to handle). The flag exists solely for
    /// byte-identity testing / source-faithful round-trip
    /// reconstruction — NOT for producing DEX that will actually be
    /// loaded.
    ///
    /// Background: Large corpus measurements found cases where the
    /// input's stored SHA-1 didn't match SHA-1(input[32..]) — the
    /// input was produced by a tool that wrote a stale/wrong SHA.
    /// Adler-32 was computed over those wrong-SHA bytes, so input
    /// was self-consistent but non-canonical. Default emit (`false`)
    /// recomputes correct
    /// checksums — output is valid DEX but differs from input bytes.
    /// With this flag `true`, output is byte-identical to input but
    /// inherits whatever defect the input had.
    ///
    /// **Always-false in production paths.** Set only by byte-identity
    /// audit harnesses (`corpus_tier_ladder`, etc.) where the test
    /// frame already accepts non-loadable output as the comparison
    /// subject.
    pub preserve_input_checksums: bool,
}

/// A canonical transformation emit actually applied to the input on
/// its way to output bytes. Each variant in
/// [`EmitOutput::applied_transformations`] is a concrete, observed
/// contribution to `output_bytes != input_bytes` — never an
/// approximation. If a variant is absent from the vec, emit did NOT
/// perform that transformation on this input; byte-identity at that
/// site is not blocked by it.
///
/// Observation discipline: a variant is pushed only when emit can
/// prove the transformation changed bytes (e.g. re-sorting reordered
/// the pool; alignment insertion wrote non-zero padding). Pushing
/// speculatively — "emit could have reordered if the input were
/// unsorted" — is a bug.
///
/// Classification (retire / fundamental) is a doc-only property of
/// each variant. The enum is `#[non_exhaustive]`; new variants are
/// added as new observation paths land.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CanonicalTransform {
    /// Retired variant. Was: strip `debug_info_item` + zero
    /// `debug_info_off`. Superseded by raw-bytes preservation via
    /// `DexFile.debug_info_raw_bytes`. Emit MUST NOT push this variant;
    /// retained so older byte-diff traces stay decodable.
    DebugInfoStripped,

    /// Emit had to reorder the string pool into UTF-16 code-unit
    /// order (DEX spec §"string_ids"). Observed at `emit_dex_collect`
    /// by comparing the parsed IR's string order against a sorted
    /// copy; pushed only when they differ. On a well-formed input
    /// this variant never appears.
    StringPoolReordered,

    /// Emit inserted alignment padding at a 4-byte-aligned section
    /// boundary AND the resulting section start offset differs from
    /// the input's section start offset for the same section. Pushed
    /// per-section by `emit_dex_inner`; the discriminating predicate
    /// is `emit_section_base != input_section_offset` (NOT just
    /// `pad > 0`) — a byte-identical round-trip preserves both pad
    /// counts AND offsets, so the variant correctly never appears
    /// on byte-identical output (preserves the
    /// `byte_identical ⇒ applied_transformations.is_empty()`
    /// invariant the proptest gate enforces).
    ///
    /// Alignment is DEX-spec-mandated and cannot be elided. The
    /// observation surfaces upstream canonicalizations (e.g. string
    /// pool reorder shifted section sizes, displacing all downstream
    /// section starts) that this variant attributes downstream.
    ///
    /// `byte_count` is the number of zero bytes emit inserted at this
    /// boundary. Always non-zero when pushed (zero pad ⇒ no insertion
    /// ⇒ no canonicalization).
    AlignmentPaddingInserted {
        section: AlignmentSection,
        byte_count: u32,
    },

    /// Emit's offset-sort actually reordered the input's `map_list`.
    /// The input was non-canonical (d8/dexopt may emit `map_items`
    /// in build-order rather than offset-order); emit's
    /// `items.sort_by_key(|m| m.offset)` at `build_map_items` produces
    /// the canonical (offset-sorted) order. Pushed iff the type-code
    /// sequence differs between input's `dex.map_entries` order and
    /// emit's post-sort order, filtered to the intersection (types
    /// appearing in BOTH — additive sections that emit produces but
    /// input lacks, or vice versa, don't constitute reordering).
    ///
    /// Observation-only — preservation (an `EmitConfig::preserve_map_list_order`
    /// toggle that retains input order) is not yet implemented.
    ///
    /// Invariant: for byte-identical round-trip, input map_list bytes
    /// equal emit map_list bytes, so the type_code sequences match
    /// → variant NOT pushed → `byte_identical ⇒ applied_transformations.is_empty()`
    /// invariant holds.
    MapListReordered,

    /// Emit re-encoded one or more `encoded_value` payloads at a
    /// narrower byte width than the input (`emit_encoded_value` always
    /// picks the minimum legal width; input may have used a wider
    /// representation). DEX spec §VII.1 allows any width in `[1,8]`
    /// for a numeric type as long as the value round-trips; min-width
    /// is canonical but the spec accepts wider forms.
    ///
    /// `count` is the number of `encoded_value` payloads in
    /// `dex.encoded_arrays` (D6 scope: static-field defaults; the
    /// dominant width-canonicalization surface) where input width
    /// strictly exceeded emit's chosen min-width.
    /// Variable-length payloads (`Array`, `Annotation`) are excluded.
    /// Float and Double are always emitted at full width (4/8 bytes);
    /// they never report divergence.
    ///
    /// Pushed iff `count > 0`. Invariant preservation:
    /// `byte_identical ⇒ applied_transformations.is_empty()` —
    /// byte-identical means emit matched input width-for-width →
    /// `count == 0` → variant NOT pushed.
    ///
    /// Annotation values + call_site encoded_arrays are NOT counted in
    /// this variant (out of D6 scope; widths aren't tracked there;
    /// not yet tracked here).
    EncodedValueReencoded {
        count: u32,
    },

    /// Emit placed data-section subsections in a different order than
    /// the input's `dex.map_entries` recorded. Cascades through every
    /// embedded offset reference (class_data.code_off,
    /// code_item.debug_info_off, class_def.static_values_off, etc.) →
    /// massive byte divergence in offset-bearing sections even on
    /// otherwise canonical input. This variant attributes that single
    /// root cause.
    ///
    /// Pushed by `emit_dex_inner` when the input's data-section
    /// subsection order differs from emit's canonical sequential
    /// order, AND `EmitConfig::preserve_data_section_layout` is false.
    /// Under preserve mode the variant is NOT pushed (emit honored
    /// input layout). On a well-formed canonical input where emit's
    /// canonical order happens to match input's, this variant also
    /// stays absent.
    ///
    /// Per corpus analysis, this variant fires on most corpus DEX
    /// under default emit; preserve mode brings byte-identity to ≥95%
    /// (target 100%).
    DataSectionLayoutReordered,

    /// Input header's stored Adler-32 + SHA-1 don't match the bytes
    /// they're supposed to cover (`Adler-32(input[12..])` and
    /// `SHA-1(input[32..])`). Emit recomputes correct checksums on
    /// output — the output is therefore valid loadable DEX that
    /// differs from source in `bytes[8..32]` (24 bytes of header).
    ///
    /// Pushed by `derive_applied_transformations` when
    /// `DexFile.input_checksums_canonical == false` AND
    /// `EmitConfig::preserve_input_checksums == false`. Under
    /// preserve_input_checksums = true, the checksums are copied
    /// verbatim and this variant is NOT pushed (no normalization
    /// happened).
    ///
    /// Background: Large-scale corpus testing showed 5.4% of DEXes
    /// ship with non-canonical checksums in their input headers —
    /// droidsaw is doing the right thing by recomputing them, but
    /// the byte-identity number understates because of it. This
    /// variant makes the correction explicit-and-attributed rather
    /// than silent-but-byte-divergent.
    InputChecksumNormalized,
}

/// Per-`align_up_u32`-site identifier for `CanonicalTransform::AlignmentPaddingInserted`.
/// Each variant maps to one of the 6 4-byte-aligned section boundaries
/// in `emit_dex_inner` (verified via `grep -n "_pad\s*=" src/emit_dex.rs`).
///
/// The variant set is `#[non_exhaustive]` against future emit-side
/// alignment additions (e.g. an 8-byte-aligned section); attribution
/// callers must handle unknown variants per `non_exhaustive` semantics.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AlignmentSection {
    /// Between `string_data` and `code_item_section`. DEX spec
    /// §"code_item" mandates 4-byte alignment.
    CodeItem,
    /// Between `annotation_item_section` and `annotation_set_section`.
    /// DEX spec §"annotation_set_item" mandates 4-byte alignment.
    AnnotationSet,
    /// Between `annotation_set_section` and `annotation_set_ref_list_section`.
    /// DEX spec §"annotation_set_ref_list" mandates 4-byte alignment.
    AnnotationSetRefList,
    /// Between `annotation_set_ref_list_section` and `annotation_directory_section`.
    /// DEX spec §"annotation_directory_item" mandates 4-byte alignment.
    AnnotationDirectory,
    /// Between `call_site_data_section` and `type_list_section`.
    /// DEX spec §"type_list" mandates 4-byte alignment.
    TypeList,
    /// Between `type_list_section` and `map_list`.
    /// DEX spec §"map_list" mandates 4-byte alignment.
    MapList,
}

impl AlignmentSection {
    /// `map_list` `type_code` for the corresponding section. Used by
    /// `input_section_offset` to look up the input's section start.
    /// Returns `None` for sections that don't have a one-to-one
    /// `map_list` correspondence.
    fn map_type_code(self) -> Option<u16> {
        match self {
            // DEX spec §"map_list" type codes (subset relevant here).
            AlignmentSection::CodeItem => Some(0x2001),
            AlignmentSection::AnnotationSet => Some(0x1003),
            AlignmentSection::AnnotationSetRefList => Some(0x1002),
            AlignmentSection::AnnotationDirectory => Some(0x2006),
            AlignmentSection::TypeList => Some(0x1001),
            AlignmentSection::MapList => Some(0x1000),
        }
    }
}

/// Look up the input's section start offset for a given alignment-padded
/// section. Reads from `dex.map_entries`, which the parser populates
/// eagerly from the input's `map_list`. Returns `None` when the input
/// had no map entry for this section (rare; pre-O DEXes may lack some
/// sections entirely).
///
/// Returning `None` is treated as "input layout unknown" by the
/// observation logic; the variant is NOT pushed in that case (we cannot
/// prove a divergence without the input baseline).
fn input_section_offset(dex: &crate::parser::DexFile, section: AlignmentSection) -> Option<u32> {
    let target = section.map_type_code()?;
    dex.map_entries
        .iter()
        .find(|e| e.type_code == target)
        .map(|e| e.offset)
}

/// Returns true iff emit's offset-sorted `map_items` differ from the
/// input's `dex.map_entries` order on the SUBSET of map types appearing
/// in BOTH (apples-to-apples comparison; sections that emit produces
/// but input lacks — or vice versa — don't constitute reordering).
///
/// Used by `emit_dex_inner` to push `CanonicalTransform::MapListReordered`
/// when emit's `items.sort_by_key(|m| m.offset)` at `build_map_items`
/// actually changed the type-code sequence vs the input. d8/dexopt
/// are known to emit `map_items` in build-order rather than offset-
/// order, which the spec then requires emit to canonicalize.
///
/// **Invariant preservation**: for byte-identical round-trip, input
/// map_list bytes equal emit map_list bytes byte-for-byte, so the
/// type_code sequences match → returns false → variant NOT pushed →
/// the `byte_identical ⇒ applied_transformations.is_empty()`
/// invariant holds (per `roundtrip_proptest::parse_emit_byte_identity_consistency`).
/// Reorder `items` (in `build_map_items`'s push-order) to match the
/// input's `dex.map_entries` type-code sequence. Used under
/// `EmitConfig::preserve_map_list_order = true` to retain the input's
/// on-disk section ordering for byte-identity roundtrips.
///
/// Items present in `input_entries` but not in `items` are dropped
/// (the input had a section we no longer produce — e.g., an
/// `ANNOTATION_SET_ITEM` section the input declared but had zero
/// entries for, and we collapsed). Items present in `items` but not
/// in `input_entries` are appended at the end (we added a section the
/// input lacked — fallback, since every output section MUST have a
/// map entry per spec §7.18).
///
/// In the 1:1 roundtrip case (we parsed this DEX, we emit it back),
/// `items`'s type-code set matches `input_entries`'s type-code set,
/// and the output is `input_entries`-ordered.
fn reorder_map_items_to_input(
    items: Vec<MapItem>,
    input_entries: &[crate::parser::MapEntry],
) -> Vec<MapItem> {
    use std::collections::HashMap;
    let mut by_type: HashMap<u16, MapItem> =
        items.into_iter().map(|m| (m.type_code, m)).collect();
    let mut out = Vec::with_capacity(input_entries.len());
    for entry in input_entries {
        if let Some(item) = by_type.remove(&entry.type_code) {
            out.push(item);
        }
    }
    // Any items not in input (rare: emit produced a section input lacked)
    // get appended. Order among the remainder is HashMap iteration order
    // — non-deterministic, but the roundtrip case never hits this branch.
    out.extend(by_type.into_values());
    out
}

fn map_list_order_diverged(
    emit_items: &[MapItem],
    input_entries: &[crate::parser::MapEntry],
) -> bool {
    use std::collections::BTreeSet;
    let emit_types: BTreeSet<u16> = emit_items.iter().map(|m| m.type_code).collect();
    let input_types: BTreeSet<u16> = input_entries.iter().map(|e| e.type_code).collect();
    let common: BTreeSet<u16> = emit_types.intersection(&input_types).copied().collect();
    if common.is_empty() {
        // Either side has zero map entries — no comparison possible.
        // Conservative: do not push variant.
        return false;
    }
    let emit_filtered: Vec<u16> = emit_items
        .iter()
        .filter(|m| common.contains(&m.type_code))
        .map(|m| m.type_code)
        .collect();
    let input_filtered: Vec<u16> = input_entries
        .iter()
        .filter(|e| common.contains(&e.type_code))
        .map(|e| e.type_code)
        .collect();
    emit_filtered != input_filtered
}

/// Structured emit result — bytes plus the canonical transformations
/// emit applied to produce them. Returned by [`emit_dex_collect`];
/// callers that only want bytes should use [`emit_dex`].
///
/// `applied_transformations` is empty iff emit made no byte-changing
/// canonicalizations on this input. When empty, byte-identity is
/// achievable; when non-empty, each variant names a concrete reason
/// it is not.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EmitOutput {
    pub bytes: Vec<u8>,
    pub applied_transformations: Vec<CanonicalTransform>,
}

/// A non-decreasing sequence — the gauge-fix for the DEX format's
/// lexicographic-ordering requirements on `string_ids`, `type_ids`,
/// `proto_ids`, `field_ids`, `method_ids`.
///
/// # Invariant
///
/// For every `i < j` in `0..self.len()`, `self[i] <= self[j]`.
///
/// # Construction
///
/// - [`NonDecreasing::from_sorted`] consumes an unsorted `Vec<T>`,
///   sorts in place, and returns a guaranteed-canonical wrapper. This
///   is the primary constructor — emit code calls this when building
///   an id pool from parsed IR.
///
/// - [`NonDecreasing::from_verified`] accepts a `Vec<T>` claimed to be
///   already sorted and verifies it, returning an
///   [`OrderingViolation`] if not. Use when a prior stage already
///   produced canonical order and resorting would be wasteful.
///
/// Both constructors are the only public ways to build a
/// `NonDecreasing<T>` — the inner `Vec` is private, so the type system
/// forbids skipping the invariant.
///
/// # Access
///
/// `NonDecreasing<T>` derefs to `[T]`, so `.iter()`, `.len()`, `[idx]`,
/// `.get(idx)`, etc. all work without ceremony.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonDecreasing<T: Ord> {
    inner: Vec<T>,
}

impl<T: Ord> NonDecreasing<T> {
    /// Sort `inner` in place and wrap. The resulting `NonDecreasing`
    /// satisfies the non-decreasing invariant by construction.
    pub fn from_sorted(mut inner: Vec<T>) -> Self {
        inner.sort();
        Self { inner }
    }

    /// Accept a `Vec<T>` whose caller claims canonical order; verify
    /// and wrap. Returns [`OrderingViolation`] (with the offending
    /// index) if the claim fails.
    pub fn from_verified(inner: Vec<T>) -> Result<Self, OrderingViolation> {
        for pair in inner.windows(2).enumerate() {
            let (i, w) = pair;
            if w[0] > w[1] {
                return Err(OrderingViolation {
                    index: i.saturating_add(1),
                });
            }
        }
        Ok(Self { inner })
    }

    /// Consume the wrapper and return the inner vec. The caller takes
    /// on responsibility for the invariant; use only when a subsequent
    /// gauge-preserving operation will re-wrap.
    pub fn into_inner(self) -> Vec<T> {
        self.inner
    }
}

impl<T: Ord> std::ops::Deref for NonDecreasing<T> {
    type Target = [T];
    fn deref(&self) -> &[T] {
        &self.inner
    }
}

impl<'a, T: Ord> IntoIterator for &'a NonDecreasing<T> {
    type Item = &'a T;
    type IntoIter = std::slice::Iter<'a, T>;
    fn into_iter(self) -> Self::IntoIter {
        self.inner.iter()
    }
}

/// The `Vec<T>` handed to [`NonDecreasing::from_verified`] was not in
/// canonical order. `index` is the position of the first violating
/// element — i.e., `inner[index - 1] > inner[index]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderingViolation {
    pub index: usize,
}

impl fmt::Display for OrderingViolation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "non-decreasing invariant violated at index {}",
            self.index
        )
    }
}

/// A strictly-ascending sequence — the gauge-fix for DEX format
/// positions that forbid duplicates, notably `sparse-switch-payload`
/// keys (spec §6: the VM does binary search; duplicate keys would
/// yield nondeterministic dispatch).
///
/// # Invariant
///
/// For every `i < j` in `0..self.len()`, `self[i] < self[j]` (strict).
///
/// # Construction
///
/// - [`StrictlyAscending::from_sorted`] consumes an unsorted `Vec<T>`,
///   sorts in place, and *verifies* uniqueness. Unlike
///   [`NonDecreasing::from_sorted`] this can fail — sorting does not
///   remove duplicates, so a duplicate in the input becomes an
///   [`OrderingViolation`]. Callers who want dedup semantics must
///   explicitly dedup before construction.
///
/// - [`StrictlyAscending::from_verified`] accepts a `Vec<T>` claimed
///   strict-ascending and verifies it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StrictlyAscending<T: Ord> {
    inner: Vec<T>,
}

impl<T: Ord> StrictlyAscending<T> {
    /// Sort `inner` in place and verify strict ascension (i.e., no
    /// duplicates). A duplicate in the input yields
    /// [`OrderingViolation`] at the index of the first repeat.
    pub fn from_sorted(mut inner: Vec<T>) -> Result<Self, OrderingViolation> {
        inner.sort();
        for (i, w) in inner.windows(2).enumerate() {
            if w[0] == w[1] {
                return Err(OrderingViolation {
                    index: i.saturating_add(1),
                });
            }
        }
        Ok(Self { inner })
    }

    /// Accept a `Vec<T>` whose caller claims strict-ascending order;
    /// verify and wrap. Returns [`OrderingViolation`] for any pair
    /// `inner[i-1] >= inner[i]` (i.e., either out-of-order OR duplicate).
    pub fn from_verified(inner: Vec<T>) -> Result<Self, OrderingViolation> {
        for (i, w) in inner.windows(2).enumerate() {
            if w[0] >= w[1] {
                return Err(OrderingViolation {
                    index: i.saturating_add(1),
                });
            }
        }
        Ok(Self { inner })
    }

    /// Empty sequence — the only construction that's free of ordering
    /// concerns. Useful for typed emits of zero-length payloads.
    pub fn empty() -> Self {
        Self { inner: Vec::new() }
    }

    pub fn into_inner(self) -> Vec<T> {
        self.inner
    }
}

impl<T: Ord> std::ops::Deref for StrictlyAscending<T> {
    type Target = [T];
    fn deref(&self) -> &[T] {
        &self.inner
    }
}

impl<'a, T: Ord> IntoIterator for &'a StrictlyAscending<T> {
    type Item = &'a T;
    type IntoIter = std::slice::Iter<'a, T>;
    fn into_iter(self) -> Self::IntoIter {
        self.inner.iter()
    }
}

impl std::error::Error for OrderingViolation {}

/// Serialize a parsed `DexFile` back to DEX bytes.
///
/// See module docs for the round-trip contract + trust boundary + the
/// `NonDecreasing` gauge-fix.
///
/// ## Scope
///
/// Full data-section emit. Every DEX subsection is supported:
/// strings, types, protos (with parameters), fields, methods, class
/// defs (with interfaces + class_data + annotations + static_values),
/// class_data (with code_items), code_items (with payloads + tries +
/// catch_handlers), annotations (directory + set + set_ref_list +
/// item), encoded_arrays, type_lists, map_list.
///
/// `debug_info_item` round-trip is implemented: `debug_info_off` now
/// points into the emitter-owned debug info region (see
/// `emit_debug_info_section`).
pub fn emit_dex(dex: &DexFile) -> Result<Vec<u8>, DexEmitError> {
    emit_dex_with_config(dex, &EmitConfig::default())
}

/// Emit a DEX file with an explicit [`EmitConfig`]. Use when the
/// caller wants to override the strict-mode defaults (e.g. permit
/// partial IR on a normalization pipeline that deliberately drops
/// malformed subsections).
///
/// The `emit_dex` free function is a thin wrapper passing
/// `EmitConfig::default()` (strict mode).
pub fn emit_dex_with_config(
    dex: &DexFile,
    config: &EmitConfig,
) -> Result<Vec<u8>, DexEmitError> {
    emit_dex_collect(dex, config).map(|o| o.bytes)
}

/// Full-fidelity emit entry point. Returns an [`EmitOutput`] that
/// bundles the output bytes with the set of canonical transformations
/// emit actually applied. Prefer over [`emit_dex_with_config`] when
/// the caller wants to reason about byte-identity or attribute
/// divergence.
///
/// Attribution is observation-only — see [`CanonicalTransform`].
/// A variant appears in `applied_transformations` iff emit changed
/// bytes at that site on this input. An empty vec means emit made no
/// canonicalizing byte changes; byte-identity is achievable and the
/// proptest `parse_emit_byte_identity_consistency` enforces the
/// converse: if bytes match, the vec must be empty.
pub fn emit_dex_collect(
    dex: &DexFile,
    config: &EmitConfig,
) -> Result<EmitOutput, DexEmitError> {
    let (bytes, mut applied_transformations) = emit_dex_inner(dex, config)?;
    // Merge IR-derived (post-emit) attributions with per-emit-site
    // tracker observations. Order matters for determinism: IR-derived
    // first (string-pool reorder is a "whole-file" canonicalization),
    // then per-site (alignment padding etc.) in emission order. Both
    // arms are observation-only; neither claims preservation.
    applied_transformations.extend(derive_applied_transformations(dex, config));
    // Reorder so IR-derived variants come first, preserving emission
    // order within each group. This keeps `applied_transformations`
    // stable across IR mutations that don't affect emit-site tracking.
    //
    // NOTE: extend the match arm when a second IR-derived variant
    // lands. Today only `StringPoolReordered` is IR-derived (built
    // post-emit by `derive_applied_transformations`); future variants
    // added to `derive_applied_transformations` MUST also be added
    // to this match or they'll silently be classified as per-site
    // and reorder out of expected position.
    let (mut ir_derived, per_site): (Vec<_>, Vec<_>) = applied_transformations
        .into_iter()
        .partition(|t| matches!(t, CanonicalTransform::StringPoolReordered));
    ir_derived.extend(per_site);
    let applied_transformations = ir_derived;
    Ok(EmitOutput {
        bytes,
        applied_transformations,
    })
}

/// Observe which byte-changing canonicalizations emit actually
/// performed on this input. A variant is pushed only when there is
/// concrete evidence that the transformation changed bytes — never
/// speculatively.
///
/// Today the only observable transform is `StringPoolReordered`:
/// compare the parsed IR's string pool against its sorted form; the
/// sort reordered iff they differ. Emit's
/// [`NonDecreasing::from_sorted`] is a no-op on already-sorted input,
/// so "sort would have reordered" exactly captures "bytes changed at
/// this site."
///
/// Additional variants (map_list order, alignment padding, etc.) land
/// alongside their observation paths as per-canonicalization work proceeds.
/// Observation for map_list and alignment requires threading
/// tracker state through `emit_dex_inner` — deferred.
fn derive_applied_transformations(dex: &DexFile, config: &EmitConfig) -> Vec<CanonicalTransform> {
    let mut out = Vec::new();
    if verify_utf16_sorted(&dex.strings).is_err() {
        out.push(CanonicalTransform::StringPoolReordered);
    }
    // Skip the EncodedValueReencoded attribution when the toggle preserves
    // input widths: emit did NOT re-encode in that case.
    if !config.preserve_encoded_value_width {
        let reencoded = count_encoded_value_width_divergences(dex);
        if reencoded > 0 {
            out.push(CanonicalTransform::EncodedValueReencoded { count: reencoded });
        }
    }
    // Input had non-canonical Adler/SHA in its header. Default emit
    // recomputes correct values; preserve_input_checksums opts to keep
    // input verbatim. Attribute only when emit normalized.
    if !dex.input_checksums_canonical && !config.preserve_input_checksums {
        out.push(CanonicalTransform::InputChecksumNormalized);
    }
    out
}

/// Count `encoded_value` payloads where the input's byte width
/// (recorded by the parser into `dex.encoded_array_widths`) strictly
/// exceeds emit's chosen min-width. Variable-length and full-width
/// types (Array / Annotation / Float / Double / Null / Boolean)
/// don't contribute (their min-width is fixed or undefined).
///
/// Used by `derive_applied_transformations` to populate
/// `CanonicalTransform::EncodedValueReencoded { count }` per D6 of
/// v1 release path.
///
/// **Invariant preservation**: byte-identical round-trip ⟹ emit
/// width matches input width for every encoded_value ⟹ count == 0
/// ⟹ variant NOT pushed ⟹ `byte_identical ⇒ applied_transformations.is_empty()`
/// holds.
fn count_encoded_value_width_divergences(dex: &DexFile) -> u32 {
    let mut count: u32 = 0;
    // Walk each encoded_array's tree in pre-order, consuming the
    // matching deep widths in lock-step. Width-bearing primitive
    // (per `is_width_bearing_primitive`) → one width entry; composites
    // / Null / Boolean → zero entries.
    for (key, values) in &dex.encoded_arrays {
        let Some(widths) = dex.encoded_array_widths.get(key) else {
            continue;
        };
        let mut iter = widths.iter();
        for val in values {
            walk_count_divergences(val, &mut iter, &mut count);
        }
    }
    // Also count nested-width divergences inside annotation_items.
    for (key, item) in &dex.annotation_items {
        let Some(widths) = dex.annotation_item_widths.get(key) else {
            continue;
        };
        let mut iter = widths.iter();
        for val in item.annotation.elements.values() {
            walk_count_divergences(val, &mut iter, &mut count);
        }
    }
    count
}

/// Recurse one EncodedValue in lock-step with the deep widths iterator,
/// counting positions where input_width > min_emit_width.
fn walk_count_divergences<'a>(
    val: &EncodedValue,
    widths: &mut impl Iterator<Item = &'a u8>,
    count: &mut u32,
) {
    match val {
        EncodedValue::Array(children) => {
            for c in children {
                walk_count_divergences(c, widths, count);
            }
        }
        EncodedValue::Annotation(ann) => {
            for c in ann.elements.values() {
                walk_count_divergences(c, widths, count);
            }
        }
        EncodedValue::Null | EncodedValue::Boolean(_) => {
            // No width entry for these.
        }
        _ => {
            let Some(&input_width) = widths.next() else {
                return;
            };
            let Some(min_width) = encoded_value_min_emit_width(val) else {
                return;
            };
            if input_width > min_width {
                *count = count.saturating_add(1);
            }
        }
    }
}

/// Return emit's min-width choice for a numeric/index encoded_value.
/// Returns `None` for variable-length / full-width / fixed-shape
/// types where width comparison is not meaningful.
///
/// Mirrors `emit_encoded_value`'s width selection (uses
/// `min_signed_bytes` / `min_unsigned_bytes` / fixed widths). Kept
/// in lock-step with `emit_encoded_value` — any change
/// to that match-arm's width logic must reflect here.
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: min_signed_bytes / min_unsigned_bytes both return a value in [1, 8] (clamp(1,8) guarantees this); `as u8` narrowing from usize is exact since max value is 8."
)]
fn encoded_value_min_emit_width(val: &EncodedValue) -> Option<u8> {
    match val {
        EncodedValue::Byte(_) => Some(1),
        EncodedValue::Short(s) => Some(min_signed_bytes(i64::from(*s)) as u8),
        EncodedValue::Char(c) => Some(min_unsigned_bytes(u64::from(*c)) as u8),
        EncodedValue::Int(i) => Some(min_signed_bytes(i64::from(*i)) as u8),
        EncodedValue::Long(l) => Some(min_signed_bytes(*l) as u8),
        // Float and Double always emit at full width per
        // `emit_encoded_value` (DEX spec allows trailing-
        // zero trimming but emit chose always-full); width is fixed.
        EncodedValue::Float(_) => Some(4),
        EncodedValue::Double(_) => Some(8),
        EncodedValue::String(s) => Some(min_unsigned_bytes(u64::from(s.0)) as u8),
        EncodedValue::Type(t) => Some(min_unsigned_bytes(u64::from(t.0)) as u8),
        EncodedValue::Field(f) => Some(min_unsigned_bytes(u64::from(f.0)) as u8),
        EncodedValue::Method(m) => Some(min_unsigned_bytes(u64::from(m.0)) as u8),
        EncodedValue::Enum(f) => Some(min_unsigned_bytes(u64::from(f.0)) as u8),
        // VALUE_METHOD_TYPE / VALUE_METHOD_HANDLE are width-variable
        // index types — emit picks `min_unsigned_bytes(idx as u64)`
        // (mirror at `emit_encoded_value` arms for
        // VT_METHOD_TYPE / VT_METHOD_HANDLE). Width comparison applies.
        EncodedValue::MethodType(p) => Some(min_unsigned_bytes(u64::from(p.0)) as u8),
        EncodedValue::MethodHandle(h) => Some(min_unsigned_bytes(u64::from(h.0)) as u8),
        // Variable-length / fixed-shape: width comparison meaningless.
        EncodedValue::Array(_)
        | EncodedValue::Annotation(_)
        | EncodedValue::Null
        | EncodedValue::Boolean(_) => None,
    }
}

/// Look up an input section's offset by `map_type` code from
/// `dex.map_entries`. Returns `None` if absent.
fn input_offset_by_code(dex: &DexFile, code: u16) -> Option<u32> {
    dex.map_entries
        .iter()
        .find(|e| e.type_code == code)
        .map(|e| e.offset)
}

/// True iff input's data-section subsection order (sorted by offset,
/// filtered to data-section type_codes) differs from emit's canonical
/// sequential order.
fn data_section_order_diverges_from_emit(dex: &DexFile) -> bool {
    use std::collections::BTreeSet;
    // Canonical sequential order emit produces by default (lines 1075-1370):
    // string_data, code_item, debug_info, class_data, annotation_item,
    // annotation_set, annotation_set_ref_list, annotation_directory,
    // encoded_array (incl. call_site_data), type_list, map_list.
    let canonical_order: &[u16] = &[
        map_type::STRING_DATA_ITEM,
        map_type::CODE_ITEM,
        map_type::DEBUG_INFO_ITEM,
        map_type::CLASS_DATA_ITEM,
        map_type::ANNOTATION_ITEM,
        map_type::ANNOTATION_SET_ITEM,
        map_type::ANNOTATION_SET_REF_LIST,
        map_type::ANNOTATION_DIRECTORY_ITEM,
        map_type::ENCODED_ARRAY_ITEM,
        map_type::TYPE_LIST,
        map_type::MAP_LIST,
    ];
    let canonical_set: BTreeSet<u16> = canonical_order.iter().copied().collect();
    let mut input_sorted: Vec<crate::parser::MapEntry> = dex.map_entries.clone();
    input_sorted.sort_by_key(|e| e.offset);
    let input_data_order: Vec<u16> = input_sorted
        .into_iter()
        .filter(|e| canonical_set.contains(&e.type_code))
        .map(|e| e.type_code)
        .collect();
    // Filter the canonical order to only the type_codes present in input
    // (apples-to-apples comparison — sections in canonical but absent in
    // input don't constitute reordering).
    let input_present: BTreeSet<u16> = input_data_order.iter().copied().collect();
    let canonical_filtered: Vec<u16> = canonical_order
        .iter()
        .copied()
        .filter(|tc| input_present.contains(tc))
        .collect();
    input_data_order != canonical_filtered
}

/// Emit `dex` honoring the input's data-section subsection layout.
/// Called from `emit_dex_inner` when `config.preserve_data_section_layout`
/// is set. Closes the byte-identity gap on inputs whose data-section
/// subsection order differs from emit's canonical sequential order
/// (typical for real-world DEX files).
///
/// Differs from the default path in `emit_dex_inner` only in the data
/// section: header pools, proto/class_def offset rewrites, and the
/// final-checksum step are identical. The data section is laid out at
/// the input's recorded offsets, and the output buffer is pre-sized to
/// `dex.header.file_size` with sections written at preserved positions
/// rather than appended sequentially.
///
/// Falls back to the default path when the input's `map_entries` is
/// empty or missing a data subsection that emit would otherwise
/// produce (e.g., IR mutated post-parse to add a section). The fallback
/// is silent — caller gets a `CanonicalTransform::DataSectionLayoutReordered`
/// in the result if they want to detect that preservation didn't apply.
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::too_many_lines,
    reason = "PROOF: same narrowing-from-u32-pool-size patterns as `emit_dex_inner`. Function is long because it mirrors the layout phases; isolated for clarity rather than weaving conditionals through the 850-line default path."
)]
fn emit_dex_inner_preserve_layout(
    dex: &DexFile,
    config: &EmitConfig,
) -> Result<(Vec<u8>, Vec<CanonicalTransform>), DexEmitError> {
    // Preserve mode requires the input to have map_entries (parser
    // populates it eagerly). If absent (hand-built IR), fall back to
    // the default path — content-equivalence still holds.
    if dex.map_entries.is_empty() {
        let mut cfg_no_preserve = config.clone();
        cfg_no_preserve.preserve_data_section_layout = false;
        return emit_dex_inner(dex, &cfg_no_preserve);
    }

    let mut emit_transforms: Vec<CanonicalTransform> = Vec::new();

    // ── Phase A: type idxs + header pools (same as default path) ──
    use std::collections::HashMap;
    let string_to_idx: HashMap<&str, StringIdx> = dex
        .strings
        .iter()
        .enumerate()
        .map(|(i, s)| (s.as_str_lossy(), StringIdx(i as u32)))
        .collect();
    let mut type_idxs: Vec<StringIdx> = Vec::with_capacity(dex.type_descriptors.len());
    for desc in &dex.type_descriptors {
        let idx = *string_to_idx.get(desc.as_str()).ok_or(
            DexEmitError::UnrepresentableIR {
                why: "emit_dex: type_descriptor not found in strings pool (IR is internally inconsistent)",
            }
        )?;
        type_idxs.push(idx);
    }
    let raw_bytes_owned: Vec<&[u8]> = dex.strings.iter().map(crate::DexString::raw_bytes).collect();
    // Compute string_data section size from input map_entries (next-section
    // offset minus string_data start). Needed for the preserve variant
    // to pre-size the blob.
    let string_data_input_base = input_offset_by_code(dex, map_type::STRING_DATA_ITEM);
    let preserve_strings = dex.string_data_offs.len() == dex.strings.len()
        && string_data_input_base.is_some()
        && !dex.strings.is_empty();
    let (string_id_items, string_data_items) = if preserve_strings {
        let sd_base = string_data_input_base.unwrap_or(0);
        // Find section size from sorted map_entries: next entry after
        // STRING_DATA's offset.
        let mut sorted = dex.map_entries.clone();
        sorted.sort_by_key(|e| e.offset);
        let next_off = sorted.iter()
            .find(|e| e.offset > sd_base)
            .map(|e| e.offset)
            .unwrap_or(dex.header.file_size);
        let sd_size = next_off.saturating_sub(sd_base);
        emit_string_pool_preserve_layout(&raw_bytes_owned, &dex.string_data_offs, sd_base, sd_size)?
    } else {
        emit_string_pool(&raw_bytes_owned)
    };
    let type_id_items = {
        let sorted = NonDecreasing::from_verified(type_idxs).map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "dex.type_descriptors not in canonical order — parser accepts; emit rejects to avoid silent index miscompile",
            }
        })?;
        emit_type_pool(&sorted)
    };
    let field_id_items = emit_field_pool(&dex.fields)?;
    let method_id_items = emit_method_pool(&dex.methods)?;

    // ── Phase B: header-section offsets (canonical fixed layout — header
    // sections come first in any DEX; never reordered by preserve mode) ──
    const PROTO_ID_STRIDE: u32 = 12;
    const CLASS_DEF_STRIDE: u32 = 32;
    let mut off: u32 = DEX_HEADER_SIZE;
    let string_ids_off = if !string_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(string_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow { section: "string_ids", context: "string_ids offset overflow" }
    })?);
    let type_ids_off = if !type_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(type_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow { section: "type_ids", context: "type_ids offset overflow" }
    })?);
    let proto_ids_off = if !dex.protos.is_empty() { off } else { 0 };
    let proto_bytes_len: u32 = u32::try_from(dex.protos.len()).ok()
        .and_then(|n| n.checked_mul(PROTO_ID_STRIDE))
        .ok_or(DexEmitError::OffsetOverflow { section: "proto_ids", context: "proto_ids size overflow" })?;
    off = off.saturating_add(proto_bytes_len);
    let field_ids_off = if !field_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(field_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow { section: "field_ids", context: "field_ids offset overflow" }
    })?);
    let method_ids_off = if !method_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(method_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow { section: "method_ids", context: "method_ids offset overflow" }
    })?);
    let class_defs_off = if !dex.class_defs.is_empty() { off } else { 0 };
    let class_def_bytes_len: u32 = u32::try_from(dex.class_defs.len()).ok()
        .and_then(|n| n.checked_mul(CLASS_DEF_STRIDE))
        .ok_or(DexEmitError::OffsetOverflow { section: "class_defs", context: "class_defs size overflow" })?;
    off = off.saturating_add(class_def_bytes_len);
    let call_site_ids_off = if !dex.call_site_ids.is_empty() { off } else { 0 };
    let call_site_ids_bytes_len: u32 = u32::try_from(dex.call_site_ids.len()).ok()
        .and_then(|n| n.checked_mul(4))
        .ok_or(DexEmitError::OffsetOverflow { section: "call_site_ids", context: "call_site_ids size overflow" })?;
    off = off.saturating_add(call_site_ids_bytes_len);
    let method_handles_off = if !dex.method_handles.is_empty() { off } else { 0 };
    let method_handles_bytes_len: u32 = u32::try_from(dex.method_handles.len()).ok()
        .and_then(|n| n.checked_mul(8))
        .ok_or(DexEmitError::OffsetOverflow { section: "method_handles", context: "method_handles size overflow" })?;
    off = off.saturating_add(method_handles_bytes_len);
    let data_off = off;

    // ── Phase C: preserved data-section bases (from input map_entries) ──
    // If any expected data section is missing from input map_entries,
    // fall back to default path.
    fn require_base(dex: &DexFile, code: u16, label: &'static str) -> Result<Option<u32>, DexEmitError> {
        let _ = label;
        Ok(input_offset_by_code(dex, code))
    }
    let preserved_string_data = require_base(dex, map_type::STRING_DATA_ITEM, "string_data")?;
    let preserved_type_list = require_base(dex, map_type::TYPE_LIST, "type_list")?;
    let preserved_code_item = require_base(dex, map_type::CODE_ITEM, "code_item")?;
    let preserved_debug_info = require_base(dex, map_type::DEBUG_INFO_ITEM, "debug_info")?;
    let preserved_class_data = require_base(dex, map_type::CLASS_DATA_ITEM, "class_data")?;
    let preserved_annotation_item = require_base(dex, map_type::ANNOTATION_ITEM, "annotation_item")?;
    let preserved_annotation_set = require_base(dex, map_type::ANNOTATION_SET_ITEM, "annotation_set")?;
    let preserved_annotation_set_ref_list = require_base(dex, map_type::ANNOTATION_SET_REF_LIST, "annotation_set_ref_list")?;
    let preserved_annotation_directory = require_base(dex, map_type::ANNOTATION_DIRECTORY_ITEM, "annotation_directory")?;
    let preserved_encoded_array = require_base(dex, map_type::ENCODED_ARRAY_ITEM, "encoded_array")?;
    let preserved_map_off = input_offset_by_code(dex, map_type::MAP_LIST)
        .ok_or(DexEmitError::UnrepresentableIR {
            why: "preserve_data_section_layout: input map_entries missing MAP_LIST entry",
        })?;

    // Helper: section base (preserved if present in input, else compute
    // a fallback by appending at end of data segment). Returns the base
    // AND whether the input had a preserved entry (for byte-write).
    // Helper: section size derived from input map_entries (next section's
    // offset minus this section's offset). Returns None if section absent.
    let section_size_from_map = |type_code: u16| -> Option<u32> {
        let base = input_offset_by_code(dex, type_code)?;
        let mut sorted = dex.map_entries.clone();
        sorted.sort_by_key(|e| e.offset);
        let next = sorted.iter().find(|e| e.offset > base).map(|e| e.offset);
        Some(next.unwrap_or(dex.header.file_size).saturating_sub(base))
    };

    // ── Phase D: emit leaf-section bytes (no base needed for emit) ──
    let (type_list_bytes, type_list_remap) = emit_type_list_section(&dex.type_lists)?;
    // code_item under preserve mode: items at input physical offsets.
    let (mut code_item_bytes, code_item_remap) = if let (Some(base), Some(size)) = (
        input_offset_by_code(dex, map_type::CODE_ITEM),
        section_size_from_map(map_type::CODE_ITEM).filter(|_| !dex.code_items.is_empty()),
    ) {
        let mut blob = vec![0u8; size as usize];
        let mut remap = std::collections::BTreeMap::new();
        for (orig_off, ci) in &dex.code_items {
            let local = orig_off.saturating_sub(base);
            remap.insert(*orig_off, local);
            let insns_size = compute_insns_size(&ci.instructions, &ci.payloads)?;
            let insn_bytes = assemble_insn_stream(&ci.instructions, &ci.payloads, insns_size)?;
            let container_bytes = emit_code_item_container(
                ci.registers_size, ci.ins_size, ci.outs_size,
                0, // debug_info_off placeholder; rewritten in Phase E below
                &insn_bytes, &ci.tries, &ci.catch_handlers,
            )?;
            let start = local as usize;
            let end = start.saturating_add(container_bytes.len());
            if end > blob.len() {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "preserve: code_item overruns section",
                });
            }
            blob[start..end].copy_from_slice(&container_bytes);
        }
        (blob, remap)
    } else {
        emit_code_item_section(&dex.code_items)?
    };
    // debug_info under preserve mode: items at input physical offsets.
    // Preserve-mode contract gauge: the input's debug_info section must be
    // a total tiling of {Entry, Gap, UnparseableTail} regions. Any
    // UnparseableTail or layout-gauge failure means the parser couldn't
    // faithfully observe the input bytes; refuse the preserve emit rather
    // than fill the gap with zeros.
    if !dex.debug_info_section_layout.is_empty() {
        let dbg_base = input_offset_by_code(dex, map_type::DEBUG_INFO_ITEM).unwrap_or(0);
        let dbg_end = section_size_from_map(map_type::DEBUG_INFO_ITEM)
            .map(|sz| dbg_base.saturating_add(sz))
            .unwrap_or(dbg_base);
        crate::parser::section_layout_gauge(
            "debug_info",
            dbg_base,
            dbg_end,
            &dex.debug_info_section_layout,
        )
        .map_err(|_| DexEmitError::UnrepresentableIR {
            why: "preserve_data_section_layout: debug_info section_layout_gauge failed (parser could not produce a total tiling — see dex.parse_errors)",
        })?;
        if dex
            .debug_info_section_layout
            .iter()
            .any(|r| r.is_unparseable_tail())
        {
            return Err(DexEmitError::UnrepresentableIR {
                why: "preserve_data_section_layout: debug_info has UnparseableTail regions — input bytes can't be round-tripped under preserve mode",
            });
        }
    }
    // Same gauge for annotation_set (orphan capture via walker; without
    // it the map_list count is off-by-one on inputs that pack a
    // referenced-by-nothing set into the section).
    if !dex.annotation_set_section_layout.is_empty() {
        let set_base = input_offset_by_code(dex, map_type::ANNOTATION_SET_ITEM).unwrap_or(0);
        let set_end = section_size_from_map(map_type::ANNOTATION_SET_ITEM)
            .map(|sz| set_base.saturating_add(sz))
            .unwrap_or(set_base);
        crate::parser::section_layout_gauge(
            "annotation_set",
            set_base,
            set_end,
            &dex.annotation_set_section_layout,
        )
        .map_err(|_| DexEmitError::UnrepresentableIR {
            why: "preserve_data_section_layout: annotation_set section_layout_gauge failed",
        })?;
        if dex
            .annotation_set_section_layout
            .iter()
            .any(|r| r.is_unparseable_tail())
        {
            return Err(DexEmitError::UnrepresentableIR {
                why: "preserve_data_section_layout: annotation_set has UnparseableTail regions",
            });
        }
    }

    let (debug_info_bytes, debug_info_remap) = if let (Some(base), Some(size)) = (
        input_offset_by_code(dex, map_type::DEBUG_INFO_ITEM),
        section_size_from_map(map_type::DEBUG_INFO_ITEM)
            .filter(|_| !dex.debug_info_raw_bytes.is_empty()),
    ) {
        let mut blob = vec![0u8; size as usize];
        let mut remap = std::collections::BTreeMap::new();
        for (orig_off, raw) in &dex.debug_info_raw_bytes {
            let local = orig_off.saturating_sub(base);
            remap.insert(*orig_off, local);
            let start = local as usize;
            let end = start.saturating_add(raw.len());
            if end > blob.len() {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "preserve: debug_info overruns section",
                });
            }
            blob[start..end].copy_from_slice(raw);
        }
        (blob, remap)
    } else {
        emit_debug_info_section(&dex.debug_info_raw_bytes)?
    };
    // annotation_item under preserve mode: lay items out at their input
    // physical offsets (BTreeMap key = input offset). emit_annotation_item_section
    // produces sequential items; we relayout based on input offsets so dependent
    // sections (annotation_set) reference correct absolute positions.
    let (annotation_item_bytes, annotation_item_remap) = {
        let (seq_bytes, _seq_remap) = emit_annotation_item_section(&dex.annotation_items)?;
        let _ = seq_bytes; // discard sequential bytes — recompute at preserved offsets
        if let (Some(base), Some(size)) = (
            input_offset_by_code(dex, map_type::ANNOTATION_ITEM),
            section_size_from_map(map_type::ANNOTATION_ITEM)
                .filter(|_| !dex.annotation_items.is_empty()),
        ) {
            let mut blob = vec![0u8; size as usize];
            let mut remap = std::collections::BTreeMap::new();
            for (orig_off, item) in &dex.annotation_items {
                let local = orig_off.saturating_sub(base);
                remap.insert(*orig_off, local);
                let mut item_bytes = Vec::new();
                // Under preserve_encoded_value_width, recover deep per-
                // value widths captured at parse time. Without this, every
                // nested Float / Double inside the annotation re-emits at
                // full width and breaks byte-identity even when the outer
                // sections match (the residual that plateaued the rate at
                // 72.73%).
                if config.preserve_encoded_value_width {
                    if let Some(widths) = dex.annotation_item_widths.get(orig_off) {
                        emit_annotation_item_with_widths(&mut item_bytes, item, widths)?;
                    } else {
                        item_bytes.push(item.visibility);
                        emit_encoded_annotation(&mut item_bytes, &item.annotation)?;
                    }
                } else {
                    item_bytes.push(item.visibility);
                    emit_encoded_annotation(&mut item_bytes, &item.annotation)?;
                }
                let start = local as usize;
                let end = start.saturating_add(item_bytes.len());
                if end > blob.len() {
                    return Err(DexEmitError::UnrepresentableIR {
                        why: "preserve: annotation_item overruns section",
                    });
                }
                blob[start..end].copy_from_slice(&item_bytes);
            }
            (blob, remap)
        } else {
            emit_annotation_item_section(&dex.annotation_items)?
        }
    };
    let widths_param = if config.preserve_encoded_value_width {
        Some(&dex.encoded_array_widths)
    } else {
        None
    };
    let (encoded_array_bytes, encoded_array_remap) =
        emit_encoded_array_section_with_widths(&dex.encoded_arrays, widths_param)?;

    // Decide section bases. If the section has data but no preserved
    // offset, fall back: emit it sequentially at the end (informational
    // — should not happen on 1:1 round-trip from parsed input).
    let string_data_base = if !string_data_items.is_empty() {
        preserved_string_data.unwrap_or(data_off)
    } else {
        0
    };
    // base_or_required: preserve emit MUST have a non-zero base whenever
    // the section has bytes to emit. Silently falling back to 0 stomps
    // the header (Tier 1 reparse_err discovered via corpus_tier_ladder:
    // 7 files including walmart/classes34.dex started with `00 00 00 00`
    // instead of `64 65 78 0a` because a section with non-empty bytes
    // wrote at offset 0). Turn the silent failure into a typed error
    // so the caller falls back to the non-preserve path instead of
    // producing invalid DEX.
    fn base_or_required(
        preserved: Option<u32>,
        has_data: bool,
        section: &'static str,
    ) -> Result<u32, DexEmitError> {
        match (has_data, preserved) {
            (false, _) => Ok(0),
            (true, Some(b)) if b != 0 => Ok(b),
            (true, _) => Err(DexEmitError::UnrepresentableIR {
                // section name embedded in why; can't pass dynamic str
                // through this error variant.
                why: match section {
                    "type_list" => "preserve_data_section_layout: type_list has bytes but preserved base is 0/missing",
                    "code_item" => "preserve_data_section_layout: code_item has bytes but preserved base is 0/missing",
                    "debug_info" => "preserve_data_section_layout: debug_info has bytes but preserved base is 0/missing",
                    "class_data" => "preserve_data_section_layout: class_data has bytes but preserved base is 0/missing",
                    "annotation_item" => "preserve_data_section_layout: annotation_item has bytes but preserved base is 0/missing",
                    "annotation_set" => "preserve_data_section_layout: annotation_set has bytes but preserved base is 0/missing",
                    "annotation_set_ref_list" => "preserve_data_section_layout: annotation_set_ref_list has bytes but preserved base is 0/missing",
                    "annotation_directory" => "preserve_data_section_layout: annotation_directory has bytes but preserved base is 0/missing",
                    "encoded_array" => "preserve_data_section_layout: encoded_array has bytes but preserved base is 0/missing",
                    _ => "preserve_data_section_layout: unknown section has bytes but preserved base is 0/missing",
                },
            }),
        }
    }
    let type_list_base = base_or_required(preserved_type_list, !type_list_bytes.is_empty(), "type_list")?;
    let code_item_base = base_or_required(preserved_code_item, !code_item_bytes.is_empty(), "code_item")?;
    let debug_info_base = base_or_required(preserved_debug_info, !debug_info_bytes.is_empty(), "debug_info")?;
    let class_data_base_provisional = base_or_required(preserved_class_data, !dex.class_datas.is_empty(), "class_data")?;
    let annotation_item_base = base_or_required(preserved_annotation_item, !annotation_item_bytes.is_empty(), "annotation_item")?;
    let annotation_set_base_provisional = base_or_required(preserved_annotation_set, !dex.annotation_sets.is_empty(), "annotation_set")?;
    let annotation_set_ref_list_base_provisional = base_or_required(preserved_annotation_set_ref_list, !dex.annotation_set_ref_lists.is_empty(), "annotation_set_ref_list")?;
    let annotation_directory_base_provisional = base_or_required(preserved_annotation_directory, !dex.annotations.is_empty(), "annotation_directory")?;
    let encoded_array_base = base_or_required(preserved_encoded_array, !encoded_array_bytes.is_empty(), "encoded_array")?;

    // ── Phase E: rewrite code_item.debug_info_off using preserved base ──
    for (ci_orig_off, ci_local) in &code_item_remap {
        let ci = match dex.code_items.get(ci_orig_off) {
            Some(c) => c,
            None => continue,
        };
        let new_debug_off: u32 = if ci.debug_info_off == 0 {
            0
        } else {
            debug_info_remap
                .get(&ci.debug_info_off)
                .and_then(|local| debug_info_base.checked_add(*local))
                .unwrap_or(0)
        };
        let dst_start = (*ci_local as usize).saturating_add(8);
        let dst_end = dst_start.saturating_add(4);
        if dst_end > code_item_bytes.len() {
            return Err(DexEmitError::InvariantViolated {
                why: "code_item blob shorter than expected container header — debug_info_off rewrite OOB",
            });
        }
        code_item_bytes[dst_start..dst_end].copy_from_slice(&new_debug_off.to_le_bytes());
    }

    // ── Phase F: emit dependent sections with preserved bases ──
    // Under preserve mode, class_data items are laid out at their
    // preserved input offsets and prefer raw bytes from
    // `raw_class_data_bytes` when available. This handles cases where
    // ULEB128 redundant-encoding normalization in field_idx_diff /
    // access_flags / code_off would introduce byte-level divergence.
    // Falls back to canonical emit for any class_data whose raw bytes
    // weren't
    // captured at parse time.
    let class_data_base = class_data_base_provisional;
    let (class_data_bytes, class_data_remap) = {
        let section_size = section_size_from_map(map_type::CLASS_DATA_ITEM)
            .filter(|_| !dex.class_datas.is_empty());
        if let Some(size) = section_size {
            let mut blob = vec![0u8; size as usize];
            let mut remap = std::collections::BTreeMap::new();
            for (orig_off, cd) in &dex.class_datas {
                let local = orig_off.saturating_sub(class_data_base);
                remap.insert(*orig_off, local);
                let item_bytes: Vec<u8> = if let Some(raw) =
                    dex.raw_class_data_bytes.get(orig_off)
                {
                    raw.clone()
                } else {
                    // Fallback: canonical emit for this one class_data.
                    let single = std::collections::BTreeMap::from([(*orig_off, cd.clone())]);
                    let (b, _) =
                        emit_class_data_section(&single, &code_item_remap, code_item_base)?;
                    b
                };
                let start = local as usize;
                let end = start.saturating_add(item_bytes.len());
                if end > blob.len() {
                    return Err(DexEmitError::UnrepresentableIR {
                        why: "preserve: class_data overruns section",
                    });
                }
                blob[start..end].copy_from_slice(&item_bytes);
            }
            (blob, remap)
        } else {
            emit_class_data_section(&dex.class_datas, &code_item_remap, code_item_base)?
        }
    };
    // annotation_set under preserve mode: items at input physical offsets.
    let annotation_set_base = annotation_set_base_provisional;
    let (annotation_set_bytes, annotation_set_remap) = if let Some(size) =
        section_size_from_map(map_type::ANNOTATION_SET_ITEM)
        .filter(|_| !dex.annotation_sets.is_empty())
    {
        let mut blob = vec![0u8; size as usize];
        let mut remap = std::collections::BTreeMap::new();
        for (orig_off, entries) in &dex.annotation_sets {
            let local = orig_off.saturating_sub(annotation_set_base);
            remap.insert(*orig_off, local);
            let absolute: Vec<u32> = entries.iter().map(|&item_off| {
                if item_off == 0 { Ok(0u32) }
                else {
                    let local = *annotation_item_remap.get(&item_off).ok_or(
                        DexEmitError::UnrepresentableIR { why: "annotation_set references missing annotation_item" })?;
                    annotation_item_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_set.entry", context: "absolute offset overflow" })
                }
            }).collect::<Result<Vec<_>, DexEmitError>>()?;
            let item_bytes = emit_annotation_set_bytes(&absolute);
            let start = local as usize;
            let end = start.saturating_add(item_bytes.len());
            if end > blob.len() {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "preserve: annotation_set overruns section",
                });
            }
            blob[start..end].copy_from_slice(&item_bytes);
        }
        (blob, remap)
    } else {
        emit_annotation_set_section(&dex.annotation_sets, annotation_item_base, &annotation_item_remap)?
    };
    // annotation_set_ref_list under preserve mode
    let annotation_set_ref_list_base = annotation_set_ref_list_base_provisional;
    let (annotation_set_ref_list_bytes, annotation_set_ref_list_remap) = if let Some(size) =
        section_size_from_map(map_type::ANNOTATION_SET_REF_LIST)
        .filter(|_| !dex.annotation_set_ref_lists.is_empty())
    {
        let mut blob = vec![0u8; size as usize];
        let mut remap = std::collections::BTreeMap::new();
        for (orig_off, entries) in &dex.annotation_set_ref_lists {
            let local = orig_off.saturating_sub(annotation_set_ref_list_base);
            remap.insert(*orig_off, local);
            let absolute: Vec<u32> = entries.iter().map(|&set_off| {
                if set_off == 0 { Ok(0u32) }
                else {
                    let local = *annotation_set_remap.get(&set_off).ok_or(
                        DexEmitError::UnrepresentableIR { why: "annotation_set_ref_list references missing annotation_set" })?;
                    annotation_set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_set_ref_list.entry", context: "absolute offset overflow" })
                }
            }).collect::<Result<Vec<_>, DexEmitError>>()?;
            let item_bytes = emit_annotation_set_bytes(&absolute);
            let start = local as usize;
            let end = start.saturating_add(item_bytes.len());
            if end > blob.len() {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "preserve: annotation_set_ref_list overruns section",
                });
            }
            blob[start..end].copy_from_slice(&item_bytes);
        }
        (blob, remap)
    } else {
        emit_annotation_set_ref_list_section(&dex.annotation_set_ref_lists, annotation_set_base, &annotation_set_remap)?
    };
    // annotation_directory under preserve mode
    let annotation_directory_base = annotation_directory_base_provisional;
    let (annotation_directory_bytes, annotation_directory_remap) = if let Some(size) =
        section_size_from_map(map_type::ANNOTATION_DIRECTORY_ITEM)
        .filter(|_| !dex.annotations.is_empty())
    {
        let mut blob = vec![0u8; size as usize];
        let mut remap = std::collections::BTreeMap::new();
        for (orig_off, dir) in &dex.annotations {
            let local = orig_off.saturating_sub(annotation_directory_base);
            remap.insert(*orig_off, local);
            // Replicate emit_annotation_directory_section per-item logic
            let class_ann_off_abs: u32 = if dir.class_annotations_off == 0 {
                0
            } else {
                let local = *annotation_set_remap.get(&dir.class_annotations_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "annotation_directory.class_annotations_off references missing annotation_set" })?;
                annotation_set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "annotation_directory.class_annotations_off", context: "offset overflow" })?
            };
            let field_offs_abs: Vec<u32> = dir.fields.iter().map(|fa| {
                if fa.annotations_off == 0 { Ok(0u32) }
                else {
                    let local = *annotation_set_remap.get(&fa.annotations_off).ok_or(
                        DexEmitError::UnrepresentableIR { why: "annotation_directory.field references missing annotation_set" })?;
                    annotation_set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_directory.field.annotations_off", context: "overflow" })
                }
            }).collect::<Result<Vec<_>, DexEmitError>>()?;
            let method_offs_abs: Vec<u32> = dir.methods.iter().map(|ma| {
                if ma.annotations_off == 0 { Ok(0u32) }
                else {
                    let local = *annotation_set_remap.get(&ma.annotations_off).ok_or(
                        DexEmitError::UnrepresentableIR { why: "annotation_directory.method references missing annotation_set" })?;
                    annotation_set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_directory.method.annotations_off", context: "overflow" })
                }
            }).collect::<Result<Vec<_>, DexEmitError>>()?;
            let param_offs_abs: Vec<u32> = dir.parameters.iter().map(|pa| {
                if pa.annotations_off == 0 { Ok(0u32) }
                else {
                    let local = *annotation_set_ref_list_remap.get(&pa.annotations_off).ok_or(
                        DexEmitError::UnrepresentableIR { why: "annotation_directory.param references missing annotation_set_ref_list" })?;
                    annotation_set_ref_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_directory.param.annotations_off", context: "overflow" })
                }
            }).collect::<Result<Vec<_>, DexEmitError>>()?;
            let item_bytes = emit_annotation_directory_bytes(
                dir, class_ann_off_abs, &field_offs_abs, &method_offs_abs, &param_offs_abs,
            )?;
            let start = local as usize;
            let end = start.saturating_add(item_bytes.len());
            if end > blob.len() {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "preserve: annotation_directory overruns section",
                });
            }
            blob[start..end].copy_from_slice(&item_bytes);
        }
        (blob, remap)
    } else {
        emit_annotation_directory_section(&dex.annotations, annotation_set_base, &annotation_set_remap, annotation_set_ref_list_base, &annotation_set_ref_list_remap)?
    };
    // call_site_id items are absolute offsets into the encoded_array
    // section. The encoded_array content for each call_site lives in
    // dex.encoded_arrays (populated by the parser keyed by the
    // call_site offset), so emit looks up each cs_off in the existing
    // encoded_array_remap. Sharing (multiple call_sites pointing at
    // the same offset) is preserved because dedup happened at parse.
    let call_site_data_offsets: Vec<u32> = dex
        .call_site_ids
        .iter()
        .map(|&cs_off| {
            if cs_off == 0 {
                Ok(0u32)
            } else {
                let local = encoded_array_remap.get(&cs_off).copied().ok_or(
                    DexEmitError::UnrepresentableIR {
                        why: "call_site_id references encoded_array not in dex.encoded_arrays",
                    },
                )?;
                encoded_array_base.checked_add(local).ok_or(
                    DexEmitError::OffsetOverflow {
                        section: "call_site_id",
                        context: "absolute offset overflow",
                    },
                )
            }
        })
        .collect::<Result<Vec<_>, DexEmitError>>()?;

    // ── Phase G: build map_list at preserved offset ──
    // Note: build_map_items uses its `data_off` parameter as the
    // STRING_DATA_ITEM offset. Under preserve mode, string_data lives
    // at `string_data_base` (preserved), which may differ from the
    // sequential `data_off`. Pass string_data_base so the resulting
    // map_list's STRING_DATA entry points at the preserved location.
    let mut map_items = build_map_items(
        string_id_items.len() / 4, type_id_items.len() / 4, dex.protos.len(),
        field_id_items.len() / 8, method_id_items.len() / 8, dex.class_defs.len(),
        dex.call_site_ids.len(), dex.method_handles.len(), dex.strings.len(),
        string_ids_off, type_ids_off, proto_ids_off, field_ids_off, method_ids_off,
        class_defs_off, call_site_ids_off, method_handles_off, string_data_base, preserved_map_off,
        &dex.type_lists, type_list_base, &dex.code_items, code_item_base,
        &dex.class_datas, class_data_base, &dex.annotation_items, annotation_item_base,
        &dex.annotation_sets, annotation_set_base, &dex.annotation_set_ref_lists,
        annotation_set_ref_list_base, &dex.annotations, annotation_directory_base,
        &dex.encoded_arrays, encoded_array_base, &dex.debug_info_raw_bytes, debug_info_base,
    );
    if config.preserve_map_list_order {
        map_items = reorder_map_items_to_input(map_items, &dex.map_entries);
    } else {
        map_items.sort_by_key(|m| m.offset);
        if map_list_order_diverged(&map_items, &dex.map_entries) {
            emit_transforms.push(CanonicalTransform::MapListReordered);
        }
    }
    let map_list_bytes = emit_map_list(&map_items, config.preserve_map_list_order)?;

    // ── Phase H: build proto / class_def with preserved offsets ──
    let proto_id_items = {
        let remapped: Vec<ProtoIdItem> = dex.protos.iter().map(|p| {
            let new_off = if p.parameters_off == 0 { 0 } else {
                let local = *type_list_remap.get(&p.parameters_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "proto.parameters_off references type_list not in DexFile.type_lists" }
                )?;
                type_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "proto.parameters_off", context: "remapped type_list offset exceeds u32",
                })?
            };
            Ok(ProtoIdItem {
                shorty_idx: p.shorty_idx, return_type_idx: p.return_type_idx, parameters_off: new_off,
            })
        }).collect::<Result<Vec<_>, DexEmitError>>()?;
        emit_proto_pool(&remapped)
    };
    let class_def_items = {
        let remapped: Vec<ClassDefItem> = dex.class_defs.iter().map(|c| {
            let new_interfaces_off = if c.interfaces_off == 0 { 0 } else {
                let local = *type_list_remap.get(&c.interfaces_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "class.interfaces_off references type_list not in DexFile.type_lists" }
                )?;
                type_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "class.interfaces_off", context: "remapped interfaces offset exceeds u32",
                })?
            };
            let new_class_data_off = if c.class_data_off == 0 { 0 } else {
                let local = *class_data_remap.get(&c.class_data_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "class.class_data_off references class_data not in DexFile.class_datas" }
                )?;
                class_data_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "class.class_data_off", context: "remapped class_data offset exceeds u32",
                })?
            };
            let new_annotations_off = if c.annotations_off == 0 { 0 } else {
                let local = *annotation_directory_remap.get(&c.annotations_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "class.annotations_off references annotation_directory not in DexFile.annotations" }
                )?;
                annotation_directory_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "class.annotations_off", context: "remapped annotation_directory offset exceeds u32",
                })?
            };
            let new_static_values_off = if c.static_values_off == 0 { 0 } else {
                let local = *encoded_array_remap.get(&c.static_values_off).ok_or(
                    DexEmitError::UnrepresentableIR { why: "class.static_values_off references encoded_array not in DexFile.encoded_arrays" }
                )?;
                encoded_array_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "class.static_values_off", context: "remapped encoded_array offset exceeds u32",
                })?
            };
            Ok(ClassDefItem {
                class_idx: c.class_idx, access_flags: c.access_flags,
                superclass_idx: c.superclass_idx, interfaces_off: new_interfaces_off,
                source_file_idx: c.source_file_idx, annotations_off: new_annotations_off,
                class_data_off: new_class_data_off, static_values_off: new_static_values_off,
            })
        }).collect::<Result<Vec<_>, DexEmitError>>()?;
        emit_class_def_pool(&remapped)?
    };
    let call_site_id_items = emit_call_site_id_section(&call_site_data_offsets)?;
    let method_handle_items = emit_method_handle_section(&dex.method_handles)?;

    // ── Phase I: rewrite string_id_items with absolute string_data offsets ──
    // Under preserve mode with string layout preservation, string_id_items
    // already contains absolute offsets (per emit_string_pool_preserve_layout).
    // Otherwise it contains relative offsets that need string_data_base added.
    let fixed_string_id_items = if preserve_strings {
        string_id_items.clone()
    } else {
        let mut fixed = Vec::with_capacity(string_id_items.len());
        for chunk in string_id_items.chunks_exact(4) {
            let mut rel = [0u8; 4];
            rel.copy_from_slice(chunk);
            let rel = u32::from_le_bytes(rel);
            let abs = string_data_base.saturating_add(rel);
            fixed.extend_from_slice(&abs.to_le_bytes());
        }
        fixed
    };

    // ── Phase J: file_size + emit header ──
    // file_size is the max of (each section's base + bytes.len()).
    let mut file_size: u32 = preserved_map_off.saturating_add(u32::try_from(map_list_bytes.len())
        .map_err(|_| DexEmitError::OffsetOverflow { section: "map_list", context: "map_list size overflow" })?);
    let candidate_ends: [(u32, u32); 10] = [
        (string_data_base, u32::try_from(string_data_items.len()).unwrap_or(0)),
        (type_list_base, u32::try_from(type_list_bytes.len()).unwrap_or(0)),
        (code_item_base, u32::try_from(code_item_bytes.len()).unwrap_or(0)),
        (debug_info_base, u32::try_from(debug_info_bytes.len()).unwrap_or(0)),
        (class_data_base, u32::try_from(class_data_bytes.len()).unwrap_or(0)),
        (annotation_item_base, u32::try_from(annotation_item_bytes.len()).unwrap_or(0)),
        (annotation_set_base, u32::try_from(annotation_set_bytes.len()).unwrap_or(0)),
        (annotation_set_ref_list_base, u32::try_from(annotation_set_ref_list_bytes.len()).unwrap_or(0)),
        (annotation_directory_base, u32::try_from(annotation_directory_bytes.len()).unwrap_or(0)),
        (encoded_array_base, u32::try_from(encoded_array_bytes.len()).unwrap_or(0)),
    ];
    for (base, len) in &candidate_ends {
        let end = base.saturating_add(*len);
        if end > file_size {
            file_size = end;
        }
    }
    // Under preserve mode, data_off + data_size are header fields the
    // input chose semi-arbitrarily — the DEX spec says "offset / size
    // of the data section" but the exact boundary is tool-dependent.
    // Some tools (e.g. d8 default) set data_off to end-of-class_defs;
    // others set it to the first data-section item (e.g. map_list
    // start), which can differ by alignment-padding bytes. For
    // byte-identity preservation, copy the input's stored values rather
    // than recomputing. Falls
    // back to the canonical recompute when the input header values
    // are obviously stale (data_off + data_size > file_size).
    let (preserved_data_off, preserved_data_size) = {
        let in_off = dex.header.data_off;
        let in_size = dex.header.data_size;
        let in_end = in_off.saturating_add(in_size);
        if in_off > 0 && in_end <= file_size {
            (in_off, in_size)
        } else {
            (data_off, file_size.saturating_sub(data_off))
        }
    };
    let layout = HeaderLayout {
        version: { let mut v = [0u8; 3]; v.copy_from_slice(&dex.header.magic[4..7]); v },
        file_size, map_off: preserved_map_off,
        string_ids_size: u32::try_from(dex.strings.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "string_ids_size" })?,
        string_ids_off,
        type_ids_size: u32::try_from(dex.type_descriptors.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "type_ids_size" })?,
        type_ids_off,
        proto_ids_size: u32::try_from(dex.protos.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "proto_ids_size" })?,
        proto_ids_off,
        field_ids_size: u32::try_from(dex.fields.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "field_ids_size" })?,
        field_ids_off,
        method_ids_size: u32::try_from(dex.methods.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "method_ids_size" })?,
        method_ids_off,
        class_defs_size: u32::try_from(dex.class_defs.len()).map_err(|_| DexEmitError::SizeOverflow { layer: "header", context: "class_defs_size" })?,
        class_defs_off, data_size: preserved_data_size, data_off: preserved_data_off,
    };
    let header_bytes = emit_header(&layout);

    // ── Phase K: assemble output buffer at preserved offsets ──
    let mut out = vec![0u8; file_size as usize];
    fn write_at(buf: &mut [u8], offset: u32, bytes: &[u8]) -> Result<(), DexEmitError> {
        let start = offset as usize;
        let end = start.saturating_add(bytes.len());
        if end > buf.len() {
            return Err(DexEmitError::InvariantViolated {
                why: "preserve_layout: section bytes overrun pre-sized output buffer",
            });
        }
        buf[start..end].copy_from_slice(bytes);
        Ok(())
    }
    // Header-stomp guard: any write at offset 0 with non-empty bytes
    // that isn't the header write itself stomps the magic. Catches
    // the Tier 1 reparse_err family (output starts with `00 00 00 00`).
    fn write_at_data(
        buf: &mut [u8],
        offset: u32,
        bytes: &[u8],
        section: &'static str,
    ) -> Result<(), DexEmitError> {
        if offset == 0 && !bytes.is_empty() {
            if std::env::var("DROIDSAW_DEBUG_PRESERVE").ok().as_deref() == Some("1") {
                eprintln!(
                    "[DROIDSAW_DEBUG_PRESERVE] header-stomp guard tripped: section={section} bytes_len={}",
                    bytes.len()
                );
            }
            return Err(DexEmitError::InvariantViolated {
                why: "preserve_layout: data section base resolved to 0 — would stomp header magic",
            });
        }
        write_at(buf, offset, bytes)
    }
    write_at(&mut out, 0, &header_bytes)?;
    write_at_data(&mut out, string_ids_off, &fixed_string_id_items, "string_ids")?;
    write_at_data(&mut out, type_ids_off, &type_id_items, "type_ids")?;
    write_at_data(&mut out, proto_ids_off, &proto_id_items, "proto_ids")?;
    write_at_data(&mut out, field_ids_off, &field_id_items, "field_ids")?;
    write_at_data(&mut out, method_ids_off, &method_id_items, "method_ids")?;
    write_at_data(&mut out, class_defs_off, &class_def_items, "class_defs")?;
    write_at_data(&mut out, call_site_ids_off, &call_site_id_items, "call_site_ids")?;
    write_at_data(&mut out, method_handles_off, &method_handle_items, "method_handles")?;
    write_at_data(&mut out, string_data_base, &string_data_items, "string_data")?;
    write_at_data(&mut out, type_list_base, &type_list_bytes, "type_list")?;
    write_at_data(&mut out, code_item_base, &code_item_bytes, "code_item")?;
    write_at_data(&mut out, debug_info_base, &debug_info_bytes, "debug_info")?;
    write_at_data(&mut out, class_data_base, &class_data_bytes, "class_data")?;
    write_at_data(&mut out, annotation_item_base, &annotation_item_bytes, "annotation_item")?;
    write_at_data(&mut out, annotation_set_base, &annotation_set_bytes, "annotation_set")?;
    write_at_data(&mut out, annotation_set_ref_list_base, &annotation_set_ref_list_bytes, "annotation_set_ref_list")?;
    write_at_data(&mut out, annotation_directory_base, &annotation_directory_bytes, "annotation_directory")?;
    write_at_data(&mut out, encoded_array_base, &encoded_array_bytes, "encoded_array")?;
    write_at(&mut out, preserved_map_off, &map_list_bytes)?;

    finalize_checksums(&mut out)?;
    apply_preserve_input_checksums(&mut out, dex, config)?;
    Ok((out, emit_transforms))
}

/// Under `EmitConfig::preserve_input_checksums`, overwrite the freshly-
/// computed Adler-32 + SHA-1 in `out[8..32]` with the values from the
/// input header. Trap: produces non-canonical DEX (Android rejects).
/// See `EmitConfig::preserve_input_checksums` doc-comment for the
/// scope (byte-identity audit harnesses only).
fn apply_preserve_input_checksums(
    out: &mut [u8],
    dex: &DexFile,
    config: &EmitConfig,
) -> Result<(), DexEmitError> {
    if !config.preserve_input_checksums {
        return Ok(());
    }
    if out.len() < 32 {
        return Err(DexEmitError::OffsetOverflow {
            section: "header",
            context: "preserve_input_checksums: output shorter than 32 bytes",
        });
    }
    out[8..12].copy_from_slice(&dex.header.checksum.to_le_bytes());
    out[12..32].copy_from_slice(&dex.header.signature);
    Ok(())
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: every `usize as u32` / `len() as u32` / `u32 as usize` in this fn is either (a) a DEX section size (file-format spec stores all section sizes/counts as u32, bounding the cast), (b) a u32 pad or file_size value widened to usize for Vec construction (lossless on 64-bit), or (c) out.len() widened to u64 for the layout invariant check (lossless — usize ≤ u64::MAX). For spec-compliant DEX, all narrowings are exact."
)]
fn emit_dex_inner(
    dex: &DexFile,
    config: &EmitConfig,
) -> Result<(Vec<u8>, Vec<CanonicalTransform>), DexEmitError> {
    // Tracker for per-emit-site canonicalization observations. Pushed
    // into by alignment-padding insertion sites + (future) other
    // per-emit observation paths. Merged into the public-facing
    // `EmitOutput.applied_transformations` by `emit_dex_collect`.
    let mut emit_transforms: Vec<CanonicalTransform> = Vec::new();

    // Partial-IR gate (evasion-primitive mitigation). A `DexFile`
    // with `parse_errors` populated represents bytes whose tolerant
    // parse dropped subsection(s); emit would produce output that
    // is a PARTIAL IMAGE of the source input. Strict mode refuses
    // this shape; callers opt in via `permit_partial_ir: true`.
    if !config.permit_partial_ir {
        if let Some(first) = dex.parse_errors.first() {
            return Err(DexEmitError::PartialIR {
                count: dex.parse_errors.len(),
                first_kind: first.kind,
                first_offset: first.offset,
            });
        }
    }

    // Preserve-mode dispatch. When the toggle is set, route to a
    // separate emit path that places each data-section subsection at
    // the offset recorded in the input's `dex.map_entries` rather than
    // emit's canonical sequential order. Required to close the
    // byte-identity gap caused by canonical emit's sequential ordering
    // diverging from real-world tool layouts.
    if config.preserve_data_section_layout {
        return emit_dex_inner_preserve_layout(dex, config);
    }

    // Fail-closed gate on the two map-list sections emit does NOT yet
    // cover (`method_handles` TYPE 0x0008 and `call_site_ids` TYPE 0x0007).
    // A DEX with invoke-custom / invoke-polymorphic at minSdk ≥ 26 retains
    // these sections; the current emit silently drops both, producing an
    // output whose invoke operands reference indexes into an absent pool
    // (baksmali `IndexOutOfBoundsException: Invalid callsite index 0,
    // not in [0, 0)`).
    //
    // Rather than emit corrupt bytes, refuse the round-trip here
    // with a typed UnrepresentableIR. Downstream callers
    // (`fuzz_emit_roundtrip`, bench `baksmali_differential`) already
    // treat UnrepresentableIR as a "skip-this-input" signal, so the
    // 938-corpus smoke's 937 passing cases stay green; the 1 failing
    // case now fails-loud instead of silent-corrupt.
    //
    // `method_handles` + `call_site_ids` map-list sections: emitted
    // from `dex.method_handles` + `dex.call_site_ids` per layout
    // phase below. Previously fail-closed with UnrepresentableIR;
    // now round-trips.

    // All data-section shapes are now supported including DebugInfo
    // round-trip (see emit_code_item_section + emit_debug_info_section).

    // Phase 1: build type_ids pool by reverse-resolving each
    // type_descriptor string to its StringIdx. DexFile stores
    // type_descriptors as resolved Vec<String>, not the raw
    // Vec<TypeIdItem>, so we rebuild the idx array via a single
    // HashMap<&str, StringIdx> pass over the strings pool.
    use std::collections::HashMap;
    let string_to_idx: HashMap<&str, StringIdx> = dex
        .strings
        .iter()
        .enumerate()
        .map(|(i, s)| (s.as_str_lossy(), StringIdx(i as u32)))
        .collect();
    let mut type_idxs: Vec<StringIdx> = Vec::with_capacity(dex.type_descriptors.len());
    for (i, desc) in dex.type_descriptors.iter().enumerate() {
        let idx = *string_to_idx.get(desc.as_str()).ok_or_else(|| {
            let _ = i;
            DexEmitError::UnrepresentableIR {
                why: "emit_dex: type_descriptor not found in strings pool (IR is internally inconsistent)",
            }
        })?;
        type_idxs.push(idx);
    }

    // Phase 2a: emit the variable-offset data subsections first —
    // their local offsets become the remap that protos + class_defs
    // need for their own emission. Order matters: code_items must
    // exist before class_data (class_data references them);
    // class_data must exist before class_defs (class_defs reference
    // them via class_data_off).
    let (type_list_bytes, type_list_remap) = emit_type_list_section(&dex.type_lists)?;
    let (mut code_item_bytes, code_item_remap) = emit_code_item_section(&dex.code_items)?;
    // code_item_bytes contains each container with `debug_info_off` set
    // to a 0 placeholder (see `emit_code_item_section`). Once
    // `debug_info_base` is computed in Phase 3 below, we rewrite the
    // 4-byte `debug_info_off` field at byte offset 8 of each container
    // to `debug_info_base + debug_info_remap[ci.debug_info_off]`.
    // class_data depends on code_item remap but not its absolute
    // base offset; we re-apply the base in the rewriting step below.
    // For now emit with a placeholder base of 0, then rewrite after
    // absolute positions are known.
    let (debug_info_bytes, debug_info_remap) =
        emit_debug_info_section(&dex.debug_info_raw_bytes)?;

    // Phase 2b: build id pool buffers. The string/type/field/method
    // pools are layout-invariant (no embedded file offsets). The
    // proto pool embeds `parameters_off`, and class_defs embeds
    // `interfaces_off`, so those are emitted later once the
    // type_list blob's absolute offset is known.
    //
    // Strings: pass the raw MUTF-8 bytes carried by each `DexString`
    // straight through — preserves byte-identity across
    // parse→emit→parse for strings containing unpaired surrogates or
    // other MUTF-8 sequences that don't round-trip through Rust's
    // `String` type. Parser preserves on-disk order; emit writes it
    // back verbatim. No re-sort, no verification — caller (parser)
    // owns the invariant.
    //
    // The old fallback path "string_raw_bytes empty → re-encode via
    // MUTF-8" is gone: with the `Vec<DexString>` shape, raw bytes
    // always exist (every variant carries `raw_bytes: Vec<u8>`).
    // Hand-built test IR that previously left `string_raw_bytes`
    // empty now constructs full `DexString::Decoded` entries with
    // pre-populated `raw_bytes` (see test helper in this file +
    // `obfuscation_features::tests::push_raw_dex_string`).
    let raw_bytes_owned: Vec<&[u8]> = dex.strings.iter().map(crate::DexString::raw_bytes).collect();
    let (string_id_items, string_data_items) = emit_string_pool(&raw_bytes_owned);
    // Type idxs are `StringIdx` values; their numerical (u32) order
    // matches the MUTF-8 byte order of their referenced strings on
    // spec-compliant DEX (string_ids is sorted in MUTF-8 order, and
    // type_ids is sorted by its descriptor's position in string_ids).
    // `NonDecreasing::from_verified` on Vec<StringIdx> uses u32 Ord
    // which is the right comparator here.
    let type_id_items = {
        let sorted = NonDecreasing::from_verified(type_idxs).map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "dex.type_descriptors not in canonical order — parser accepts; emit rejects to avoid silent index miscompile",
            }
        })?;
        emit_type_pool(&sorted)
    };
    let field_id_items = emit_field_pool(&dex.fields)?;
    let method_id_items = emit_method_pool(&dex.methods)?;
    // proto_id_items and class_def_items deferred to Phase 4.

    // Phase 3: resolve offsets. DEX layout (spec §7.2 + canonical
    // ordering by d8/r8):
    //   [0, HEADER_SIZE)           header
    //   [HEADER_SIZE, ...)         string_id_items (fixed-stride)
    //   ...                        type_id_items
    //   ...                        proto_id_items
    //   ...                        field_id_items
    //   ...                        method_id_items
    //   ...                        class_def_items
    //   data_off:                  string_data_items (variable-stride)
    //                              type_list blob (4-byte aligned)
    //                              map_list
    //   file_size
    //
    // Pool item stride constants used below to pre-compute sections
    // whose emission is deferred to Phase 3b.
    const PROTO_ID_STRIDE: u32 = 12;
    const CLASS_DEF_STRIDE: u32 = 32;

    let mut off: u32 = DEX_HEADER_SIZE;
    let string_ids_off = if !string_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(string_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "string_ids",
            context: "string_ids section exceeds u32 byte offset",
        }
    })?);
    let type_ids_off = if !type_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(type_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "type_ids",
            context: "type_ids offset overflow",
        }
    })?);
    let proto_ids_off = if !dex.protos.is_empty() { off } else { 0 };
    let proto_bytes_len: u32 = u32::try_from(dex.protos.len())
        .ok()
        .and_then(|n| n.checked_mul(PROTO_ID_STRIDE))
        .ok_or(DexEmitError::OffsetOverflow {
            section: "proto_ids",
            context: "proto_ids byte size exceeds u32",
        })?;
    off = off.saturating_add(proto_bytes_len);
    let field_ids_off = if !field_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(field_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "field_ids",
            context: "field_ids offset overflow",
        }
    })?);
    let method_ids_off = if !method_id_items.is_empty() { off } else { 0 };
    off = off.saturating_add(u32::try_from(method_id_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "method_ids",
            context: "method_ids offset overflow",
        }
    })?);
    let class_defs_off = if !dex.class_defs.is_empty() { off } else { 0 };
    let class_def_bytes_len: u32 = u32::try_from(dex.class_defs.len())
        .ok()
        .and_then(|n| n.checked_mul(CLASS_DEF_STRIDE))
        .ok_or(DexEmitError::OffsetOverflow {
            section: "class_defs",
            context: "class_defs byte size exceeds u32",
        })?;
    off = off.saturating_add(class_def_bytes_len);

    // call_site_ids (TYPE 0x0007) — u32 offset per entry pointing at
    // an encoded_array_item in the data section. Omitted when the DEX
    // has no invoke-custom / invoke-polymorphic call sites
    // (pre-O DEX, or post-O DEX that d8 desugared). Sits between
    // class_defs (0x0006) and method_handles (0x0008) in map_list
    // offset order.
    let call_site_ids_off = if !dex.call_site_ids.is_empty() { off } else { 0 };
    let call_site_ids_bytes_len: u32 = u32::try_from(dex.call_site_ids.len())
        .ok()
        .and_then(|n| n.checked_mul(4))
        .ok_or(DexEmitError::OffsetOverflow {
            section: "call_site_ids",
            context: "call_site_ids byte size exceeds u32",
        })?;
    off = off.saturating_add(call_site_ids_bytes_len);

    // method_handles (TYPE 0x0008) — 8-byte fixed record per entry.
    // Same omission rule as call_site_ids.
    let method_handles_off = if !dex.method_handles.is_empty() { off } else { 0 };
    let method_handles_bytes_len: u32 = u32::try_from(dex.method_handles.len())
        .ok()
        .and_then(|n| n.checked_mul(8))
        .ok_or(DexEmitError::OffsetOverflow {
            section: "method_handles",
            context: "method_handles byte size exceeds u32",
        })?;
    off = off.saturating_add(method_handles_bytes_len);

    // Data section begins here.
    let data_off = off;
    off = off.saturating_add(u32::try_from(string_data_items.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "string_data",
            context: "string_data offset overflow",
        }
    })?);

    // code_item_section follows string_data, 4-byte aligned.
    let code_item_base = align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
        section: "code_item_section",
        context: "alignment padding overflowed u32",
    })?;
    let code_item_pad = code_item_base.saturating_sub(off);
    if code_item_pad > 0
        && input_section_offset(dex, AlignmentSection::CodeItem) != Some(code_item_base)
    {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::CodeItem,
            byte_count: code_item_pad,
        });
    }
    off = code_item_base;
    off = off.saturating_add(u32::try_from(code_item_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "code_item_section",
            context: "code_item_section offset overflow",
        }
    })?);

    // debug_info_section follows code_items (no alignment required per
    // DEX spec — debug_info_item is a variable-length byte stream that
    // starts at a byte boundary). Section sits between code_item and
    // class_data so the per-code_item debug_info_off rewrite below has
    // `debug_info_base` available at the time code_item bytes are
    // rewritten in-place.
    let debug_info_base = off;
    off = off.saturating_add(u32::try_from(debug_info_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "debug_info_section",
            context: "debug_info_section offset overflow",
        }
    })?);

    // Rewrite each code_item container's `debug_info_off` field
    // (4 bytes at local byte offset 8 within the container) with the
    // resolved absolute offset `debug_info_base + local`. Preserves
    // the dangling-pointer closure by construction:
    // the offset now references emitter-derived bytes that are byte-
    // copies of the original debug_info (see emit_debug_info_section
    // doc-block). Code_items whose original debug_info_off points to
    // an entry the parser silently skipped (malformed state machine;
    // typed Err path in scan_debug_info_bytes) fall back to 0 — the
    // same security-safe zero-sentinel used for all code_items,
    // applied here only to the residual skipped subset.
    for (ci_orig_off, ci_local) in &code_item_remap {
        let ci = match dex.code_items.get(ci_orig_off) {
            Some(c) => c,
            None => continue,
        };
        let new_debug_off: u32 = if ci.debug_info_off == 0 {
            0
        } else {
            debug_info_remap
                .get(&ci.debug_info_off)
                .and_then(|local| debug_info_base.checked_add(*local))
                .unwrap_or(0)
        };
        let dst_start = (*ci_local as usize).saturating_add(8);
        let dst_end = dst_start.saturating_add(4);
        if dst_end > code_item_bytes.len() {
            return Err(DexEmitError::InvariantViolated {
                why: "code_item blob shorter than expected container header — debug_info_off rewrite OOB",
            });
        }
        code_item_bytes[dst_start..dst_end]
            .copy_from_slice(&new_debug_off.to_le_bytes());
    }

    // class_data_section follows debug_info (no alignment required).
    // Emit it now that code_item_base is known — it embeds remapped
    // code_off values.
    let class_data_base = off;
    let (class_data_bytes, class_data_remap) =
        emit_class_data_section(&dex.class_datas, &code_item_remap, code_item_base)?;
    off = off.saturating_add(u32::try_from(class_data_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "class_data_section",
            context: "class_data_section offset overflow",
        }
    })?);

    // annotation_item section (leaf — no dependencies).
    let annotation_item_base = off;
    let (annotation_item_bytes, annotation_item_remap) =
        emit_annotation_item_section(&dex.annotation_items)?;
    off = off.saturating_add(u32::try_from(annotation_item_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "annotation_item_section",
            context: "annotation_item_section offset overflow",
        }
    })?);

    // annotation_set section (references annotation_items), 4-byte aligned.
    let annotation_set_base = align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
        section: "annotation_set_section",
        context: "alignment padding overflowed u32",
    })?;
    let annotation_set_pad = annotation_set_base.saturating_sub(off);
    if annotation_set_pad > 0
        && input_section_offset(dex, AlignmentSection::AnnotationSet) != Some(annotation_set_base)
    {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::AnnotationSet,
            byte_count: annotation_set_pad,
        });
    }
    off = annotation_set_base;
    let (annotation_set_bytes, annotation_set_remap) = emit_annotation_set_section(
        &dex.annotation_sets,
        annotation_item_base,
        &annotation_item_remap,
    )?;
    off = off.saturating_add(u32::try_from(annotation_set_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "annotation_set_section",
            context: "annotation_set_section offset overflow",
        }
    })?);

    // annotation_set_ref_list section (references annotation_sets), 4-byte aligned.
    let annotation_set_ref_list_base =
        align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
            section: "annotation_set_ref_list_section",
            context: "alignment padding overflowed u32",
        })?;
    let annotation_set_ref_list_pad = annotation_set_ref_list_base.saturating_sub(off);
    if annotation_set_ref_list_pad > 0
        && input_section_offset(dex, AlignmentSection::AnnotationSetRefList)
            != Some(annotation_set_ref_list_base)
    {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::AnnotationSetRefList,
            byte_count: annotation_set_ref_list_pad,
        });
    }
    off = annotation_set_ref_list_base;
    let (annotation_set_ref_list_bytes, annotation_set_ref_list_remap) =
        emit_annotation_set_ref_list_section(
            &dex.annotation_set_ref_lists,
            annotation_set_base,
            &annotation_set_remap,
        )?;
    off = off.saturating_add(u32::try_from(annotation_set_ref_list_bytes.len()).map_err(
        |_| DexEmitError::OffsetOverflow {
            section: "annotation_set_ref_list_section",
            context: "annotation_set_ref_list_section offset overflow",
        },
    )?);

    // annotation_directory section (references sets + ref_lists), 4-byte aligned.
    let annotation_directory_base =
        align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
            section: "annotation_directory_section",
            context: "alignment padding overflowed u32",
        })?;
    let annotation_directory_pad = annotation_directory_base.saturating_sub(off);
    if annotation_directory_pad > 0
        && input_section_offset(dex, AlignmentSection::AnnotationDirectory)
            != Some(annotation_directory_base)
    {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::AnnotationDirectory,
            byte_count: annotation_directory_pad,
        });
    }
    off = annotation_directory_base;
    let (annotation_directory_bytes, annotation_directory_remap) =
        emit_annotation_directory_section(
            &dex.annotations,
            annotation_set_base,
            &annotation_set_remap,
            annotation_set_ref_list_base,
            &annotation_set_ref_list_remap,
        )?;
    off = off.saturating_add(u32::try_from(annotation_directory_bytes.len()).map_err(
        |_| DexEmitError::OffsetOverflow {
            section: "annotation_directory_section",
            context: "annotation_directory_section offset overflow",
        },
    )?);

    // encoded_array_item section (static field initial values). No
    // alignment required — leaf section. Under preserve mode, thread
    // dex.encoded_array_widths through so per-value input widths are
    // honored (defends byte-identity roundtrip against re-encoding to
    // min-width).
    let encoded_array_base = off;
    let widths_param = if config.preserve_encoded_value_width {
        Some(&dex.encoded_array_widths)
    } else {
        None
    };
    let (encoded_array_bytes, encoded_array_remap) =
        emit_encoded_array_section_with_widths(&dex.encoded_arrays, widths_param)?;
    off = off.saturating_add(u32::try_from(encoded_array_bytes.len()).map_err(
        |_| DexEmitError::OffsetOverflow {
            section: "encoded_array_section",
            context: "encoded_array_section offset overflow",
        },
    )?);

    // call_site encoded_arrays now live in dex.encoded_arrays (parser
    // inserts each call_site's content keyed by its on-disk offset);
    // the call_site_ids pool just stores absolute offsets into the
    // encoded_array section. Look up each cs_off in the existing
    // encoded_array_remap. Offset sharing across call_sites and with
    // class_def static_values is preserved by construction.
    let call_site_data_offsets: Vec<u32> = dex
        .call_site_ids
        .iter()
        .map(|&cs_off| {
            if cs_off == 0 {
                Ok(0u32)
            } else {
                let local = encoded_array_remap.get(&cs_off).copied().ok_or(
                    DexEmitError::UnrepresentableIR {
                        why: "call_site_id references encoded_array not in dex.encoded_arrays",
                    },
                )?;
                encoded_array_base.checked_add(local).ok_or(
                    DexEmitError::OffsetOverflow {
                        section: "call_site_id",
                        context: "absolute offset overflow",
                    },
                )
            }
        })
        .collect::<Result<Vec<_>, DexEmitError>>()?;

    // type_list blob follows annotation + encoded_array sections, 4-byte aligned.
    let type_list_base = align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
        section: "type_list_section",
        context: "alignment padding overflowed u32",
    })?;
    let type_list_pad = type_list_base.saturating_sub(off);
    if type_list_pad > 0
        && input_section_offset(dex, AlignmentSection::TypeList) != Some(type_list_base)
    {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::TypeList,
            byte_count: type_list_pad,
        });
    }
    off = type_list_base;
    off = off.saturating_add(u32::try_from(type_list_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "type_list_section",
            context: "type_list_section offset overflow",
        }
    })?);

    // map_list lives at the end of the data section, 4-byte aligned.
    let map_off = align_up_u32(off, 4).ok_or(DexEmitError::OffsetOverflow {
        section: "map_list",
        context: "map_list alignment overflow",
    })?;
    let map_pad = map_off.saturating_sub(off);
    if map_pad > 0 && input_section_offset(dex, AlignmentSection::MapList) != Some(map_off) {
        emit_transforms.push(CanonicalTransform::AlignmentPaddingInserted {
            section: AlignmentSection::MapList,
            byte_count: map_pad,
        });
    }
    off = map_off;
    let mut map_items = build_map_items(
        string_id_items.len() / 4,
        type_id_items.len() / 4,
        dex.protos.len(),
        field_id_items.len() / 8,
        method_id_items.len() / 8,
        dex.class_defs.len(),
        dex.call_site_ids.len(),
        dex.method_handles.len(),
        dex.strings.len(),
        string_ids_off,
        type_ids_off,
        proto_ids_off,
        field_ids_off,
        method_ids_off,
        class_defs_off,
        call_site_ids_off,
        method_handles_off,
        data_off,
        map_off,
        &dex.type_lists,
        type_list_base,
        &dex.code_items,
        code_item_base,
        &dex.class_datas,
        class_data_base,
        &dex.annotation_items,
        annotation_item_base,
        &dex.annotation_sets,
        annotation_set_base,
        &dex.annotation_set_ref_lists,
        annotation_set_ref_list_base,
        &dex.annotations,
        annotation_directory_base,
        &dex.encoded_arrays,
        encoded_array_base,
        &dex.debug_info_raw_bytes,
        debug_info_base,
    );
    // D7 observation: emit's offset-sort canonicalizes map_items into
    // ascending-offset order (DEX spec §7.18). Push
    // `MapListReordered` iff the input's on-disk order differs from
    // the canonical order on the type-code subset present in both —
    // and only when the canonicalizing sort actually fires (default).
    // When `preserve_map_list_order` is set we reorder to the input's
    // on-disk type-code sequence (no canonicalization, no push).
    if config.preserve_map_list_order {
        map_items = reorder_map_items_to_input(map_items, &dex.map_entries);
    } else {
        map_items.sort_by_key(|m| m.offset);
        if map_list_order_diverged(&map_items, &dex.map_entries) {
            emit_transforms.push(CanonicalTransform::MapListReordered);
        }
    }
    // Push DataSectionLayoutReordered when input's data-section
    // subsection order differs from emit's canonical sequential order.
    // This is the variant that, when fired, would be eliminated by
    // `EmitConfig::preserve_data_section_layout = true`.
    if data_section_order_diverges_from_emit(dex) {
        emit_transforms.push(CanonicalTransform::DataSectionLayoutReordered);
    }
    let map_list_bytes = emit_map_list(&map_items, config.preserve_map_list_order)?;
    off = off.saturating_add(u32::try_from(map_list_bytes.len()).map_err(|_| {
        DexEmitError::OffsetOverflow {
            section: "map_list",
            context: "map_list offset overflow",
        }
    })?);

    let file_size = off;
    let data_size = file_size.saturating_sub(data_off);

    // Phase 3b: emit proto_id_items + class_def_items with
    // type_list offsets rewritten to point into the new layout.
    let proto_id_items = {
        let remapped: Vec<ProtoIdItem> = dex
            .protos
            .iter()
            .map(|p| {
                let new_off = if p.parameters_off == 0 {
                    0
                } else {
                    let local = *type_list_remap.get(&p.parameters_off).ok_or(
                        DexEmitError::UnrepresentableIR {
                            why: "proto.parameters_off references type_list not in DexFile.type_lists",
                        },
                    )?;
                    type_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "proto.parameters_off",
                        context: "remapped type_list offset exceeds u32",
                    })?
                };
                Ok(ProtoIdItem {
                    shorty_idx: p.shorty_idx,
                    return_type_idx: p.return_type_idx,
                    parameters_off: new_off,
                })
            })
            .collect::<Result<Vec<_>, DexEmitError>>()?;
        emit_proto_pool(&remapped)
    };
    let class_def_items = {
        let remapped: Vec<ClassDefItem> = dex
            .class_defs
            .iter()
            .map(|c| {
                let new_interfaces_off = if c.interfaces_off == 0 {
                    0
                } else {
                    let local = *type_list_remap.get(&c.interfaces_off).ok_or(
                        DexEmitError::UnrepresentableIR {
                            why: "class.interfaces_off references type_list not in DexFile.type_lists",
                        },
                    )?;
                    type_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "class.interfaces_off",
                        context: "remapped interfaces offset exceeds u32",
                    })?
                };
                let new_class_data_off = if c.class_data_off == 0 {
                    0
                } else {
                    let local = *class_data_remap.get(&c.class_data_off).ok_or(
                        DexEmitError::UnrepresentableIR {
                            why: "class.class_data_off references class_data not in DexFile.class_datas",
                        },
                    )?;
                    class_data_base.checked_add(local).ok_or(
                        DexEmitError::OffsetOverflow {
                            section: "class.class_data_off",
                            context: "remapped class_data offset exceeds u32",
                        },
                    )?
                };
                let new_annotations_off = if c.annotations_off == 0 {
                    0
                } else {
                    let local = *annotation_directory_remap
                        .get(&c.annotations_off)
                        .ok_or(DexEmitError::UnrepresentableIR {
                            why: "class.annotations_off references annotation_directory not in DexFile.annotations",
                        })?;
                    annotation_directory_base.checked_add(local).ok_or(
                        DexEmitError::OffsetOverflow {
                            section: "class.annotations_off",
                            context: "remapped annotation_directory offset exceeds u32",
                        },
                    )?
                };
                let new_static_values_off = if c.static_values_off == 0 {
                    0
                } else {
                    let local = *encoded_array_remap
                        .get(&c.static_values_off)
                        .ok_or(DexEmitError::UnrepresentableIR {
                            why: "class.static_values_off references encoded_array not in DexFile.encoded_arrays",
                        })?;
                    encoded_array_base.checked_add(local).ok_or(
                        DexEmitError::OffsetOverflow {
                            section: "class.static_values_off",
                            context: "remapped encoded_array offset exceeds u32",
                        },
                    )?
                };
                Ok(ClassDefItem {
                    class_idx: c.class_idx,
                    access_flags: c.access_flags,
                    superclass_idx: c.superclass_idx,
                    interfaces_off: new_interfaces_off,
                    source_file_idx: c.source_file_idx,
                    annotations_off: new_annotations_off,
                    class_data_off: new_class_data_off,
                    static_values_off: new_static_values_off,
                })
            })
            .collect::<Result<Vec<_>, DexEmitError>>()?;
        emit_class_def_pool(&remapped)?
    };

    // call_site_id_items — one u32 offset per entry into the
    // call_site_data region written in the data section (above).
    let call_site_id_items = emit_call_site_id_section(&call_site_data_offsets)?;
    debug_assert_eq!(
        call_site_id_items.len() as u32,
        call_site_ids_bytes_len,
        "call_site_ids byte length disagrees with pre-computed layout"
    );

    // method_handle_items — fixed 8-byte record per entry.
    let method_handle_items = emit_method_handle_section(&dex.method_handles)?;
    debug_assert_eq!(
        method_handle_items.len() as u32,
        method_handles_bytes_len,
        "method_handles byte length disagrees with pre-computed layout"
    );

    debug_assert_eq!(
        proto_id_items.len() as u32,
        proto_bytes_len,
        "proto pool byte length disagrees with pre-computed layout"
    );
    debug_assert_eq!(
        class_def_items.len() as u32,
        class_def_bytes_len,
        "class_def pool byte length disagrees with pre-computed layout"
    );

    // Phase 4: rewrite string_id_items with absolute string_data
    // offsets. emit_string_pool produced offsets relative to the
    // start of the string_data block; add data_off to lift them.
    let fixed_string_id_items = {
        let mut fixed = Vec::with_capacity(string_id_items.len());
        for chunk in string_id_items.chunks_exact(4) {
            let mut rel = [0u8; 4];
            rel.copy_from_slice(chunk);
            let rel = u32::from_le_bytes(rel);
            let abs = data_off.saturating_add(rel);
            fixed.extend_from_slice(&abs.to_le_bytes());
        }
        // String_id_items is emitted in 4-byte strides by
        // emit_string_pool; any trailing bytes would indicate an
        // upstream bug, so assert in debug builds.
        debug_assert!(
            string_id_items.len().is_multiple_of(4),
            "string_id_items length must be a multiple of 4 bytes (stride of StringIdItem)"
        );
        fixed
    };

    // Phase 5: emit header with resolved offsets (placeholder
    // checksum + signature), then concatenate all sections.
    let layout = HeaderLayout {
        version: {
            let mut v = [0u8; 3];
            v.copy_from_slice(&dex.header.magic[4..7]);
            v
        },
        file_size,
        map_off,
        string_ids_size: u32::try_from(dex.strings.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "string_ids_size exceeds u32",
            }
        })?,
        string_ids_off,
        type_ids_size: u32::try_from(dex.type_descriptors.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "type_ids_size exceeds u32",
            }
        })?,
        type_ids_off,
        proto_ids_size: u32::try_from(dex.protos.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "proto_ids_size exceeds u32",
            }
        })?,
        proto_ids_off,
        field_ids_size: u32::try_from(dex.fields.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "field_ids_size exceeds u32",
            }
        })?,
        field_ids_off,
        method_ids_size: u32::try_from(dex.methods.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "method_ids_size exceeds u32",
            }
        })?,
        method_ids_off,
        class_defs_size: u32::try_from(dex.class_defs.len()).map_err(|_| {
            DexEmitError::SizeOverflow {
                layer: "header",
                context: "class_defs_size exceeds u32",
            }
        })?,
        class_defs_off,
        data_size,
        data_off,
    };
    let header_bytes = emit_header(&layout);

    let mut out = Vec::with_capacity(alloc_cap(file_size as usize));
    out.extend_from_slice(&header_bytes);
    out.extend_from_slice(&fixed_string_id_items);
    out.extend_from_slice(&type_id_items);
    out.extend_from_slice(&proto_id_items);
    out.extend_from_slice(&field_id_items);
    out.extend_from_slice(&method_id_items);
    out.extend_from_slice(&class_def_items);
    out.extend_from_slice(&call_site_id_items);
    out.extend_from_slice(&method_handle_items);
    out.extend_from_slice(&string_data_items);
    out.extend_from_slice(&vec![0u8; code_item_pad as usize]);
    out.extend_from_slice(&code_item_bytes);
    out.extend_from_slice(&debug_info_bytes);
    out.extend_from_slice(&class_data_bytes);
    out.extend_from_slice(&annotation_item_bytes);
    out.extend_from_slice(&vec![0u8; annotation_set_pad as usize]);
    out.extend_from_slice(&annotation_set_bytes);
    out.extend_from_slice(&vec![0u8; annotation_set_ref_list_pad as usize]);
    out.extend_from_slice(&annotation_set_ref_list_bytes);
    out.extend_from_slice(&vec![0u8; annotation_directory_pad as usize]);
    out.extend_from_slice(&annotation_directory_bytes);
    out.extend_from_slice(&encoded_array_bytes);
    out.extend_from_slice(&vec![0u8; type_list_pad as usize]);
    out.extend_from_slice(&type_list_bytes);
    out.extend_from_slice(&vec![0u8; map_pad as usize]);
    out.extend_from_slice(&map_list_bytes);

    // Hard invariant: layout arithmetic must match actual buffer length.
    // Return typed Err rather than debug_assert so release builds surface
    // internal layout bugs instead of shipping corrupt bytes.
    if out.len() as u64 != u64::from(file_size) {
        return Err(DexEmitError::InvariantViolated {
            why: "emit_dex layout arithmetic disagrees with concatenated buffer length",
        });
    }

    // Phase 6: finalize.
    finalize_checksums(&mut out)?;
    apply_preserve_input_checksums(&mut out, dex, config)?;
    Ok((out, emit_transforms))
}

/// Round `v` up to the next multiple of `align` (a power of 2).
/// Returns None if the rounded value overflows u32.
fn align_up_u32(v: u32, align: u32) -> Option<u32> {
    debug_assert!(align.is_power_of_two(), "align must be power-of-two");
    let mask = align.wrapping_sub(1);
    v.checked_add(mask).map(|x| x & !mask)
}

/// Synthesize the map_list entries from resolved section offsets +
/// sizes. Called once per emit_dex invocation. Zero-size sections
/// (e.g., a DEX with no protos) are omitted — the spec permits
/// absent sections to have no map_item.
#[allow(clippy::too_many_arguments, reason = "DEX map_list section is naturally many-fielded (one arg per section count + offset; ~16 sections). Bundling into a struct here would just relocate the field-listing.")]
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: every `usize as u32` here is a section count or .len() of a section's emit_buf, each bounded by the DEX file-format spec (every section header counter is a u32 in DEX). For a spec-compliant DEX, narrowing is exact."
)]
fn build_map_items(
    string_ids_count: usize,
    type_ids_count: usize,
    proto_ids_count: usize,
    field_ids_count: usize,
    method_ids_count: usize,
    class_defs_count: usize,
    call_site_ids_count: usize,
    method_handles_count: usize,
    string_data_count: usize,
    string_ids_off: u32,
    type_ids_off: u32,
    proto_ids_off: u32,
    field_ids_off: u32,
    method_ids_off: u32,
    class_defs_off: u32,
    call_site_ids_off: u32,
    method_handles_off: u32,
    data_off: u32,
    map_off: u32,
    type_lists: &std::collections::BTreeMap<u32, Vec<TypeIdx>>,
    type_list_base: u32,
    code_items: &std::collections::BTreeMap<u32, crate::decode::CodeItem>,
    code_item_base: u32,
    class_datas: &std::collections::BTreeMap<u32, crate::decode::ClassData>,
    class_data_base: u32,
    annotation_items: &std::collections::BTreeMap<u32, crate::annotation::AnnotationItem>,
    annotation_item_base: u32,
    annotation_sets: &std::collections::BTreeMap<u32, Vec<u32>>,
    annotation_set_base: u32,
    annotation_set_ref_lists: &std::collections::BTreeMap<u32, Vec<u32>>,
    annotation_set_ref_list_base: u32,
    annotation_directories: &std::collections::BTreeMap<
        u32,
        crate::annotation::AnnotationDirectoryItem,
    >,
    annotation_directory_base: u32,
    encoded_arrays: &std::collections::BTreeMap<u32, Vec<EncodedValue>>,
    encoded_array_base: u32,
    debug_info_raw_bytes: &std::collections::BTreeMap<u32, Vec<u8>>,
    debug_info_base: u32,
) -> Vec<MapItem> {
    let mut items = Vec::new();
    // HEADER_ITEM is always present and at offset 0.
    items.push(MapItem {
        type_code: map_type::HEADER_ITEM,
        size: 1,
        offset: 0,
    });
    if string_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::STRING_ID_ITEM,
            size: string_ids_count as u32,
            offset: string_ids_off,
        });
    }
    if type_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::TYPE_ID_ITEM,
            size: type_ids_count as u32,
            offset: type_ids_off,
        });
    }
    if proto_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::PROTO_ID_ITEM,
            size: proto_ids_count as u32,
            offset: proto_ids_off,
        });
    }
    if field_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::FIELD_ID_ITEM,
            size: field_ids_count as u32,
            offset: field_ids_off,
        });
    }
    if method_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::METHOD_ID_ITEM,
            size: method_ids_count as u32,
            offset: method_ids_off,
        });
    }
    if class_defs_count > 0 {
        items.push(MapItem {
            type_code: map_type::CLASS_DEF_ITEM,
            size: class_defs_count as u32,
            offset: class_defs_off,
        });
    }
    if call_site_ids_count > 0 {
        items.push(MapItem {
            type_code: map_type::CALL_SITE_ID_ITEM,
            size: call_site_ids_count as u32,
            offset: call_site_ids_off,
        });
    }
    if method_handles_count > 0 {
        items.push(MapItem {
            type_code: map_type::METHOD_HANDLE_ITEM,
            size: method_handles_count as u32,
            offset: method_handles_off,
        });
    }
    if string_data_count > 0 {
        items.push(MapItem {
            type_code: map_type::STRING_DATA_ITEM,
            size: string_data_count as u32,
            offset: data_off,
        });
    }
    if !code_items.is_empty() {
        items.push(MapItem {
            type_code: map_type::CODE_ITEM,
            size: code_items.len() as u32,
            offset: code_item_base,
        });
    }
    if !class_datas.is_empty() {
        items.push(MapItem {
            type_code: map_type::CLASS_DATA_ITEM,
            size: class_datas.len() as u32,
            offset: class_data_base,
        });
    }
    if !annotation_items.is_empty() {
        items.push(MapItem {
            type_code: map_type::ANNOTATION_ITEM,
            size: annotation_items.len() as u32,
            offset: annotation_item_base,
        });
    }
    if !annotation_sets.is_empty() {
        items.push(MapItem {
            type_code: map_type::ANNOTATION_SET_ITEM,
            size: annotation_sets.len() as u32,
            offset: annotation_set_base,
        });
    }
    if !annotation_set_ref_lists.is_empty() {
        items.push(MapItem {
            type_code: map_type::ANNOTATION_SET_REF_LIST,
            size: annotation_set_ref_lists.len() as u32,
            offset: annotation_set_ref_list_base,
        });
    }
    if !annotation_directories.is_empty() {
        items.push(MapItem {
            type_code: map_type::ANNOTATION_DIRECTORY_ITEM,
            size: annotation_directories.len() as u32,
            offset: annotation_directory_base,
        });
    }
    if !encoded_arrays.is_empty() {
        items.push(MapItem {
            type_code: map_type::ENCODED_ARRAY_ITEM,
            size: encoded_arrays.len() as u32,
            offset: encoded_array_base,
        });
    }
    if !debug_info_raw_bytes.is_empty() {
        items.push(MapItem {
            type_code: map_type::DEBUG_INFO_ITEM,
            size: debug_info_raw_bytes.len() as u32,
            offset: debug_info_base,
        });
    }
    if !type_lists.is_empty() {
        items.push(MapItem {
            type_code: map_type::TYPE_LIST,
            size: type_lists.len() as u32,
            offset: type_list_base,
        });
    }
    items.push(MapItem {
        type_code: map_type::MAP_LIST,
        size: 1,
        offset: map_off,
    });
    // Sorting was hoisted out of this builder so callers can opt out
    // for byte-identity round-trips via
    // `EmitConfig::preserve_map_list_order`. The default emit path
    // sorts immediately after this builder returns; the preserve
    // path skips the sort and pushes no `MapListReordered` transform.
    items
}

// ── ULEB128 + MUTF-8 encoders (DEX §7.1, §7.5) ──────────────────────
//
// `droidsaw-common::encoding` has the decoders; encoders live here per
// the second-adopter rule (move to common only once a second format
// bundle adopts them). These are the primitives every DEX section
// emitter depends on.

/// Cap for `Vec::with_capacity` pre-allocation hints on emit paths
/// that consume attacker-controlled pool counts. 1 GB is well above
/// any legitimate DEX size (largest shipping primary-dex is ~30 MB)
/// but bounded enough that adversarial IR with pool counts near
/// `usize::MAX` can't drive a multi-GB allocation request.
///
/// Vec grows as needed past the cap — this only affects
/// pre-allocation, not correctness. The trade-off: on genuinely
/// large (but not adversarial) output, the first few pushes past the
/// cap trigger one reallocation each, then geometric growth kicks in.
/// Negligible cost; meaningful protection.
const ALLOC_HINT_CAP: usize = 1 << 30;

/// Bound an allocation-size hint against adversarial input-driven
/// overflow. Use at every `Vec::with_capacity` site where the capacity
/// is derived from an attacker-controlled count.
#[inline]
fn alloc_cap(hint: usize) -> usize {
    hint.min(ALLOC_HINT_CAP)
}

/// Append the MUTF-8 encoding of `s` to `out`. DEX spec §7.5.
///
/// Differences from UTF-8: the NUL character (U+0000) is encoded as
/// the two-byte sequence `C0 80` so the stream can be null-terminated;
/// supplementary-plane characters (U+10000..=U+10FFFF) are encoded as a
/// surrogate pair, each surrogate as a 3-byte MUTF-8 sequence, rather
/// than the single 4-byte UTF-8 form.
///
/// Does NOT emit a trailing 0x00 terminator — the caller
/// (`emit_string_pool`) adds that.
#[allow(clippy::arithmetic_side_effects, reason = "PROOF: every arithmetic op here is on values bounded by the char domain. `cp` is a Rust `char as u32` so 0..=0x10FFFF. Shifts are by compile-time constants. `cp - 0x10000` is only reached in the `else` branch where `cp >= 0x10000`, so subtraction cannot underflow. `adj = cp - 0x10000` is at most 0xFFFFF (20 bits), so `adj >> 10` ≤ 0x3FF and `high` / `low` fit in 16 bits. None of these sites are attacker-controllable beyond the char domain.")]
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: `ch as u32` widens a Rust char (scalar value 0..=0x10FFFF) to u32, lossless. Every `as u8` is preceded by a `& mask` (0x1F, 0x3F, or 0x0F) that bounds the value to ≤ 0x3F ≤ u8::MAX; narrowing is exact. `cp as u8` in the ASCII branch is guarded by `cp < 0x80` so the value is ≤ 0x7F, exact."
)]
pub fn encode_mutf8_into(out: &mut Vec<u8>, s: &str) {
    for ch in s.chars() {
        let cp = ch as u32;
        if cp == 0 {
            out.push(0xC0);
            out.push(0x80);
        } else if cp < 0x80 {
            out.push(cp as u8);
        } else if cp < 0x800 {
            out.push(0xC0 | ((cp >> 6) & 0x1F) as u8);
            out.push(0x80 | (cp & 0x3F) as u8);
        } else if cp < 0x10000 {
            out.push(0xE0 | ((cp >> 12) & 0x0F) as u8);
            out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
            out.push(0x80 | (cp & 0x3F) as u8);
        } else {
            // Supplementary plane: encode as surrogate pair, each
            // surrogate as a 3-byte MUTF-8 sequence (NOT UTF-8's
            // 4-byte form).
            let adj = cp - 0x10000;
            let high = 0xD800 | (adj >> 10);
            let low = 0xDC00 | (adj & 0x3FF);
            for surrogate in [high, low] {
                out.push(0xE0 | ((surrogate >> 12) & 0x0F) as u8);
                out.push(0x80 | ((surrogate >> 6) & 0x3F) as u8);
                out.push(0x80 | (surrogate & 0x3F) as u8);
            }
        }
    }
}

/// Count the UTF-16 code units the string would occupy in MUTF-8 /
/// Java semantics — the value that goes in the `utf16_size` ULEB128
/// prefix of a `string_data_item` (DEX spec §7.5). Supplementary-plane
/// characters count as 2 (one per surrogate half); all other
/// characters count as 1.
#[allow(
    clippy::as_conversions,
    reason = "PROOF: `ch as u32` widens a Rust char (scalar value 0..=0x10FFFF) to u32, lossless on all platforms."
)]
pub fn utf16_unit_count(s: &str) -> u32 {
    let mut n: u32 = 0;
    for ch in s.chars() {
        n = n.saturating_add(if (ch as u32) >= 0x10000 { 2 } else { 1 });
    }
    n
}

// ── String pool emit (DEX §7.4 string_id_item + §7.5 string_data_item) ─

/// Serialize the string pool into two byte blobs:
///
/// - `id_items`: the `string_id_item[]` array, stride 4 bytes per entry: a u32
///   LE offset into the final DEX file pointing at the `string_data_item` for
///   that string. Offsets are returned RELATIVE to the start of the data
///   section — callers shift by `data_section_base_off` when placing into the
///   final image.
/// - `data_items`: the concatenated `string_data_item[]` bodies. Each body is
///   `ULEB128(utf16_size) || MUTF-8 || 0x00`.
///
/// Verify `strings` is in canonical UTF-16 code-unit order — the sort
/// key DEX spec §7.4 requires for `string_id_item`.
///
/// ## Why not Rust's `str: Ord` (UTF-8 byte order)?
///
/// UTF-8 byte order matches UTF-16 code-unit order for BMP codepoints
/// but DIVERGES for supplementary-plane codepoints:
///   "🥰" (U+1F970)   UTF-8: F0 9F A5 B0    UTF-16: D83E DD70
///   "\u{f000}"       UTF-8: EF 80 80       UTF-16: F000
/// UTF-8 says 🥰 > \u{f000} (F0 > EF); UTF-16 says 🥰 < \u{f000}
/// (D83E < F000). Real d8/r8-produced DEX uses UTF-16 code-unit order,
/// so Rust's string comparison is the wrong check — fires false
/// positives on corpus DEX.
///
/// ## Why not MUTF-8 byte order?
///
/// Same BMP-agreement, same supp-plane-agreement, but differs from
/// UTF-16 for U+0000: MUTF-8 encodes U+0000 as `C0 80` (2 bytes);
/// UTF-16 code unit is 0x0000 (1 unit). Strings containing embedded
/// NULs sort differently.
///
/// This function encodes each string as UTF-16 code units and
/// compares the resulting `Vec<u16>`s lexicographically.
pub fn verify_utf16_sorted(strings: &[crate::DexString]) -> Result<(), OrderingViolation> {
    let mut prev: Vec<u16> = Vec::new();
    let mut curr: Vec<u16> = Vec::new();
    for (i, entry) in strings.iter().enumerate() {
        curr.clear();
        // For `Decoded` entries, use the decoded `&str`'s UTF-16
        // encoding. For `MalformedMutf8` entries, walk the raw
        // MUTF-8 bytes as CESU-8 → UTF-16 code units — this is the
        // on-disk sort key the DEX spec uses, and it matches what
        // d8/r8 emit. Using the lossy U+FFFD-substituted view would
        // sort `MalformedMutf8` entries by U+FFFD position instead
        // of by their original CESU-8 code units, breaking the
        // round-trip on lossy strings (corpus observation from emit
        // validation testing).
        match entry {
            crate::DexString::Decoded { s, .. } => {
                curr.extend(s.encode_utf16());
            }
            crate::DexString::MalformedMutf8 { raw_bytes, .. } => {
                push_cesu8_units(&mut curr, raw_bytes);
            }
        }
        if i > 0 && prev.as_slice() > curr.as_slice() {
            return Err(OrderingViolation { index: i });
        }
        std::mem::swap(&mut prev, &mut curr);
    }
    Ok(())
}

/// Walk `bytes` as CESU-8 / MUTF-8 and push each emitted UTF-16
/// code unit onto `out`. Used by [`verify_utf16_sorted`] to derive
/// the on-disk sort key from raw bytes for
/// [`crate::DexString::MalformedMutf8`] entries whose decoded view
/// would be U+FFFD-substituted and sort wrong.
///
/// Malformed sequences (truncated continuation, bad continuation
/// byte, etc.) push the lead byte as a `u16` and advance by 1 —
/// best-effort recovery to avoid panicking on arbitrary input.
fn push_cesu8_units(out: &mut Vec<u16>, bytes: &[u8]) {
    let mut i = 0;
    while i < bytes.len() {
        let b0 = bytes[i];
        if b0 < 0x80 {
            out.push(u16::from(b0));
            i = i.saturating_add(1);
        } else if b0 & 0xE0 == 0xC0 {
            // 2-byte: U+0000..=U+07FF (including encoded NUL C0 80).
            let b1 = bytes.get(i.saturating_add(1)).copied().unwrap_or(0);
            let unit = (u16::from(b0 & 0x1F) << 6) | u16::from(b1 & 0x3F);
            out.push(unit);
            i = i.saturating_add(2);
        } else if b0 & 0xF0 == 0xE0 {
            // 3-byte: U+0800..=U+FFFF OR one surrogate half (D800..DFFF).
            let b1 = bytes.get(i.saturating_add(1)).copied().unwrap_or(0);
            let b2 = bytes.get(i.saturating_add(2)).copied().unwrap_or(0);
            let unit = (u16::from(b0 & 0x0F) << 12)
                | (u16::from(b1 & 0x3F) << 6)
                | u16::from(b2 & 0x3F);
            out.push(unit);
            i = i.saturating_add(3);
        } else {
            // Malformed lead — advance by 1 with the byte as a unit.
            out.push(u16::from(b0));
            i = i.saturating_add(1);
        }
    }
}

/// Emit the string_id_item[] + string_data_item[] blobs from raw
/// MUTF-8 byte sequences. Each `string_raw_bytes[i]` is the
/// on-disk byte content (excluding the ULEB128 utf16_size prefix
/// and the trailing null terminator) from the input DEX. Parser
/// populates this on DexFile.
///
/// ## Why raw bytes, not `&[String]`?
///
/// Rust's `String` cannot represent unpaired surrogate halves or
/// other byte sequences valid under MUTF-8 but invalid under Unicode
/// scalar rules. Real-world DEX files contain such strings. The
/// parser's fallback on decode failure is
/// `String::from_utf8_lossy`, which substitutes U+FFFD — lossy.
/// Re-encoding the U+FFFD-substituted string back to MUTF-8 would
/// change the byte content AND potentially change sort position.
///
/// By carrying raw bytes through the pipeline, emit guarantees
/// byte-identity on strings regardless of encoding edge cases.
/// Caller contract: `string_raw_bytes` must be in the on-disk order
/// (which the parser preserves).
///
/// Each entry produces: ULEB128(utf16_unit_count) || raw_bytes || 0x00.
/// The utf16_unit_count is derived by walking the MUTF-8 bytes and
/// counting code units emitted (NOT codepoints — supplementary-plane
/// chars count as 2 surrogate halves per DEX spec §7.5).
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: `data_items.len() as u32` — data_items accumulates one string_data_item per string in the pool; DEX file-format spec stores string_ids_size as u32, bounding the total string pool byte length to u32::MAX. Narrowing usize → u32 is exact for any spec-compliant DEX."
)]
pub fn emit_string_pool(
    string_raw_bytes: &[&[u8]],
) -> (Vec<u8>, Vec<u8>) {
    let mut id_items = Vec::with_capacity(alloc_cap(string_raw_bytes.len().saturating_mul(4)));
    let mut data_items: Vec<u8> = Vec::new();

    for &bytes in string_raw_bytes.iter() {
        let rel_off = data_items.len() as u32;
        id_items.extend_from_slice(&rel_off.to_le_bytes());
        write_uleb128(&mut data_items, utf16_unit_count_from_mutf8(bytes));
        data_items.extend_from_slice(bytes);
        data_items.push(0x00);
    }

    (id_items, data_items)
}

/// Preserve-mode variant of `emit_string_pool`: lays out string_data
/// at the input's recorded per-string offsets rather than sequentially
/// in StringIdx order.
///
/// `input_data_offs[i]` is the input's `string_id_item.string_data_off`
/// for StringIdx i — the absolute byte offset of that string's data in
/// the input DEX. `string_data_base` is the absolute byte offset of
/// the string_data section in the output (= input's section base under
/// preserve mode). `section_size` is the input's string_data section
/// length (max byte we may write within the blob).
///
/// Returns `(string_id_items, string_data_items)` where:
/// - `string_id_items[i]` is a 4-byte LE u32 = `input_data_offs[i]`
///   (the input's absolute offset, byte-identical to input's string_ids).
/// - `string_data_items` is a `section_size`-byte buffer with each
///   string's MUTF-8-encoded data (uleb128(char_count) + raw_bytes + NUL)
///   written at `input_data_offs[i] - string_data_base`. Gaps between
///   strings retain zero padding.
///
/// Used by `emit_dex_inner_preserve_layout` to close the byte-identity
/// gap on inputs whose string_data physical order doesn't match the
/// StringIdx sequential order.
pub fn emit_string_pool_preserve_layout(
    string_raw_bytes: &[&[u8]],
    input_data_offs: &[u32],
    string_data_base: u32,
    section_size: u32,
) -> Result<(Vec<u8>, Vec<u8>), DexEmitError> {
    if string_raw_bytes.len() != input_data_offs.len() {
        return Err(DexEmitError::UnrepresentableIR {
            why: "emit_string_pool_preserve_layout: string_raw_bytes and input_data_offs length mismatch",
        });
    }
    let mut id_items = Vec::with_capacity(alloc_cap(string_raw_bytes.len().saturating_mul(4)));
    let section_size_usize = usize::try_from(section_size).map_err(|_| DexEmitError::UnrepresentableIR {
        why: "emit_string_pool_preserve_layout: section_size exceeds usize",
    })?;
    let mut data_items = vec![0u8; section_size_usize];
    for (i, &bytes) in string_raw_bytes.iter().enumerate() {
        let abs_off = input_data_offs[i];
        id_items.extend_from_slice(&abs_off.to_le_bytes());
        let rel_u32 = abs_off.checked_sub(string_data_base).ok_or(DexEmitError::UnrepresentableIR {
            why: "emit_string_pool_preserve_layout: input string_data_off precedes section base",
        })?;
        let rel = usize::try_from(rel_u32).map_err(|_| DexEmitError::UnrepresentableIR {
            why: "emit_string_pool_preserve_layout: rel offset exceeds usize",
        })?;
        // Encode uleb128(char_count) + raw_bytes + NUL into a scratch
        // buffer, then place at the preserved offset.
        let mut scratch = Vec::with_capacity(bytes.len().saturating_add(6));
        write_uleb128(&mut scratch, utf16_unit_count_from_mutf8(bytes));
        scratch.extend_from_slice(bytes);
        scratch.push(0x00);
        let end = rel.saturating_add(scratch.len());
        if end > data_items.len() {
            return Err(DexEmitError::UnrepresentableIR {
                why: "emit_string_pool_preserve_layout: string overruns preserved section size",
            });
        }
        data_items[rel..end].copy_from_slice(&scratch);
    }
    Ok((id_items, data_items))
}

/// Count UTF-16 code units in a MUTF-8 byte sequence without
/// allocating a Rust `String`. Mirrors DEX spec §7.5 `utf16_size`
/// semantics: each 1-byte ASCII char is 1 code unit, each 2-byte
/// MUTF-8 (including `C0 80` for U+0000) is 1 code unit, each 3-byte
/// MUTF-8 is 1 code unit (this includes surrogate halves, which are
/// 1 code unit each — two surrogate halves = 2 code units = 1
/// supplementary-plane codepoint).
///
/// Assumes well-formed MUTF-8 (parser already validated). Malformed
/// bytes count conservatively (skip-1) — produces wrong count but
/// doesn't panic.
fn utf16_unit_count_from_mutf8(bytes: &[u8]) -> u32 {
    let mut count: u32 = 0;
    let mut i = 0;
    while i < bytes.len() {
        let b0 = bytes[i];
        let step = if b0 < 0x80 {
            1
        } else if b0 & 0xE0 == 0xC0 {
            2
        } else if b0 & 0xF0 == 0xE0 {
            3
        } else {
            1 // malformed byte — skip
        };
        count = count.saturating_add(1);
        i = i.saturating_add(step);
    }
    count
}

// ── Type pool emit (DEX §7.6 type_id_item) ──────────────────────────

/// Serialize the type pool into the `type_id_item[]` byte blob.
///
/// Each `type_id_item` is a single u32 LE: a `descriptor_idx` —
/// an index into the (already-sorted) string pool, pointing at the
/// type's descriptor string (e.g. `"Ljava/lang/String;"`).
///
/// The DEX spec (§7.6) requires `type_ids` to be sorted by
/// `descriptor_idx`, matching the ordering of the string pool they
/// reference. The `NonDecreasing<StringIdx>` input encodes that
/// invariant at the type level.
///
/// Returns an empty vec for an empty pool (spec: `type_ids_off = 0`
/// when the pool is empty; header writer handles the 0-or-offset
/// distinction).
pub fn emit_type_pool(type_descriptor_idxs: &NonDecreasing<StringIdx>) -> Vec<u8> {
    let mut out = Vec::with_capacity(alloc_cap(type_descriptor_idxs.len().saturating_mul(4)));
    for idx in type_descriptor_idxs.iter() {
        out.extend_from_slice(&idx.0.to_le_bytes());
    }
    out
}

// ── Proto pool + type_list emit (DEX §7.7 proto_id_item, §7.3 type_list) ─

/// Serialize a single `type_list` (DEX §7.3):
///
/// ```text
/// struct type_list {
///     uint  size;              // u32 LE, count of entries
///     u16   list[size];        // type_item array
/// }
/// ```
///
/// Each `type_item` is a u16 `type_idx` — the DEX format's u16 stride
/// caps addressable types at 65,536 per `type_list`. A `TypeIdx` whose
/// u32 value exceeds `u16::MAX` returns
/// [`DexEmitError::UnrepresentableIR`] — the caller-supplied IR
/// references a type that cannot fit in a type_list slot.
///
/// Returns the serialized bytes. Caller is responsible for 4-byte
/// alignment of the `type_list` within the data section.
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: `size as u32` where size = type_idxs.len() — DEX type_list (§7.3) stores the element count as u32; pool sizes are bounded by u32::MAX per the DEX file-format spec. Narrowing is exact for any spec-compliant DEX."
)]
pub fn emit_type_list(type_idxs: &[TypeIdx]) -> Result<Vec<u8>, DexEmitError> {
    let size = type_idxs.len();
    let byte_len = 4_usize.saturating_add(size.saturating_mul(2));
    let mut out = Vec::with_capacity(alloc_cap(byte_len));
    out.extend_from_slice(&(size as u32).to_le_bytes());
    for tidx in type_idxs {
        let narrowed: u16 = tidx.0.try_into().map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "type_list entry exceeds u16 type_id space (DEX §7.3 type_item is u16)",
            }
        })?;
        out.extend_from_slice(&narrowed.to_le_bytes());
    }
    Ok(out)
}

/// Emit a contiguous `type_list` data-section blob from a set of
/// logical lists keyed by their *original* offset. Returns:
///
/// - the blob bytes (4-byte aligned internally between entries)
/// - a remap from each logical key (the original `parameters_off` /
///   `interfaces_off` that identified the list in the source DEX) to
///   the byte offset at which the list appears in the emitted blob
///
/// The caller applies `blob_base_offset + remap[original_off]` to each
/// `proto.parameters_off` / `class.interfaces_off` before writing the
/// id pools, so post-emission references resolve to the new layout.
///
/// ## Layout and alignment (DEX §7.7 / §7.3)
///
/// Each entry is a `type_list { u32 size; u16 list[size]; }`. DEX
/// requires `type_list` records to be 4-byte-aligned. The 4-byte
/// `size` header naturally aligns the start; the u16 list may leave
/// the buffer odd-byte-terminated, so this function pads with a zero
/// byte pair between entries as needed.
pub fn emit_type_list_section(
    lists: &std::collections::BTreeMap<u32, Vec<TypeIdx>>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    let total = lists.len();
    for (i, (original_off, idxs)) in lists.iter().enumerate() {
        // Record the local offset (relative to the start of the blob).
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "type_list_section",
            context: "type_list blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        blob.extend_from_slice(&emit_type_list(idxs)?);
        // 4-byte alignment pad: a type_list of N entries occupies
        // 4 + 2*N bytes, which is 4-byte-aligned iff N is even. Pad
        // with two zero bytes when N is odd — but ONLY between items
        // (the spec aligns each type_list's start, not the section
        // tail). Trailing pad on the final entry overruns the next
        // section under `EmitConfig::preserve_data_section_layout` when
        // the input's adjacent section is not 4-byte aligned (e.g.,
        // string_data follows immediately with no alignment slack).
        if i.saturating_add(1) < total && !idxs.len().is_multiple_of(2) {
            blob.extend_from_slice(&[0u8, 0u8]);
        }
    }
    Ok((blob, remap))
}

/// Assemble a flat u16 insn stream for a single method from its
/// decoded `instructions` + `payloads`. Instructions are written at
/// `insn.addr * 2`; payloads are written at their BTreeMap key
/// (also a pc in 16-bit code units), with the switch-instruction's
/// pc looked up in `instructions` to produce correct relative
/// offsets.
///
/// Returns `insn_bytes` — a whole number of u16 code units.
#[allow(
    clippy::as_conversions,
    reason = "PROOF: all casts here are u32 → usize widenings. `insns_size as usize`: DEX spec §7.16 caps insns_size to ≤ 0xFFFF code units (65535), so the value fits in usize on all supported 32-bit+ platforms. `insn.addr as usize` and `*payload_pc as usize`: instruction addresses are u32 PC values bounded by insns_size, so they're also ≤ 0xFFFF. All widening casts are lossless."
)]
fn assemble_insn_stream(
    instructions: &[crate::decode::Instruction],
    payloads: &std::collections::BTreeMap<u32, crate::decode::PayloadData>,
    insns_size: u32,
) -> Result<Vec<u8>, DexEmitError> {
    use crate::decode::PayloadData;
    let byte_size = (insns_size as usize).saturating_mul(2);
    let mut stream = vec![0u8; byte_size];

    for insn in instructions {
        let byte_off = (insn.addr as usize).saturating_mul(2);
        let mut buf = Vec::new();
        emit_instruction(&mut buf, insn)?;
        let end = byte_off
            .checked_add(buf.len())
            .ok_or(DexEmitError::OffsetOverflow {
                section: "insn_stream",
                context: "instruction end offset overflowed usize",
            })?;
        if end > stream.len() {
            return Err(DexEmitError::OffsetOverflow {
                section: "insn_stream",
                context: "instruction would write past declared insns_size",
            });
        }
        stream[byte_off..end].copy_from_slice(&buf);
    }

    // Payloads are independent bytes in the stream at their pc addrs.
    // For switch payloads we must recover switch_pc by finding the
    // insn whose `target` is this payload's pc.
    for (payload_pc, payload) in payloads {
        let switch_pc = match payload {
            PayloadData::PackedSwitch { .. } | PayloadData::SparseSwitch { .. } => {
                // Locate the matching switch instruction.
                instructions
                    .iter()
                    .find(|i| {
                        matches!(
                            i.op,
                            crate::opcodes::Opcode::PackedSwitch
                                | crate::opcodes::Opcode::SparseSwitch
                        ) && i.target == Some(*payload_pc)
                    })
                    .map(|i| i.addr)
                    .ok_or(DexEmitError::UnrepresentableIR {
                        why: "switch payload in code_item has no matching switch instruction",
                    })?
            }
            PayloadData::FillArrayData { .. } => 0, // switch_pc unused for fill
        };

        let byte_off = (*payload_pc as usize).saturating_mul(2);
        let mut buf = Vec::new();
        emit_payload(&mut buf, payload, switch_pc)?;
        let end = byte_off
            .checked_add(buf.len())
            .ok_or(DexEmitError::OffsetOverflow {
                section: "insn_stream",
                context: "payload end offset overflowed usize",
            })?;
        if end > stream.len() {
            return Err(DexEmitError::OffsetOverflow {
                section: "insn_stream",
                context: "payload would write past declared insns_size",
            });
        }
        stream[byte_off..end].copy_from_slice(&buf);
    }

    Ok(stream)
}

/// Emit a contiguous `code_item` data-section blob from a set of
/// decoded code items keyed by their original `code_off`. Each entry
/// is 4-byte aligned internally; returns (blob, remap) where
/// `remap[original_off] = local_offset_within_blob`. The caller adds
/// the section's absolute base offset to each local to resolve
/// `encoded_method.code_off` references.
///
/// Per DEX spec §7.16, code_items must be 4-byte aligned within the
/// data section. This function pads between entries to maintain the
/// invariant.
///
/// # Debug-info handling
///
/// `debug_info_off` references are rewritten to point into the
/// emitter-owned debug info region (see `emit_debug_info_section`).
/// Preserving the IR's raw offset values would yield dangling pointers
/// into the input byte layout, since the emitted output is an
/// independent byte buffer. Dangling debug_info pointers are not
/// merely a UX paper cut — they're an attacker-controlled dereference
/// vector: a lazy-reading consumer would land inside whatever emitted
/// data the attacker has populated.
pub fn emit_code_item_section(
    code_items: &std::collections::BTreeMap<u32, crate::decode::CodeItem>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, ci) in code_items {
        // 4-byte alignment before this entry.
        while !blob.len().is_multiple_of(4) {
            blob.push(0);
        }
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "code_item_section",
            context: "code_item blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);

        // Compute insns_size from instruction + payload spans.
        let insns_size = compute_insns_size(&ci.instructions, &ci.payloads)?;
        let insn_bytes = assemble_insn_stream(&ci.instructions, &ci.payloads, insns_size)?;
        let container_bytes = emit_code_item_container(
            ci.registers_size,
            ci.ins_size,
            ci.outs_size,
            /* debug_info_off */ 0, // stripped; see section-level docs above
            &insn_bytes,
            &ci.tries,
            &ci.catch_handlers,
        )?;
        blob.extend_from_slice(&container_bytes);
    }
    Ok((blob, remap))
}

/// Derive insns_size (in 16-bit code units) from the max-extent of
/// any instruction or payload in a CodeItem.
fn compute_insns_size(
    instructions: &[crate::decode::Instruction],
    payloads: &std::collections::BTreeMap<u32, crate::decode::PayloadData>,
) -> Result<u32, DexEmitError> {
    use crate::decode::PayloadData;
    let mut max_end: u32 = 0;
    for insn in instructions {
        let end = insn.addr.saturating_add(u32::from(insn.size));
        if end > max_end {
            max_end = end;
        }
    }
    for (pc, payload) in payloads {
        let sz: u32 = match payload {
            PayloadData::PackedSwitch { targets, .. } => {
                // 1 (ident) + 1 (size) + 2 (first_key) + 2*N (targets)
                4u32.saturating_add(
                    u32::try_from(targets.len())
                        .map_err(|_| DexEmitError::SizeOverflow {
                            layer: "compute_insns_size",
                            context: "packed-switch target count exceeds u32",
                        })?
                        .saturating_mul(2),
                )
            }
            PayloadData::SparseSwitch { keys, .. } => {
                // 1 (ident) + 1 (size) + 2*N (keys) + 2*N (targets)
                2u32.saturating_add(
                    u32::try_from(keys.len())
                        .map_err(|_| DexEmitError::SizeOverflow {
                            layer: "compute_insns_size",
                            context: "sparse-switch key count exceeds u32",
                        })?
                        .saturating_mul(4),
                )
            }
            PayloadData::FillArrayData { data, .. } => {
                // 1 (ident) + 1 (element_width) + 2 (size as u32) + ceil(data_len/2)
                let data_units = u32::try_from(data.len())
                    .map_err(|_| DexEmitError::SizeOverflow {
                        layer: "compute_insns_size",
                        context: "fill-array-data byte length exceeds u32",
                    })?
                    .saturating_add(1)
                    .saturating_div(2);
                4u32.saturating_add(data_units)
            }
        };
        let end = pc.saturating_add(sz);
        if end > max_end {
            max_end = end;
        }
    }
    Ok(max_end)
}

/// Emit a contiguous `class_data_item` data-section blob. Each entry
/// references code_items via `encoded_method.code_off`; the caller
/// provides the `code_item_remap + code_item_base` so this function
/// can rewrite those offsets into the new layout.
pub fn emit_class_data_section(
    class_datas: &std::collections::BTreeMap<u32, crate::decode::ClassData>,
    code_item_remap: &std::collections::BTreeMap<u32, u32>,
    code_item_base: u32,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, cd) in class_datas {
        // class_data_item has no alignment requirement per spec, but
        // we still pack them contiguously (no alignment pad needed).
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "class_data_section",
            context: "class_data blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);

        // Rewrite each method's code_off via the code_item remap.
        let remap_method = |em: &crate::decode::EncodedMethod| -> Result<crate::decode::EncodedMethod, DexEmitError> {
            let new_code_off = if em.code_off == 0 {
                0
            } else {
                let local = *code_item_remap.get(&em.code_off).ok_or(
                    DexEmitError::UnrepresentableIR {
                        why: "encoded_method.code_off references code_item not in DexFile.code_items",
                    },
                )?;
                code_item_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                    section: "class_data.code_off",
                    context: "remapped code_off exceeds u32",
                })?
            };
            Ok(crate::decode::EncodedMethod {
                method_idx: em.method_idx,
                access_flags: em.access_flags,
                code_off: new_code_off,
            })
        };
        let direct: Vec<crate::decode::EncodedMethod> =
            cd.direct_methods.iter().map(remap_method).collect::<Result<_, _>>()?;
        let virtual_: Vec<crate::decode::EncodedMethod> =
            cd.virtual_methods.iter().map(remap_method).collect::<Result<_, _>>()?;

        let cd_bytes =
            emit_class_data_item(&cd.static_fields, &cd.instance_fields, &direct, &virtual_)?;
        blob.extend_from_slice(&cd_bytes);
    }
    Ok((blob, remap))
}

// ── Annotation section family (DEX §7.23–7.30) ──────────────────────
//
// The annotation section has four sub-blobs keyed by offset:
//   1. annotation_item       — leaf: u8 visibility + encoded_annotation
//   2. annotation_set        — u32 size + u32[size] annotation_off
//   3. annotation_set_ref_list — u32 size + u32[size] annotation_set_off
//   4. annotation_directory  — u32 class_annotations_off + 3 count
//                              fields + field/method/parameter entries
//
// Emit order (dependency-first):
//   item → set (references items) → set_ref_list (references sets) →
//   directory (references sets + set_ref_lists)
//
// Each section carries its own remap so downstream references can be
// rewritten to absolute offsets.

// Note: emit_call_site_data_section was removed as part of the
// call_site_ids offset-sharing fix. Call_site encoded_array content
// now lives in dex.encoded_arrays (keyed by offset) so it emits through
// emit_encoded_array_section_with_widths alongside
// class_def static_values. Call_site_id items reference encoded_array
// section offsets directly via encoded_array_remap, preserving DEX
// §3.10 sharing.

/// Emit the `call_site_ids` section (pool region, between class_defs
/// and method_handles per DEX map-list offset-ascending order). Each
/// entry is a 4-byte little-endian u32 absolute offset into the data
/// section's per-call-site encoded_array region (from
/// `emit_call_site_data_section`).
pub fn emit_call_site_id_section(absolute_offsets: &[u32]) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::with_capacity(absolute_offsets.len().saturating_mul(4));
    for &abs in absolute_offsets {
        out.extend_from_slice(&abs.to_le_bytes());
    }
    Ok(out)
}

/// Emit the `method_handles` section. Each entry is an 8-byte fixed
/// record per DEX spec §"method_handle_item":
///   `method_handle_type:u16, unused:u16, field_or_method_id:u16,
///    unused:u16`.
pub fn emit_method_handle_section(
    handles: &[crate::parser::MethodHandleItem],
) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::with_capacity(handles.len().saturating_mul(8));
    for mh in handles {
        out.extend_from_slice(&mh.kind.to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(&mh.field_or_method_id.to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
    }
    Ok(out)
}

pub fn emit_encoded_array_section(
    arrays: &std::collections::BTreeMap<u32, Vec<EncodedValue>>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    emit_encoded_array_section_with_widths(arrays, None)
}

/// Like `emit_encoded_array_section` but optionally preserves per-value
/// input widths. `widths`, when `Some`, is keyed by the same
/// `original_off` as `arrays` (each value carries a `Vec<u8>` parallel
/// to that array's `Vec<EncodedValue>`). Used by `emit_dex_inner` under
/// `EmitConfig::preserve_encoded_value_width = true`.
pub fn emit_encoded_array_section_with_widths(
    arrays: &std::collections::BTreeMap<u32, Vec<EncodedValue>>,
    widths: Option<&std::collections::BTreeMap<u32, Vec<u8>>>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, values) in arrays {
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "encoded_array_section",
            context: "encoded_array blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        let per_array_widths = widths.and_then(|w| w.get(original_off).map(Vec::as_slice));
        blob.extend_from_slice(&emit_encoded_array_with_widths(values, per_array_widths)?);
    }
    Ok((blob, remap))
}

/// Emit a contiguous `debug_info_item` data section from the
/// offset-keyed raw-bytes map populated by the parser's eager
/// `scan_debug_info_bytes` walk. Each entry's raw on-disk bytes are
/// copied verbatim; no re-synthesis of the state-machine bytecode.
/// No alignment required per DEX spec §"debug_info_item" (variable-
/// length byte stream). Returns (blob, remap) where
/// `remap[original_off] = local_offset_within_blob`; the caller adds
/// the section's absolute base offset to resolve
/// `code_item.debug_info_off` references.
///
/// # Security: dangling-pointer vector closed by construction
///
/// `debug_info_off` is zeroed in emit to close a substitution attack:
/// an attacker-crafted input DEX
/// where `code_item.debug_info_off` pointed into a region that was
/// NOT a legitimate debug_info_item in the input byte layout — but
/// WILL be attacker-populated in the emitted output (a string blob,
/// a code_item bytecode region, a class_data entry). Parser accepts
/// the stored offset with no range check against section bounds;
/// passing that value through to emit without writing debug_info
/// bytes yields a dangling pointer into attacker-controlled emitted
/// content. A downstream lazy-reading debug-info consumer that
/// dereferences the stale offset runs the debug-info state-machine
/// over attacker-chosen bytes (DBG_START_LOCAL with attacker-indexed
/// name/type references).
///
/// Option (a) byte-preservation — implemented here — closes the
/// vector by construction. The parser's `scan_debug_info_bytes`
/// captures the exact on-disk byte range of each debug_info_item,
/// walking the state machine through DBG_END_SEQUENCE. The emitter
/// writes those raw bytes into its own data section and rewrites
/// each `code_item.debug_info_off` to point into the EMITTER-
/// controlled, EMITTER-derived, EMITTER-deterministic output region.
/// Post-round-trip, the offset NEVER references attacker-crafted
/// foreign content (strings / code / class_data / etc.) — it
/// references bytes that are byte-copies of the original debug_info
/// section. The substitution vector is closed: emitted bytes ARE
/// the original bytes.
///
/// A separate pre-existing attack surface remains: malicious
/// debug_info state-machine bytecode (DBG_START_LOCAL with crafted
/// StringIdx values) in the input can still drive a downstream
/// debug-info interpreter to dereference attacker-chosen indices —
/// but (a) this is not a new exposure (parser accepts and stores
/// those bytes regardless of byte-preservation), (b) it is bounded
/// by the parsed pool sizes (no out-of-bounds dereference possible
/// against bounded pools), and (c) `parse_debug_info` already
/// tolerates adversarial opcodes (break-on-unknown, checked-add on
/// state-machine accumulators).
pub fn emit_debug_info_section(
    raw_bytes: &std::collections::BTreeMap<u32, Vec<u8>>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, raw) in raw_bytes {
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "debug_info_section",
            context: "debug_info blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        blob.extend_from_slice(raw);
    }
    Ok((blob, remap))
}

/// Emit a single `annotation_item` — u8 visibility + encoded_annotation.
fn emit_annotation_item_bytes(
    item: &crate::annotation::AnnotationItem,
) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::new();
    out.push(item.visibility);
    emit_encoded_annotation(&mut out, &item.annotation)?;
    Ok(out)
}

/// Emit a contiguous `annotation_item` blob. Items have no alignment
/// requirement (variable-length ULEB128 body starts at byte boundary).
pub fn emit_annotation_item_section(
    items: &std::collections::BTreeMap<u32, crate::annotation::AnnotationItem>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, item) in items {
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "annotation_item_section",
            context: "annotation_item blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        blob.extend_from_slice(&emit_annotation_item_bytes(item)?);
    }
    Ok((blob, remap))
}

/// Emit a single `annotation_set_item` — u32 size + u32[size]
/// annotation_off (ascending offsets per spec §7.25). Offsets passed
/// in are already absolute.
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: `absolute_offs.len() as u32` — annotation_set_item.size is u32 per DEX §7.25; the number of annotation_item entries per set is bounded by u32::MAX by spec. Narrowing is exact for any spec-compliant DEX."
)]
fn emit_annotation_set_bytes(absolute_offs: &[u32]) -> Vec<u8> {
    let mut out = Vec::with_capacity(alloc_cap(
        4usize.saturating_add(absolute_offs.len().saturating_mul(4)),
    ));
    out.extend_from_slice(&(absolute_offs.len() as u32).to_le_bytes());
    for off in absolute_offs {
        out.extend_from_slice(&off.to_le_bytes());
    }
    out
}

/// Emit an annotation_set_item section. Each entry is 4-byte aligned
/// (natural alignment since size is u32).
pub fn emit_annotation_set_section(
    sets: &std::collections::BTreeMap<u32, Vec<u32>>,
    item_base: u32,
    item_remap: &std::collections::BTreeMap<u32, u32>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, entries) in sets {
        while !blob.len().is_multiple_of(4) {
            blob.push(0);
        }
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "annotation_set_section",
            context: "annotation_set blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        let absolute: Vec<u32> = entries
            .iter()
            .map(|&item_off| {
                if item_off == 0 {
                    Ok(0u32)
                } else {
                    let local = *item_remap.get(&item_off).ok_or(
                        DexEmitError::UnrepresentableIR {
                            why: "annotation_set references annotation_item not in DexFile.annotation_items",
                        },
                    )?;
                    item_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_set.annotation_off",
                        context: "remapped annotation_item offset exceeds u32",
                    })
                }
            })
            .collect::<Result<Vec<_>, DexEmitError>>()?;
        blob.extend_from_slice(&emit_annotation_set_bytes(&absolute));
    }
    Ok((blob, remap))
}

/// Emit an annotation_set_ref_list section. Same on-disk layout as
/// annotation_set but each entry points to an annotation_set rather
/// than an annotation_item.
pub fn emit_annotation_set_ref_list_section(
    ref_lists: &std::collections::BTreeMap<u32, Vec<u32>>,
    set_base: u32,
    set_remap: &std::collections::BTreeMap<u32, u32>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, entries) in ref_lists {
        while !blob.len().is_multiple_of(4) {
            blob.push(0);
        }
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "annotation_set_ref_list_section",
            context: "annotation_set_ref_list blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);
        let absolute: Vec<u32> = entries
            .iter()
            .map(|&set_off| {
                if set_off == 0 {
                    Ok(0u32)
                } else {
                    let local = *set_remap.get(&set_off).ok_or(
                        DexEmitError::UnrepresentableIR {
                            why: "annotation_set_ref_list references annotation_set not in DexFile.annotation_sets",
                        },
                    )?;
                    set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
                        section: "annotation_set_ref_list.annotations_off",
                        context: "remapped annotation_set offset exceeds u32",
                    })
                }
            })
            .collect::<Result<Vec<_>, DexEmitError>>()?;
        blob.extend_from_slice(&emit_annotation_set_bytes(&absolute));
    }
    Ok((blob, remap))
}

/// Emit a single `annotation_directory_item`. Sizes are followed by
/// field/method/parameter arrays, each 8 bytes (u32 idx + u32 off).
fn emit_annotation_directory_bytes(
    dir: &crate::annotation::AnnotationDirectoryItem,
    class_ann_off_abs: u32,
    field_offs_abs: &[u32],
    method_offs_abs: &[u32],
    param_offs_abs: &[u32],
) -> Result<Vec<u8>, DexEmitError> {
    let total = 16usize
        .saturating_add(dir.fields.len().saturating_mul(8))
        .saturating_add(dir.methods.len().saturating_mul(8))
        .saturating_add(dir.parameters.len().saturating_mul(8));
    let mut out = Vec::with_capacity(alloc_cap(total));
    out.extend_from_slice(&class_ann_off_abs.to_le_bytes());
    out.extend_from_slice(&(u32::try_from(dir.fields.len()).map_err(|_| {
        DexEmitError::SizeOverflow {
            layer: "annotation_directory",
            context: "fields_size exceeds u32",
        }
    })?).to_le_bytes());
    out.extend_from_slice(&(u32::try_from(dir.methods.len()).map_err(|_| {
        DexEmitError::SizeOverflow {
            layer: "annotation_directory",
            context: "methods_size exceeds u32",
        }
    })?).to_le_bytes());
    out.extend_from_slice(&(u32::try_from(dir.parameters.len()).map_err(|_| {
        DexEmitError::SizeOverflow {
            layer: "annotation_directory",
            context: "parameters_size exceeds u32",
        }
    })?).to_le_bytes());
    for (f, abs) in dir.fields.iter().zip(field_offs_abs.iter()) {
        out.extend_from_slice(&f.field_idx.0.to_le_bytes());
        out.extend_from_slice(&abs.to_le_bytes());
    }
    for (m, abs) in dir.methods.iter().zip(method_offs_abs.iter()) {
        out.extend_from_slice(&m.method_idx.0.to_le_bytes());
        out.extend_from_slice(&abs.to_le_bytes());
    }
    for (p, abs) in dir.parameters.iter().zip(param_offs_abs.iter()) {
        out.extend_from_slice(&p.method_idx.0.to_le_bytes());
        out.extend_from_slice(&abs.to_le_bytes());
    }
    Ok(out)
}

/// Emit the annotation_directory_item section. Each directory is
/// 4-byte aligned.
pub fn emit_annotation_directory_section(
    directories: &std::collections::BTreeMap<
        u32,
        crate::annotation::AnnotationDirectoryItem,
    >,
    set_base: u32,
    set_remap: &std::collections::BTreeMap<u32, u32>,
    ref_list_base: u32,
    ref_list_remap: &std::collections::BTreeMap<u32, u32>,
) -> Result<(Vec<u8>, std::collections::BTreeMap<u32, u32>), DexEmitError> {
    let resolve_set = |off: u32| -> Result<u32, DexEmitError> {
        if off == 0 {
            return Ok(0);
        }
        let local =
            *set_remap
                .get(&off)
                .ok_or(DexEmitError::UnrepresentableIR {
                    why: "annotation_directory references annotation_set not in DexFile.annotation_sets",
                })?;
        set_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
            section: "annotation_directory.annotations_off",
            context: "remapped annotation_set offset exceeds u32",
        })
    };
    let resolve_ref_list = |off: u32| -> Result<u32, DexEmitError> {
        if off == 0 {
            return Ok(0);
        }
        let local = *ref_list_remap.get(&off).ok_or(
            DexEmitError::UnrepresentableIR {
                why: "annotation_directory parameters_off references annotation_set_ref_list not in DexFile.annotation_set_ref_lists",
            },
        )?;
        ref_list_base.checked_add(local).ok_or(DexEmitError::OffsetOverflow {
            section: "annotation_directory.parameters_off",
            context: "remapped annotation_set_ref_list offset exceeds u32",
        })
    };

    let mut blob = Vec::new();
    let mut remap = std::collections::BTreeMap::new();
    for (original_off, dir) in directories {
        while !blob.len().is_multiple_of(4) {
            blob.push(0);
        }
        let local = u32::try_from(blob.len()).map_err(|_| DexEmitError::OffsetOverflow {
            section: "annotation_directory_section",
            context: "annotation_directory blob exceeds u32 byte offset",
        })?;
        remap.insert(*original_off, local);

        let class_abs = resolve_set(dir.class_annotations_off)?;
        let field_abs: Vec<u32> = dir
            .fields
            .iter()
            .map(|f| resolve_set(f.annotations_off))
            .collect::<Result<_, _>>()?;
        let method_abs: Vec<u32> = dir
            .methods
            .iter()
            .map(|m| resolve_set(m.annotations_off))
            .collect::<Result<_, _>>()?;
        let param_abs: Vec<u32> = dir
            .parameters
            .iter()
            .map(|p| resolve_ref_list(p.annotations_off))
            .collect::<Result<_, _>>()?;
        blob.extend_from_slice(&emit_annotation_directory_bytes(
            dir,
            class_abs,
            &field_abs,
            &method_abs,
            &param_abs,
        )?);
    }
    Ok((blob, remap))
}

/// Serialize the proto pool into the `proto_id_item[]` byte blob (DEX §7.7).
///
/// Each `proto_id_item` is 12 bytes:
/// ```text
/// struct proto_id_item {
///     uint shorty_idx;       // u32 LE, index into string_ids
///     uint return_type_idx;  // u32 LE, index into type_ids
///     uint parameters_off;   // u32 LE, offset to type_list, or 0 if no params
/// }
/// ```
///
/// **Caller-side invariant.** DEX §7.7 requires protos sorted first by
/// `return_type_idx`, then lexicographically by argument-list
/// `type_idx` sequence. The argument list lives behind
/// `parameters_off` as a separate `type_list` — not directly in
/// `ProtoIdItem` — so a pure `NonDecreasing<ProtoIdItem>` by struct
/// `Ord` cannot express the full invariant. The assembly layer that
/// calls this function is responsible for:
///   1. Emitting `type_list`s in an order such that
///      `parameters_off(a) < parameters_off(b)` iff `a`'s argument list
///      is lexicographically less than `b`'s, AND
///   2. Sorting the `ProtoIdItem` slice by `(return_type_idx,
///      parameters_off)` — which then matches the §7.7 ordering.
///
/// This function emits whatever order the caller provides. A future
/// retrofit could introduce an `EnrichedProtoIdItem { item, argument_list }`
/// type wrapped in `NonDecreasing` to fix the gauge at the type level —
/// tracked as follow-up if the caller-side sort becomes error-prone.
pub fn emit_proto_pool(protos: &[ProtoIdItem]) -> Vec<u8> {
    let mut out = Vec::with_capacity(alloc_cap(protos.len().saturating_mul(12)));
    for p in protos {
        out.extend_from_slice(&p.shorty_idx.0.to_le_bytes());
        out.extend_from_slice(&p.return_type_idx.0.to_le_bytes());
        out.extend_from_slice(&p.parameters_off.to_le_bytes());
    }
    out
}

// ── Field pool emit (DEX §7.8 field_id_item) ────────────────────────

/// Serialize the field pool into the `field_id_item[]` byte blob
/// (DEX §7.8). Each entry is 8 bytes:
///
/// ```text
/// struct field_id_item {
///     ushort class_idx;      // u16 LE, type_idx of defining class
///     ushort type_idx;       // u16 LE, type_idx of field's type
///     uint   name_idx;       // u32 LE, string_idx of field name
/// }
/// ```
///
/// **Caller-side invariant.** DEX §7.8 requires field_ids sorted by
/// `(class_idx, name_idx, type_idx)` — lexicographic on the three
/// fields in that order. The `FieldIdItem` struct's natural `Ord`
/// would sort `(class_idx, type_idx, name_idx)` (the struct field
/// order). The assembly layer that calls this function is
/// responsible for pre-sorting the slice using the
/// `(class_idx, name_idx, type_idx)` key.
///
/// Returns `UnrepresentableIR` when `class_idx` or `type_idx` exceeds
/// `u16::MAX` — the DEX `field_id_item` format can only address the
/// first 65,536 types.
pub fn emit_field_pool(fields: &[FieldIdItem]) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::with_capacity(alloc_cap(fields.len().saturating_mul(8)));
    for f in fields {
        let class_u16: u16 = f.class_idx.0.try_into().map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "field_id_item.class_idx exceeds u16 type_id space (DEX §7.8)",
            }
        })?;
        let type_u16: u16 = f.type_idx.0.try_into().map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "field_id_item.type_idx exceeds u16 type_id space (DEX §7.8)",
            }
        })?;
        out.extend_from_slice(&class_u16.to_le_bytes());
        out.extend_from_slice(&type_u16.to_le_bytes());
        out.extend_from_slice(&f.name_idx.0.to_le_bytes());
    }
    Ok(out)
}

// ── Method pool emit (DEX §7.9 method_id_item) ──────────────────────

/// Serialize the method pool into the `method_id_item[]` byte blob
/// (DEX §7.9). Each entry is 8 bytes:
///
/// ```text
/// struct method_id_item {
///     ushort class_idx;      // u16 LE, type_idx of defining class
///     ushort proto_idx;      // u16 LE, proto_idx of method signature
///     uint   name_idx;       // u32 LE, string_idx of method name
/// }
/// ```
///
/// **Caller-side invariant.** DEX §7.9 requires method_ids sorted by
/// `(class_idx, name_idx, proto_idx)` — same shape as field_ids but
/// with `proto_idx` replacing `type_idx`. The assembly layer pre-sorts
/// with that key before handoff.
///
/// Returns `UnrepresentableIR` when `class_idx` or `proto_idx` exceeds
/// `u16::MAX` — the DEX `method_id_item` format can only address the
/// first 65,536 types / protos.
pub fn emit_method_pool(methods: &[MethodIdItem]) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::with_capacity(alloc_cap(methods.len().saturating_mul(8)));
    for m in methods {
        let class_u16: u16 = m.class_idx.0.try_into().map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "method_id_item.class_idx exceeds u16 type_id space (DEX §7.9)",
            }
        })?;
        let proto_u16: u16 = m.proto_idx.0.try_into().map_err(|_| {
            DexEmitError::UnrepresentableIR {
                why: "method_id_item.proto_idx exceeds u16 proto_id space (DEX §7.9)",
            }
        })?;
        out.extend_from_slice(&class_u16.to_le_bytes());
        out.extend_from_slice(&proto_u16.to_le_bytes());
        out.extend_from_slice(&m.name_idx.0.to_le_bytes());
    }
    Ok(out)
}

// ── encoded_value emit (DEX §7.11) ──────────────────────────────────
//
// Every encoded_value is 1 header byte + variable payload:
//   header.low_5  = value_type
//   header.high_3 = value_arg (size-1 for variable-width types;
//                              bool-value for VALUE_BOOLEAN; unused
//                              for ARRAY / ANNOTATION / NULL)

// Value type codes (DEX spec §7.11).
const VT_BYTE:        u8 = 0x00;
const VT_SHORT:       u8 = 0x02;
const VT_CHAR:        u8 = 0x03;
const VT_INT:         u8 = 0x04;
const VT_LONG:        u8 = 0x06;
const VT_FLOAT:       u8 = 0x10;
const VT_DOUBLE:      u8 = 0x11;
const VT_STRING:      u8 = 0x17;
const VT_TYPE:        u8 = 0x18;
const VT_FIELD:       u8 = 0x19;
const VT_METHOD:      u8 = 0x1a;
const VT_METHOD_TYPE: u8 = 0x15;
const VT_METHOD_HANDLE: u8 = 0x16;
const VT_ENUM:        u8 = 0x1b;
const VT_ARRAY:       u8 = 0x1c;
const VT_ANNOTATION:  u8 = 0x1d;
const VT_NULL:        u8 = 0x1e;
const VT_BOOLEAN:     u8 = 0x1f;

/// Minimum number of bytes to represent `val` with sign-extension to i64.
/// Returns a value in `1..=8`.
#[allow(
    clippy::as_conversions,
    reason = "PROOF: `bytes.clamp(1, 8) as usize` — clamp guarantees the result is in [1, 8] ⊆ usize::MAX; widening from u32 to usize is lossless on all supported platforms (32-bit and 64-bit)."
)]
fn min_signed_bytes(val: i64) -> usize {
    if val == 0 || val == -1 {
        return 1;
    }
    // Number of significant magnitude bits, plus one for the sign.
    let abs_bits: u32 = if val >= 0 {
        64_u32.saturating_sub(val.leading_zeros())
    } else {
        64_u32.saturating_sub((!val).leading_zeros())
    };
    let needed_bits = abs_bits.saturating_add(1);
    // Ceiling-divide by 8, clamped to 1..=8.
    let bytes = needed_bits.div_ceil(8);
    bytes.clamp(1, 8) as usize
}

/// Minimum number of bytes to represent `val` with zero-extension to u64.
/// Returns a value in `1..=8`.
#[allow(
    clippy::as_conversions,
    reason = "PROOF: `bytes.clamp(1, 8) as usize` — clamp guarantees the result is in [1, 8] ⊆ usize::MAX; widening from u32 to usize is lossless on all supported platforms (32-bit and 64-bit)."
)]
fn min_unsigned_bytes(val: u64) -> usize {
    if val == 0 {
        return 1;
    }
    let bits = 64_u32.saturating_sub(val.leading_zeros());
    let bytes = bits.div_ceil(8);
    bytes.clamp(1, 8) as usize
}

/// Append the low `size` bytes of `val` (LE) to `out`.
fn write_le_bytes(out: &mut Vec<u8>, val: u64, size: usize) {
    let all = val.to_le_bytes();
    out.extend_from_slice(&all[..size.min(8)]);
}

/// Emit a width-variable `encoded_value` at a requested width instead
/// of min-width. Used under `EmitConfig::preserve_encoded_value_width
/// = true` for byte-identity roundtrips that retain the input's
/// on-disk encoded_value byte widths.
///
/// Returns `true` when emitted at `requested_width`. Returns `false`
/// (caller MUST fall back to `emit_encoded_value` / min-width path) when:
/// - `val` is variable-shape (Array/Annotation) or fixed-width
///   (Byte/Boolean/Null/Float/Double) — preservation N/A.
/// - `requested_width` is outside the spec-allowed range for the
///   value's type (defends against malformed parser-stored widths
///   that would yield spec-invalid output).
/// - `requested_width` is too narrow to represent the IR value
///   (value-fits-defense; defends against IR mutation after parse).
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: (1) `requested_width - 1` is in 0..=7 (early-bail above ensures requested_width in 1..=8); narrowing usize→u8 in the header byte composition is exact. (2) `min_signed_bytes` / `min_unsigned_bytes` return 1..=8; usize→u8 exact. (3) `*s as u64` / `*i as u64` / `*l as u64` for Short/Int/Long: bit-reinterpretation for write_le_bytes, INTENT (same as emit_encoded_value's existing PROOF)."
)]
fn emit_encoded_value_at_width(
    out: &mut Vec<u8>,
    val: &EncodedValue,
    requested_width: u8,
) -> bool {
    if !(1..=8).contains(&requested_width) {
        return false;
    }
    // VT_FLOAT and VT_DOUBLE handled separately: DEX spec §VII.1 trims
    // TRAILING bytes from the LE representation (reader zero-pads the
    // HIGH-LE side). Trim is safe iff bytes [requested_width..] of the
    // LE rep are all zero — otherwise the trim would lose precision.
    match val {
        EncodedValue::Float(f) => {
            if requested_width > 4 {
                return false;
            }
            let le = f.to_le_bytes();
            // Trim safety: bytes after requested_width must be zero
            // (reader will zero-pad them anyway). High-order LE bytes
            // are the trimmable end.
            let trim_end = (requested_width as usize).min(4);
            // For f, the on-disk format trims from the BACK of the LE
            // representation. Per parser convention (parse_encoded_value
            // for VT_FLOAT/DOUBLE), the input bytes occupy LE[4-N..4]
            // and the LOW-LE bytes are zero-padded. Mirror by checking
            // LE[0..4-N] are all zero, then write LE[4-N..4].
            let pad_count = 4_usize.saturating_sub(trim_end);
            if le[..pad_count].iter().any(|&b| b != 0) {
                return false;
            }
            out.push(VT_FLOAT | (requested_width.saturating_sub(1) << 5));
            out.extend_from_slice(&le[pad_count..]);
            return true;
        }
        EncodedValue::Double(d) => {
            if requested_width > 8 {
                return false;
            }
            let le = d.to_le_bytes();
            let trim_end = (requested_width as usize).min(8);
            let pad_count = 8_usize.saturating_sub(trim_end);
            if le[..pad_count].iter().any(|&b| b != 0) {
                return false;
            }
            out.push(VT_DOUBLE | (requested_width.saturating_sub(1) << 5));
            out.extend_from_slice(&le[pad_count..]);
            return true;
        }
        _ => {}
    }
    // Per-type spec-allowed max width (DEX §VII.1).
    let (vt_tag, raw, min_width, max_width): (u8, u64, u8, u8) = match val {
        EncodedValue::Short(s) => (VT_SHORT, *s as u64, min_signed_bytes(i64::from(*s)) as u8, 2),
        EncodedValue::Char(c) => (VT_CHAR, u64::from(*c), min_unsigned_bytes(u64::from(*c)) as u8, 2),
        EncodedValue::Int(i) => (VT_INT, *i as u64, min_signed_bytes(i64::from(*i)) as u8, 4),
        EncodedValue::Long(l) => (VT_LONG, *l as u64, min_signed_bytes(*l) as u8, 8),
        EncodedValue::String(s) => (VT_STRING, u64::from(s.0), min_unsigned_bytes(u64::from(s.0)) as u8, 4),
        EncodedValue::Type(t) => (VT_TYPE, u64::from(t.0), min_unsigned_bytes(u64::from(t.0)) as u8, 4),
        EncodedValue::Field(f) => (VT_FIELD, u64::from(f.0), min_unsigned_bytes(u64::from(f.0)) as u8, 4),
        EncodedValue::Method(m) => (VT_METHOD, u64::from(m.0), min_unsigned_bytes(u64::from(m.0)) as u8, 4),
        EncodedValue::Enum(f) => (VT_ENUM, u64::from(f.0), min_unsigned_bytes(u64::from(f.0)) as u8, 4),
        EncodedValue::MethodType(p) => (VT_METHOD_TYPE, u64::from(p.0), min_unsigned_bytes(u64::from(p.0)) as u8, 4),
        EncodedValue::MethodHandle(h) => (VT_METHOD_HANDLE, u64::from(h.0), min_unsigned_bytes(u64::from(h.0)) as u8, 4),
        // Width preservation N/A: fixed-width or variable-shape.
        EncodedValue::Byte(_)
        | EncodedValue::Boolean(_)
        | EncodedValue::Null
        | EncodedValue::Array(_)
        | EncodedValue::Annotation(_) => return false,
        // Float/Double handled in the early-match above; this arm is
        // structurally unreachable but kept for enum exhaustiveness.
        // debug_assert surfaces logic bugs loudly in dev; production
        // returns false rather than panic on adversarial input.
        EncodedValue::Float(_) | EncodedValue::Double(_) => {
            debug_assert!(false, "Float/Double should have early-returned above");
            return false;
        }
    };
    if requested_width > max_width || requested_width < min_width {
        return false;
    }
    // requested_width is in 1..=8 (early-bail above); saturating_sub
    // protects the lint without changing semantics (≥1 guarantees no
    // underflow). usize::from is lossless on u8.
    out.push(vt_tag | (requested_width.saturating_sub(1) << 5));
    write_le_bytes(out, raw, usize::from(requested_width));
    true
}

/// Serialize a single `EncodedValue` per DEX §7.11. Recursive for
/// `Array` and `Annotation`.
// PROOF: arithmetic in the size-narrowing helpers uses checked /
// saturating ops. `size - 1` below ranges over 0..=7 (return of
// min_*_bytes) so the header-byte composition `(size - 1) << 5`
// cannot overflow u8.
//
// Emit work-stack size cap (mirrors the parse-side cap).
// Each `EmitTask` holds a reference — pointer-size ≈ 16 bytes.
// 65 536 tasks × 16 bytes ≈ 1 MiB; well within a 4 MiB budget.
const EMIT_STACK_CAP: usize = 65_536;

/// A single task on the iterative emit work-stack.
///
/// Tasks are pushed in reverse emission order (last-to-emit pushed
/// first); popping from the back of the Vec processes them in the
/// correct serialization order.
enum EmitTask<'a> {
    /// Emit a single `EncodedValue` (header + payload).
    Value(&'a EncodedValue),
    /// Emit an `EncodedAnnotation` body (type_idx ULEB + size ULEB +
    /// elements). The VT_ANNOTATION header byte is NOT included —
    /// callers push that separately via `RawByte` before this task.
    AnnBody(&'a EncodedAnnotation),
    /// Write a single pre-computed ULEB128 value.
    Uleb128(u32),
}

/// Iterative `encoded_value` emitter.
///
/// Serializes `val` into `out` using an explicit heap-allocated work-
/// stack.  No recursive call sites; no stack-depth limit.
///
/// Heap-stack overflow (> `EMIT_STACK_CAP` pending tasks) returns
/// `DexEmitError::UnrepresentableIR` — this signals adversarially
/// deep hand-built IR, not a data-integrity failure.
#[allow(clippy::arithmetic_side_effects, reason = "PROOF: min_{signed,unsigned}_bytes returns a value in 1..=8 by construction; subsequent bit-shift / offset arithmetic on parsed/in-memory encoded_value tags is bounded by the value-domain enum.")]
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF / INTENT: (1) `((size - 1) as u8)` — size ∈ [1,8] from min_{signed,unsigned}_bytes, so size-1 ∈ [0,7], narrowing to u8 exact. (2) `*b as u8` for i8 → u8: two's-complement byte extraction, INTENT. (3) `*s as u64` / `*i as u64` / `*l as u64` for Short/Int/Long: bit-reinterpretation for write_le_bytes, INTENT. (4) `(4 - 1) as u8` / `(8 - 1) as u8`: compile-time constants 3 and 7, exact."
)]
// return 1..=8 by construction; `size - 1` is 0..=7, no underflow.
pub fn emit_encoded_value(out: &mut Vec<u8>, val: &EncodedValue) -> Result<(), DexEmitError> {
    let mut stack: Vec<EmitTask<'_>> = Vec::new();
    stack.push(EmitTask::Value(val));

    while let Some(task) = stack.pop() {
        match task {
            EmitTask::Uleb128(v) => {
                write_uleb128(out, v);
            }
            EmitTask::AnnBody(ann) => {
                // Emit annotation body: type_idx ULEB, size ULEB, then
                // elements in order (BTreeMap is sorted by key ascending).
                write_uleb128(out, ann.type_idx.0);
                write_uleb128(
                    out,
                    u32::try_from(ann.elements.len()).map_err(|_| DexEmitError::SizeOverflow {
                        layer: "encoded_annotation",
                        context: "element count exceeds u32",
                    })?,
                );
                // Push elements in reverse order so they pop in forward order.
                for (name, elem_val) in ann.elements.iter().rev() {
                    if stack.len().saturating_add(2) > EMIT_STACK_CAP {
                        return Err(DexEmitError::UnrepresentableIR {
                            why: "encoded_annotation emit work-stack exceeded cap (hand-built IR too deeply nested)",
                        });
                    }
                    stack.push(EmitTask::Value(elem_val));
                    stack.push(EmitTask::Uleb128(name.0));
                }
            }
            EmitTask::Value(v) => {
                match v {
                    EncodedValue::Byte(b) => {
                        // VALUE_BYTE: value_arg MUST be 0. Payload is always 1 byte.
                        out.push(VT_BYTE);
                        out.push(*b as u8);
                    }
                    EncodedValue::Short(s) => {
                        let size = min_signed_bytes(i64::from(*s));
                        out.push(VT_SHORT | (((size - 1) as u8) << 5));
                        write_le_bytes(out, *s as u64, size);
                    }
                    EncodedValue::Char(c) => {
                        let size = min_unsigned_bytes(u64::from(*c));
                        out.push(VT_CHAR | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(*c), size);
                    }
                    EncodedValue::Int(i) => {
                        let size = min_signed_bytes(i64::from(*i));
                        out.push(VT_INT | (((size - 1) as u8) << 5));
                        write_le_bytes(out, *i as u64, size);
                    }
                    EncodedValue::Long(l) => {
                        let size = min_signed_bytes(*l);
                        out.push(VT_LONG | (((size - 1) as u8) << 5));
                        write_le_bytes(out, *l as u64, size);
                    }
                    EncodedValue::Float(f) => {
                        // DEX spec: float/double values are right-zero-padded.
                        // Always write full width — trimming trailing-zero
                        // bytes is optional; always-full is correct and simpler.
                        out.push(VT_FLOAT | (((4 - 1) as u8) << 5));
                        out.extend_from_slice(&f.to_le_bytes());
                    }
                    EncodedValue::Double(d) => {
                        out.push(VT_DOUBLE | (((8 - 1) as u8) << 5));
                        out.extend_from_slice(&d.to_le_bytes());
                    }
                    EncodedValue::String(s) => {
                        let size = min_unsigned_bytes(u64::from(s.0));
                        out.push(VT_STRING | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(s.0), size);
                    }
                    EncodedValue::Type(t) => {
                        let size = min_unsigned_bytes(u64::from(t.0));
                        out.push(VT_TYPE | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(t.0), size);
                    }
                    EncodedValue::Field(f) => {
                        let size = min_unsigned_bytes(u64::from(f.0));
                        out.push(VT_FIELD | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(f.0), size);
                    }
                    EncodedValue::Method(m) => {
                        let size = min_unsigned_bytes(u64::from(m.0));
                        out.push(VT_METHOD | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(m.0), size);
                    }
                    EncodedValue::Enum(f) => {
                        let size = min_unsigned_bytes(u64::from(f.0));
                        out.push(VT_ENUM | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(f.0), size);
                    }
                    EncodedValue::Array(items) => {
                        // Emit header + ULEB count now; push children in
                        // reverse order so they pop in forward order.
                        out.push(VT_ARRAY);
                        write_uleb128(out, u32::try_from(items.len()).map_err(|_| {
                            DexEmitError::SizeOverflow {
                                layer: "encoded_array",
                                context: "length exceeds u32",
                            }
                        })?);
                        for item in items.iter().rev() {
                            if stack.len() >= EMIT_STACK_CAP {
                                return Err(DexEmitError::UnrepresentableIR {
                                    why: "encoded_array emit work-stack exceeded cap (hand-built IR too deeply nested)",
                                });
                            }
                            stack.push(EmitTask::Value(item));
                        }
                    }
                    EncodedValue::Annotation(ann) => {
                        // Emit VT_ANNOTATION header byte now; push body task.
                        out.push(VT_ANNOTATION);
                        if stack.len() >= EMIT_STACK_CAP {
                            return Err(DexEmitError::UnrepresentableIR {
                                why: "encoded_annotation emit work-stack exceeded cap (hand-built IR too deeply nested)",
                            });
                        }
                        stack.push(EmitTask::AnnBody(ann));
                    }
                    EncodedValue::Null => {
                        out.push(VT_NULL);
                    }
                    EncodedValue::Boolean(b) => {
                        // VALUE_BOOLEAN: value_arg bit 0 = the bool value.
                        out.push(VT_BOOLEAN | (u8::from(*b) << 5));
                    }
                    EncodedValue::MethodType(p) => {
                        let size = min_unsigned_bytes(u64::from(p.0));
                        out.push(VT_METHOD_TYPE | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(p.0), size);
                    }
                    EncodedValue::MethodHandle(h) => {
                        let size = min_unsigned_bytes(u64::from(h.0));
                        out.push(VT_METHOD_HANDLE | (((size - 1) as u8) << 5));
                        write_le_bytes(out, u64::from(h.0), size);
                    }
                }
            }
        }
    }
    Ok(())
}

/// Serialize an `encoded_annotation` (DEX §7.11 item-body, shared by
/// VALUE_ANNOTATION and annotations-directory). The header byte
/// (VT_ANNOTATION) is written by the *caller*; this function emits
/// only the body.
///
/// Body: ULEB(type_idx) || ULEB(size) || size × (ULEB(name_idx) ||
///       encoded_value).
///
/// Iterative: no recursive call sites.  Calls `emit_encoded_value`
/// (also iterative) for each element value.
#[allow(clippy::arithmetic_side_effects, reason = "PROOF: same domain as emit_encoded_value — operates on parser-validated encoded_value tags whose value-domain is enum-bounded.")]
pub fn emit_encoded_annotation(
    out: &mut Vec<u8>,
    ann: &EncodedAnnotation,
) -> Result<(), DexEmitError> {
    write_uleb128(out, ann.type_idx.0);
    write_uleb128(
        out,
        u32::try_from(ann.elements.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "encoded_annotation",
            context: "element count exceeds u32",
        })?,
    );
    // BTreeMap iteration is already in ascending-by-key order, which is
    // the canonical order DEX expects for annotation_element arrays
    // (sorted by string_idx of name).
    for (name, val) in &ann.elements {
        write_uleb128(out, name.0);
        emit_encoded_value(out, val)?;
    }
    Ok(())
}

/// Serialize an `encoded_array_item` (DEX §7.12). Shape:
///   ULEB(size) || size × encoded_value.
///
/// Used for static-field initial values (`ClassDefItem.static_values_off`).
pub fn emit_encoded_array(values: &[EncodedValue]) -> Result<Vec<u8>, DexEmitError> {
    emit_encoded_array_with_widths(values, None)
}

/// Like `emit_encoded_array` but optionally preserves per-value input
/// widths. `widths`, when `Some`, is a DEEP pre-order width sequence
/// covering every width-bearing primitive descendant (see
/// [`crate::annotation::collect_encoded_array_widths_pre_order`]). Walks
/// the IR tree in pre-order and consumes one width per width-bearing
/// primitive — composites (Array / Annotation) and zero-payload
/// primitives (Null / Boolean) consume nothing, keeping pop/push in
/// lock-step with the parser-side collector.
///
/// For each width-bearing primitive: tries `emit_encoded_value_at_width`
/// at the consumed width; on failure (e.g. value mutated post-parse so
/// requested width is now too narrow), falls back to the min-width path.
/// Used by `emit_encoded_array_section` under
/// `EmitConfig::preserve_encoded_value_width = true`.
pub fn emit_encoded_array_with_widths(
    values: &[EncodedValue],
    widths: Option<&[u8]>,
) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::new();
    write_uleb128(
        &mut out,
        u32::try_from(values.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "encoded_array_item",
            context: "array length exceeds u32",
        })?,
    );
    match widths {
        None => {
            for v in values {
                emit_encoded_value(&mut out, v)?;
            }
        }
        Some(ws) => {
            let mut iter = ws.iter();
            for v in values {
                emit_encoded_value_deep_widths(&mut out, v, &mut iter)?;
            }
        }
    }
    Ok(out)
}

/// Like `emit_encoded_annotation` but preserves per-value DEEP widths.
/// `widths` is a pre-order width sequence covering every width-bearing
/// primitive descendant inside this annotation's elements. The body
/// shape (type_idx ULEB || size ULEB || element pairs) is identical to
/// `emit_encoded_annotation`. Caller writes the `0x1d` VT_ANNOTATION
/// header byte separately when emitting as a free-standing value.
pub fn emit_encoded_annotation_with_widths(
    out: &mut Vec<u8>,
    ann: &EncodedAnnotation,
    widths: &[u8],
) -> Result<(), DexEmitError> {
    let mut iter = widths.iter();
    emit_encoded_annotation_body_with_iter(out, ann, &mut iter)
}

fn emit_encoded_annotation_body_with_iter<'a>(
    out: &mut Vec<u8>,
    ann: &EncodedAnnotation,
    iter: &mut std::slice::Iter<'a, u8>,
) -> Result<(), DexEmitError> {
    write_uleb128(out, ann.type_idx.0);
    write_uleb128(
        out,
        u32::try_from(ann.elements.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "encoded_annotation",
            context: "element count exceeds u32",
        })?,
    );
    for (name, val) in &ann.elements {
        write_uleb128(out, name.0);
        emit_encoded_value_deep_widths(out, val, iter)?;
    }
    Ok(())
}

/// Serialize an `annotation_item` (DEX §7.13: `u8 visibility ||
/// encoded_annotation body`) with deep per-value width preservation.
/// `widths` is the deep pre-order width sequence from
/// [`crate::annotation::collect_annotation_item_widths`].
pub fn emit_annotation_item_with_widths(
    out: &mut Vec<u8>,
    item: &crate::annotation::AnnotationItem,
    widths: &[u8],
) -> Result<(), DexEmitError> {
    out.push(item.visibility);
    emit_encoded_annotation_with_widths(out, &item.annotation, widths)
}

/// Walk one EncodedValue in pre-order, consuming widths in lock-step
/// with the parser-side collector. Each width-bearing primitive
/// consumes exactly one width from `iter`; composites consume zero (but
/// recurse into children); Null / Boolean consume zero.
fn emit_encoded_value_deep_widths<'a>(
    out: &mut Vec<u8>,
    val: &EncodedValue,
    iter: &mut std::slice::Iter<'a, u8>,
) -> Result<(), DexEmitError> {
    match val {
        EncodedValue::Array(items) => {
            out.push(VT_ARRAY);
            write_uleb128(
                out,
                u32::try_from(items.len()).map_err(|_| DexEmitError::SizeOverflow {
                    layer: "encoded_array",
                    context: "length exceeds u32",
                })?,
            );
            for item in items {
                emit_encoded_value_deep_widths(out, item, iter)?;
            }
            Ok(())
        }
        EncodedValue::Annotation(ann) => {
            out.push(VT_ANNOTATION);
            emit_encoded_annotation_body_with_iter(out, ann, iter)
        }
        EncodedValue::Null | EncodedValue::Boolean(_) => {
            // No width to consume; fall through to min-width emit which
            // writes just the header byte for these variants.
            emit_encoded_value(out, val)
        }
        _ => {
            // Width-bearing primitive: consume one width and try
            // preserved emit; on failure fall back to min-width.
            let preserved = iter.next().copied();
            if let Some(w) = preserved {
                if emit_encoded_value_at_width(out, val, w) {
                    return Ok(());
                }
            }
            emit_encoded_value(out, val)
        }
    }
}

// ── Class-def pool emit (DEX §7.10 class_def_item) ──────────────────

/// Serialize the class-def pool into the `class_def_item[]` byte blob
/// (DEX §7.10). Each entry is 32 bytes — all u32 LE:
///
/// ```text
/// struct class_def_item {
///     uint class_idx;          // type_idx of this class
///     uint access_flags;       // DexAccessFlags (class/interface/abstract/...)
///     uint superclass_idx;     // type_idx of superclass, or NO_INDEX
///     uint interfaces_off;     // offset to type_list of interfaces, or 0
///     uint source_file_idx;    // string_idx of source filename, or NO_INDEX
///     uint annotations_off;    // offset to annotations_directory_item, or 0
///     uint class_data_off;     // offset to class_data_item, or 0
///     uint static_values_off;  // offset to encoded_array_item, or 0
/// }
/// ```
///
/// Unlike other id-pool items, class_def_items are **not** required to
/// be sorted by any field — their order is semantic (reflecting the
/// class-definition dependency order the DEX VM needs for loading), so
/// no `NonDecreasing` newtype gauge-fix applies here. The caller hands
/// in a slice and this function emits in that order.
///
/// `superclass_idx: Option<TypeIdx>` and `source_file_idx:
/// Option<StringIdx>` are mapped to the sentinel `NO_INDEX`
/// (0xFFFFFFFF) when absent. `interfaces_off`, `annotations_off`,
/// `class_data_off`, `static_values_off` pass through as u32 (zero
/// means "no such section for this class"); the assembly layer
/// resolves these offsets from the emitted data sections before
/// passing a `ClassDefItem` to this function.
///
/// Returns `UnrepresentableIR` when `class_idx` — held as `TypeIdx` —
/// exceeds `NO_INDEX - 1` (reserved sentinel). Other `TypeIdx` fields
/// accept the full u32 range because `Option<TypeIdx>::None` maps to
/// the sentinel at emit; `Some(TypeIdx(NO_INDEX))` would be
/// indistinguishable and is thus rejected as unrepresentable.
pub fn emit_class_def_pool(classes: &[ClassDefItem]) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::with_capacity(alloc_cap(classes.len().saturating_mul(32)));
    for c in classes {
        if c.class_idx.0 == NO_INDEX {
            return Err(DexEmitError::UnrepresentableIR {
                why: "class_def_item.class_idx collides with NO_INDEX sentinel (DEX §7.10)",
            });
        }
        // Sentinel collision: Some(TypeIdx(NO_INDEX)) / Some(StringIdx(NO_INDEX))
        // is indistinguishable from None after emit (both write NO_INDEX).
        // Reject at emit rather than silently merge.
        if let Some(TypeIdx(idx)) = c.superclass_idx {
            if idx == NO_INDEX {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "class_def.superclass_idx = Some(TypeIdx(NO_INDEX)) collides with None sentinel",
                });
            }
        }
        if let Some(StringIdx(idx)) = c.source_file_idx {
            if idx == NO_INDEX {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "class_def.source_file_idx = Some(StringIdx(NO_INDEX)) collides with None sentinel",
                });
            }
        }
        out.extend_from_slice(&c.class_idx.0.to_le_bytes());
        out.extend_from_slice(&c.access_flags.to_le_bytes());
        let superclass = c.superclass_idx.map(|t| t.0).unwrap_or(NO_INDEX);
        out.extend_from_slice(&superclass.to_le_bytes());
        out.extend_from_slice(&c.interfaces_off.to_le_bytes());
        let source_file = c.source_file_idx.map(|s| s.0).unwrap_or(NO_INDEX);
        out.extend_from_slice(&source_file.to_le_bytes());
        out.extend_from_slice(&c.annotations_off.to_le_bytes());
        out.extend_from_slice(&c.class_data_off.to_le_bytes());
        out.extend_from_slice(&c.static_values_off.to_le_bytes());
    }
    Ok(out)
}

/// Emit a `class_data_item` (DEX §7.13) — the per-class field and
/// method inventory that `class_def_item.class_data_off` points at.
///
/// ## Layout
///
/// ```text
/// class_data_item {
///     uleb128 static_fields_size;
///     uleb128 instance_fields_size;
///     uleb128 direct_methods_size;
///     uleb128 virtual_methods_size;
///     encoded_field  static_fields   [static_fields_size];
///     encoded_field  instance_fields [instance_fields_size];
///     encoded_method direct_methods  [direct_methods_size];
///     encoded_method virtual_methods [virtual_methods_size];
/// }
///
/// encoded_field  { uleb128 field_idx_diff; uleb128 access_flags; }
/// encoded_method { uleb128 method_idx_diff; uleb128 access_flags; uleb128 code_off; }
/// ```
///
/// ## idx_diff encoding
///
/// Each list is stored as delta-encoded indices: the first entry's
/// `*_idx_diff` is its absolute index; each subsequent entry stores
/// `current_idx - previous_idx`. This requires the list to be in
/// **strictly ascending** order by idx — duplicates are unrepresentable
/// (the diff would be 0 which the parser disambiguates as "first
/// entry"). Callers must pre-sort; this function rejects non-ascending
/// or duplicate inputs with `UnrepresentableIR`.
///
/// The four lists have independent accumulators — the diff in
/// `instance_fields[0]` is its absolute idx, not a diff from
/// `static_fields[last]`. Parser-side `parse_class_data` resets the
/// accumulator at each list boundary; emit mirrors that.
pub fn emit_class_data_item(
    static_fields: &[EncodedField],
    instance_fields: &[EncodedField],
    direct_methods: &[EncodedMethod],
    virtual_methods: &[EncodedMethod],
) -> Result<Vec<u8>, DexEmitError> {
    let mut out = Vec::new();

    let static_sz = u32::try_from(static_fields.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "class_data_item",
        context: "static_fields_size exceeds u32",
    })?;
    let instance_sz =
        u32::try_from(instance_fields.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "class_data_item",
            context: "instance_fields_size exceeds u32",
        })?;
    let direct_sz =
        u32::try_from(direct_methods.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "class_data_item",
            context: "direct_methods_size exceeds u32",
        })?;
    let virtual_sz =
        u32::try_from(virtual_methods.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "class_data_item",
            context: "virtual_methods_size exceeds u32",
        })?;

    write_uleb128(&mut out, static_sz);
    write_uleb128(&mut out, instance_sz);
    write_uleb128(&mut out, direct_sz);
    write_uleb128(&mut out, virtual_sz);

    emit_encoded_field_list(&mut out, static_fields)?;
    emit_encoded_field_list(&mut out, instance_fields)?;
    emit_encoded_method_list(&mut out, direct_methods)?;
    emit_encoded_method_list(&mut out, virtual_methods)?;

    Ok(out)
}

fn emit_encoded_field_list(
    out: &mut Vec<u8>,
    fields: &[EncodedField],
) -> Result<(), DexEmitError> {
    let mut prev: Option<u32> = None;
    for f in fields {
        let idx = f.field_idx.0;
        let diff = match prev {
            None => idx,
            Some(p) => idx.checked_sub(p).filter(|d| *d > 0).ok_or(
                DexEmitError::UnrepresentableIR {
                    why: "class_data_item: encoded_field list must be strictly ascending by field_idx (DEX §7.14)",
                },
            )?,
        };
        write_uleb128(out, diff);
        write_uleb128(out, f.access_flags);
        prev = Some(idx);
    }
    Ok(())
}

fn emit_encoded_method_list(
    out: &mut Vec<u8>,
    methods: &[EncodedMethod],
) -> Result<(), DexEmitError> {
    let mut prev: Option<u32> = None;
    for m in methods {
        let idx = m.method_idx.0;
        let diff = match prev {
            None => idx,
            Some(p) => idx.checked_sub(p).filter(|d| *d > 0).ok_or(
                DexEmitError::UnrepresentableIR {
                    why: "class_data_item: encoded_method list must be strictly ascending by method_idx (DEX §7.15)",
                },
            )?,
        };
        write_uleb128(out, diff);
        write_uleb128(out, m.access_flags);
        write_uleb128(out, m.code_off);
        prev = Some(idx);
    }
    Ok(())
}

/// Emit the `code_item` container (DEX §7.16) — the fixed-shape code
/// block referenced by `encoded_method.code_off`. This function owns
/// the header + padding + `try_item[]` + `encoded_catch_handler_list`
/// layout but treats the instruction stream as opaque bytes: the
/// caller pre-encodes instructions (and their inline payloads) into
/// `insn_bytes`. Instruction-level encoding lives in a separate step.
///
/// ## Layout
///
/// ```text
/// code_item {
///     u16 registers_size;
///     u16 ins_size;
///     u16 outs_size;
///     u16 tries_size;
///     u32 debug_info_off;
///     u32 insns_size;              // in 16-bit code units
///     u16 insns[insns_size];
///     u16 padding;                 // if tries_size != 0 AND insns_size is odd
///     try_item    tries[tries_size];
///     encoded_catch_handler_list handlers;  // if tries_size != 0
/// }
///
/// try_item {
///     u32 start_addr;
///     u16 insn_count;
///     u16 handler_off;             // BYTE offset into handlers list
/// }
/// ```
///
/// ## Handler-offset resolution
///
/// Parser-side `parse_code_item` reads `try_item.handler_off` as a
/// byte offset relative to the start of `encoded_catch_handler_list`
/// and converts it to a handler index (`TryItem.handler_idx`). Emit
/// does the inverse: it lays the handlers out linearly, records the
/// byte offset of each, and rewrites each try's `handler_idx` into
/// the corresponding u16 `handler_off`. Multiple tries may reference
/// the same handler — their emitted `handler_off` values coincide.
///
/// ## Errors
///
/// - `UnrepresentableIR` — odd-byte `insn_bytes`, `handler_idx` out of
///   range, non-empty `catch_handlers` with empty `tries` (spec §7.16
///   requires handler list be reachable only through tries), or a
///   handler with `size=0` and no `catch_all_addr` (spec §7.17 — an
///   encoded handler with neither typed catches nor a catch-all has
///   no runtime meaning and isn't representable in the format).
/// - `SizeOverflow` — `insn_bytes.len()/2` exceeds u32, or `tries.len()`
///   exceeds u16.
/// - `OffsetOverflow` — computed `handler_off` exceeds u16 (the handler
///   list grew past 64 KiB).
pub fn emit_code_item_container(
    registers_size: u16,
    ins_size: u16,
    outs_size: u16,
    debug_info_off: u32,
    insn_bytes: &[u8],
    tries: &[TryItem],
    catch_handlers: &[CatchHandler],
) -> Result<Vec<u8>, DexEmitError> {
    if !insn_bytes.len().is_multiple_of(2) {
        return Err(DexEmitError::UnrepresentableIR {
            why: "code_item: insn_bytes must be a whole number of u16 code units (DEX §7.16)",
        });
    }
    let insns_size =
        u32::try_from(insn_bytes.len() / 2).map_err(|_| DexEmitError::SizeOverflow {
            layer: "code_item",
            context: "insns_size exceeds u32 (instruction stream > 8 GiB of code units)",
        })?;
    let tries_size = u16::try_from(tries.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "code_item",
        context: "tries_size exceeds u16 (DEX §7.16)",
    })?;

    if tries_size == 0 && !catch_handlers.is_empty() {
        return Err(DexEmitError::UnrepresentableIR {
            why: "code_item: catch_handlers non-empty but tries is empty — list is unreachable per §7.16",
        });
    }
    for t in tries {
        if t.handler_idx >= catch_handlers.len() {
            return Err(DexEmitError::UnrepresentableIR {
                why: "code_item: try_item.handler_idx out of range for catch_handlers list",
            });
        }
    }

    // Serialize the encoded_catch_handler_list into a scratch buffer
    // and record each handler's start byte offset (relative to list
    // start). The list starts with a ULEB128 count, so handler[0]
    // begins at offset = bytes consumed by that ULEB128.
    let mut handlers_blob: Vec<u8> = Vec::new();
    let mut handler_starts: Vec<usize> = Vec::with_capacity(catch_handlers.len());
    if tries_size > 0 {
        let count = u32::try_from(catch_handlers.len()).map_err(|_| DexEmitError::SizeOverflow {
            layer: "code_item",
            context: "catch_handlers count exceeds u32",
        })?;
        write_uleb128(&mut handlers_blob, count);
        for h in catch_handlers {
            handler_starts.push(handlers_blob.len());
            emit_encoded_catch_handler(&mut handlers_blob, h)?;
        }
    }

    // Resolve each try's handler_off.
    let mut try_handler_offs: Vec<u16> = Vec::with_capacity(tries.len());
    for t in tries {
        // `handler_starts` was populated above only when tries_size > 0
        // (and thus catch_handlers non-empty); a try existing implies
        // tries_size > 0, so this index is in bounds.
        let off = handler_starts[t.handler_idx];
        let off_u16 = u16::try_from(off).map_err(|_| DexEmitError::OffsetOverflow {
            section: "code_item",
            context: "try_item.handler_off exceeds u16 (catch-handler list > 64 KiB)",
        })?;
        try_handler_offs.push(off_u16);
    }

    let padding_len = if tries_size > 0 && !insns_size.is_multiple_of(2) {
        2
    } else {
        0
    };
    let total = 16usize
        .saturating_add(insn_bytes.len())
        .saturating_add(padding_len)
        .saturating_add(tries.len().saturating_mul(8))
        .saturating_add(handlers_blob.len());
    let mut out = Vec::with_capacity(alloc_cap(total));

    out.extend_from_slice(&registers_size.to_le_bytes());
    out.extend_from_slice(&ins_size.to_le_bytes());
    out.extend_from_slice(&outs_size.to_le_bytes());
    out.extend_from_slice(&tries_size.to_le_bytes());
    out.extend_from_slice(&debug_info_off.to_le_bytes());
    out.extend_from_slice(&insns_size.to_le_bytes());
    out.extend_from_slice(insn_bytes);

    if tries_size > 0 {
        if padding_len > 0 {
            out.extend_from_slice(&[0u8; 2]);
        }
        for (t, handler_off) in tries.iter().zip(&try_handler_offs) {
            out.extend_from_slice(&t.start_addr.to_le_bytes());
            out.extend_from_slice(&t.insn_count.to_le_bytes());
            out.extend_from_slice(&handler_off.to_le_bytes());
        }
        out.extend_from_slice(&handlers_blob);
    }

    Ok(out)
}

/// Emit one `encoded_catch_handler` (DEX §7.17). The size field is
/// SLEB128 and encodes two signals together: its absolute value is the
/// count of typed catches; its sign encodes whether a `catch_all_addr`
/// follows (`size <= 0` means yes, `size > 0` means no). The special
/// case `size == 0` must be paired with a `catch_all_addr` — a handler
/// with neither typed catches nor a catch-all has no runtime meaning
/// and is rejected.
fn emit_encoded_catch_handler(
    out: &mut Vec<u8>,
    h: &CatchHandler,
) -> Result<(), DexEmitError> {
    let typed_count = i32::try_from(h.catches.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "encoded_catch_handler",
        context: "typed-catch count exceeds i32",
    })?;
    let size_field = match (typed_count, h.catch_all_addr.is_some()) {
        (0, false) => {
            return Err(DexEmitError::UnrepresentableIR {
                why: "encoded_catch_handler: no typed catches AND no catch_all_addr (§7.17 — handler has no runtime effect)",
            });
        }
        (_, true) => i32::checked_neg(typed_count).ok_or(DexEmitError::SizeOverflow {
            layer: "encoded_catch_handler",
            context: "typed-catch count = i32::MIN cannot be negated for size field",
        })?,
        (_, false) => typed_count,
    };
    write_sleb128(out, size_field);
    for c in &h.catches {
        write_uleb128(out, c.exception_type.0);
        write_uleb128(out, c.handler_addr);
    }
    if let Some(addr) = h.catch_all_addr {
        write_uleb128(out, addr);
    }
    Ok(())
}

// ── Instruction emission ────────────────────────────────────────────

/// Append the little-endian u16 encoding of `val` to `out`.
fn push_u16(out: &mut Vec<u8>, val: u16) {
    out.extend_from_slice(&val.to_le_bytes());
}

/// Compute the branch offset in 16-bit code units, i.e. `target - pc`.
/// Offsets live in the instruction's immediate field (signed i8/i16/i32
/// depending on format). Returns the signed difference; the caller
/// bounds-checks it against the format's width.
#[allow(clippy::arithmetic_side_effects, reason = "PROOF: target, pc: u32 from parser-validated instruction addresses (DEX spec caps insns_size to 0xFFFE units); the signed difference is computed in i64 to span the full u32 range, so subtraction cannot wrap.")]
// (range 0..=u32::MAX ≈ 4.29e9). i64::from widens to i64. The
// difference lies in [-u32::MAX, +u32::MAX] = [-4.29e9, +4.29e9],
// well within i64 range [-9.22e18, +9.22e18] — the subtraction
// cannot overflow or wrap.
fn branch_offset(target: u32, pc: u32) -> i64 {
    i64::from(target) - i64::from(pc)
}

fn dst_u4(insn: &Instruction) -> Result<u16, DexEmitError> {
    let d = insn.dst.ok_or(DexEmitError::UnrepresentableIR {
        why: "instruction format requires dst register but IR has none",
    })?;
    if d > 0x0F {
        return Err(DexEmitError::UnrepresentableIR {
            why: "4-bit register field overflow: dst > 0x0F (instruction format violation)",
        });
    }
    Ok(d)
}

fn dst_u8(insn: &Instruction) -> Result<u16, DexEmitError> {
    let d = insn.dst.ok_or(DexEmitError::UnrepresentableIR {
        why: "instruction format requires dst register but IR has none",
    })?;
    if d > 0xFF {
        return Err(DexEmitError::UnrepresentableIR {
            why: "8-bit register field overflow: dst > 0xFF",
        });
    }
    Ok(d)
}

fn src_u4(insn: &Instruction, slot: usize) -> Result<u16, DexEmitError> {
    let s = *insn.src.as_slice().get(slot).ok_or(DexEmitError::UnrepresentableIR {
        why: "instruction format requires src register that IR RegList does not provide",
    })?;
    if s > 0x0F {
        return Err(DexEmitError::UnrepresentableIR {
            why: "4-bit register field overflow: src > 0x0F",
        });
    }
    Ok(s)
}

fn src_u8(insn: &Instruction, slot: usize) -> Result<u16, DexEmitError> {
    let s = *insn.src.as_slice().get(slot).ok_or(DexEmitError::UnrepresentableIR {
        why: "instruction format requires src register that IR RegList does not provide",
    })?;
    if s > 0xFF {
        return Err(DexEmitError::UnrepresentableIR {
            why: "8-bit register field overflow: src > 0xFF",
        });
    }
    Ok(s)
}

fn src_u16(insn: &Instruction, slot: usize) -> Result<u16, DexEmitError> {
    Ok(*insn.src.as_slice().get(slot).ok_or(
        DexEmitError::UnrepresentableIR {
            why: "instruction format requires src register that IR RegList does not provide",
        },
    )?)
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: bounds-checked above to -8..=7 (4-bit signed). `insn.literal as i8 as u8 & 0x0F` is the IDIOM for two's-complement packing — the range check at the top of the fn guarantees value ∈ [-8, 7], so narrowing i64→i8→u8 and then masking to 0x0F is exact."
)]
fn literal_i4(insn: &Instruction) -> Result<u16, DexEmitError> {
    // 4-bit signed: -8..=7.
    if insn.literal < -8 || insn.literal > 7 {
        return Err(DexEmitError::UnrepresentableIR {
            why: "const/4: literal out of 4-bit signed range (-8..=7)",
        });
    }
    // Pack as 4-bit two's complement.
    Ok(u16::from(insn.literal as i8 as u8 & 0x0F))
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: bounds-checked above to i8::MIN..=i8::MAX by the range guard. `insn.literal as i8 as u8` is the IDIOM for two's-complement byte extraction — narrowing i64→i8 is exact in [i8::MIN, i8::MAX]; then u8 reinterprets the bit pattern, which is the intent."
)]
fn literal_i8_as_u8(insn: &Instruction) -> Result<u8, DexEmitError> {
    if insn.literal < i64::from(i8::MIN) || insn.literal > i64::from(i8::MAX) {
        return Err(DexEmitError::UnrepresentableIR {
            why: "8-bit signed literal out of range (i8::MIN..=i8::MAX)",
        });
    }
    Ok(insn.literal as i8 as u8)
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: bounds-checked above to i16::MIN..=i16::MAX by the range guard. `insn.literal as i16 as u16` is the IDIOM for two's-complement 16-bit extraction — narrowing i64→i16 is exact in [i16::MIN, i16::MAX]; then u16 reinterprets the bit pattern."
)]
fn literal_i16_as_u16(insn: &Instruction) -> Result<u16, DexEmitError> {
    if insn.literal < i64::from(i16::MIN) || insn.literal > i64::from(i16::MAX) {
        return Err(DexEmitError::UnrepresentableIR {
            why: "16-bit signed literal out of range (i16::MIN..=i16::MAX)",
        });
    }
    Ok(insn.literal as i16 as u16)
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    reason = "PROOF: bounds-checked above to i32::MIN..=i32::MAX by the range guard. `insn.literal as i32` narrows i64 to i32; the range check guarantees value ∈ [i32::MIN, i32::MAX], so narrowing is exact."
)]
fn literal_i32(insn: &Instruction) -> Result<i32, DexEmitError> {
    if insn.literal < i64::from(i32::MIN) || insn.literal > i64::from(i32::MAX) {
        return Err(DexEmitError::UnrepresentableIR {
            why: "32-bit signed literal out of range (i32::MIN..=i32::MAX)",
        });
    }
    Ok(insn.literal as i32)
}

/// Extract a pool index's u32 value, discarding the variant tag (which
/// the opcode already encodes). Rejects `None` as structural-shape
/// violation: the format slot demands a pool reference.
fn pool_u32(insn: &Instruction) -> Result<u32, DexEmitError> {
    let p = insn.pool_idx.ok_or(DexEmitError::UnrepresentableIR {
        why: "instruction format requires pool index but IR has none",
    })?;
    Ok(match p {
        PoolIndex::String(s) => s.0,
        PoolIndex::Type(t) => t.0,
        PoolIndex::Field(f) => f.0,
        PoolIndex::Method(m) => m.0,
        PoolIndex::MethodAndProto(m, _) => m.0,
        PoolIndex::CallSite(c) => c.0,
    })
}

fn pool_u16(insn: &Instruction) -> Result<u16, DexEmitError> {
    let idx = pool_u32(insn)?;
    u16::try_from(idx).map_err(|_| DexEmitError::UnrepresentableIR {
        why: "pool index exceeds u16 but instruction format is 16-bit (use jumbo form?)",
    })
}

/// Extract the call-site proto_idx from an `invoke-polymorphic{,/range}`
/// instruction's `MethodAndProto` pool slot. Error shape matches
/// `pool_u16` for non-MethodAndProto inputs since the call site is
/// format-specific (F45cc / F4rcc only).
fn pool_proto_u16(insn: &Instruction) -> Result<u16, DexEmitError> {
    let p = insn.pool_idx.ok_or(DexEmitError::UnrepresentableIR {
        why: "F45cc/F4rcc instruction missing pool index",
    })?;
    let PoolIndex::MethodAndProto(_, proto) = p else {
        return Err(DexEmitError::UnrepresentableIR {
            why: "F45cc/F4rcc instruction must carry MethodAndProto pool index",
        });
    };
    u16::try_from(proto.0).map_err(|_| DexEmitError::UnrepresentableIR {
        why: "call-site proto_idx exceeds u16",
    })
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: bounds-checked above to i8::MIN..=i8::MAX by the `.contains(&diff)` guard. `diff as i8 as u8` is the IDIOM for two's-complement byte branch-offset encoding — narrowing i64→i8 is exact in [i8::MIN, i8::MAX]; u8 reinterprets the pattern."
)]
fn branch_i8_as_u8(target: u32, pc: u32) -> Result<u8, DexEmitError> {
    let diff = branch_offset(target, pc);
    if !(i64::from(i8::MIN)..=i64::from(i8::MAX)).contains(&diff) {
        return Err(DexEmitError::OffsetOverflow {
            section: "instruction_branch",
            context: "8-bit branch offset out of range (use goto/16 or goto/32)",
        });
    }
    Ok(diff as i8 as u8)
}

#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "PROOF: bounds-checked above to i16::MIN..=i16::MAX by the `.contains(&diff)` guard. `diff as i16 as u16` is the IDIOM for two's-complement 16-bit branch-offset encoding — narrowing i64→i16 is exact in [i16::MIN, i16::MAX]; u16 reinterprets the pattern."
)]
fn branch_i16_as_u16(target: u32, pc: u32) -> Result<u16, DexEmitError> {
    let diff = branch_offset(target, pc);
    if !(i64::from(i16::MIN)..=i64::from(i16::MAX)).contains(&diff) {
        return Err(DexEmitError::OffsetOverflow {
            section: "instruction_branch",
            context: "16-bit branch offset out of range (use goto/32)",
        });
    }
    Ok(diff as i16 as u16)
}

fn branch_i32(target: u32, pc: u32) -> Result<i32, DexEmitError> {
    let diff = branch_offset(target, pc);
    i32::try_from(diff).map_err(|_| DexEmitError::OffsetOverflow {
        section: "instruction_branch",
        context: "32-bit branch offset overflow (target and pc more than 2^31 code units apart)",
    })
}

/// Emit one instruction to the u16 code-unit stream. The caller is
/// responsible for interleaving payload pseudo-instructions
/// (packed-switch-payload, sparse-switch-payload, fill-array-data-
/// payload) at the addresses referenced by switch / fill-array-data
/// instructions — this function emits the switch/fill instruction
/// itself but not the payload block.
///
/// `insn.addr` is the PC (in 16-bit code units) where this instruction
/// sits. It's used to resolve branch targets (`target - addr`) into the
/// signed-offset immediate field.
#[allow(
    clippy::as_conversions,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::cast_possible_wrap,
    reason = "PROOF / INTENT: every cast in emit_instruction is the inverse of decode_single's reading discipline. (1) `(literal as i32) >> 16 as u16` / `literal >> 48 as u16` extract the high half of an i32 / i64 ConstHigh16 / ConstWideHigh16 immediate — narrowing exact since emit rejects literals with nonzero low bits. (2) `off as u32` / `lit as u32` on F30t/F31t/F31i is the inverse of decode's `(lo | hi<<16) as i32` — IDIOM bit-reinterpretation. (3) `arg_count as u16` is widening (count ≤ 5 per F35c/F45cc). (4) `aa = literal as u16` on F3rc/F4rcc is bounds-checked to [0,255] above. (5) `lit as u64` on F51l is IDIOM bit-reinterpretation (i64 literal stored as raw 64-bit pattern). (6) mask-then-narrow `(x & 0xFFFF) as u16` is exact since the mask zeros the high bits."
)]
pub fn emit_instruction(out: &mut Vec<u8>, insn: &Instruction) -> Result<(), DexEmitError> {
    use crate::opcodes::Opcode as Op;
    let op_byte = insn.op.to_u8();
    let pc = insn.addr;
    let fmt = insn_format(insn.op);

    match fmt {
        InsnFormat::F10x => {
            // op ignored-byte
            push_u16(out, u16::from(op_byte));
        }
        InsnFormat::F12x => {
            let a = dst_u4(insn)?;
            let b = src_u4(insn, 0)?;
            push_u16(out, u16::from(op_byte) | (a << 8) | (b << 12));
        }
        InsnFormat::F11n => {
            let a = dst_u4(insn)?;
            let b = literal_i4(insn)?;
            push_u16(out, u16::from(op_byte) | (a << 8) | (b << 12));
        }
        InsnFormat::F11x => {
            let aa = dst_u8(insn)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
        }
        InsnFormat::F10t => {
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "goto: missing branch target",
            })?;
            let off = branch_i8_as_u8(target, pc)?;
            push_u16(out, u16::from(op_byte) | (u16::from(off) << 8));
        }
        InsnFormat::F20t => {
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "goto/16: missing branch target",
            })?;
            let off = branch_i16_as_u16(target, pc)?;
            push_u16(out, u16::from(op_byte));
            push_u16(out, off);
        }
        InsnFormat::F22x => {
            let aa = dst_u8(insn)?;
            let b = src_u16(insn, 0)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, b);
        }
        InsnFormat::F21t => {
            let aa = dst_u8(insn)?;
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "21t: missing branch target",
            })?;
            let off = branch_i16_as_u16(target, pc)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, off);
        }
        InsnFormat::F21s => {
            let aa = dst_u8(insn)?;
            let lit = literal_i16_as_u16(insn)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, lit);
        }
        InsnFormat::F21h => {
            let aa = dst_u8(insn)?;
            // const/high16 and const-wide/high16: the low 16 (or 48) bits of
            // the literal are implicit zero; emit only the significant u16.
            let unit = match insn.op {
                Op::ConstHigh16 => {
                    // literal = (u1 << 16) as i32 as i64. Extract bits [16..32].
                    if (insn.literal & 0xFFFF) != 0 {
                        return Err(DexEmitError::UnrepresentableIR {
                            why: "const/high16: low 16 bits must be zero",
                        });
                    }
                    if insn.literal < i64::from(i32::MIN) || insn.literal > i64::from(i32::MAX) {
                        return Err(DexEmitError::UnrepresentableIR {
                            why: "const/high16: literal out of i32 range",
                        });
                    }
                    ((insn.literal as i32) >> 16) as u16
                }
                Op::ConstWideHigh16 => {
                    // literal = (u1 << 48) as i64. Extract bits [48..64].
                    if (insn.literal & 0x0000_FFFF_FFFF_FFFF) != 0 {
                        return Err(DexEmitError::UnrepresentableIR {
                            why: "const-wide/high16: low 48 bits must be zero",
                        });
                    }
                    (insn.literal >> 48) as u16
                }
                _ => {
                    return Err(DexEmitError::UnrepresentableIR {
                        why: "F21h format reached for opcode that isn't const/high16 or const-wide/high16",
                    });
                }
            };
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, unit);
        }
        InsnFormat::F21c => {
            let aa = dst_u8(insn)?;
            let idx = pool_u16(insn)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, idx);
        }
        InsnFormat::F23x => {
            let aa = dst_u8(insn)?;
            let bb = src_u8(insn, 0)?;
            let cc = src_u8(insn, 1)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, bb | (cc << 8));
        }
        InsnFormat::F22t => {
            let a = src_u4(insn, 0)?;
            let b = src_u4(insn, 1)?;
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "22t: missing branch target",
            })?;
            let off = branch_i16_as_u16(target, pc)?;
            push_u16(out, u16::from(op_byte) | (a << 8) | (b << 12));
            push_u16(out, off);
        }
        InsnFormat::F22s => {
            let a = dst_u4(insn)?;
            let b = src_u4(insn, 0)?;
            let lit = literal_i16_as_u16(insn)?;
            push_u16(out, u16::from(op_byte) | (a << 8) | (b << 12));
            push_u16(out, lit);
        }
        InsnFormat::F22c => {
            let a = dst_u4(insn)?;
            let b = src_u4(insn, 0)?;
            let idx = pool_u16(insn)?;
            push_u16(out, u16::from(op_byte) | (a << 8) | (b << 12));
            push_u16(out, idx);
        }
        InsnFormat::F22b => {
            let aa = dst_u8(insn)?;
            let bb = src_u8(insn, 0)?;
            let cc = literal_i8_as_u8(insn)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, bb | (u16::from(cc) << 8));
        }
        InsnFormat::F30t => {
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "goto/32: missing branch target",
            })?;
            let off = branch_i32(target, pc)?;
            let off_u = off as u32;
            push_u16(out, u16::from(op_byte));
            push_u16(out, (off_u & 0xFFFF) as u16);
            push_u16(out, ((off_u >> 16) & 0xFFFF) as u16);
        }
        InsnFormat::F32x => {
            let d = insn.dst.ok_or(DexEmitError::UnrepresentableIR {
                why: "F32x: missing dst register",
            })?;
            let s = src_u16(insn, 0)?;
            push_u16(out, u16::from(op_byte));
            push_u16(out, d);
            push_u16(out, s);
        }
        InsnFormat::F31i => {
            let aa = dst_u8(insn)?;
            let lit = literal_i32(insn)?;
            let lit_u = lit as u32;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, (lit_u & 0xFFFF) as u16);
            push_u16(out, ((lit_u >> 16) & 0xFFFF) as u16);
        }
        InsnFormat::F31t => {
            let aa = dst_u8(insn)?;
            let target = insn.target.ok_or(DexEmitError::UnrepresentableIR {
                why: "31t: missing payload target",
            })?;
            let off = branch_i32(target, pc)?;
            let off_u = off as u32;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, (off_u & 0xFFFF) as u16);
            push_u16(out, ((off_u >> 16) & 0xFFFF) as u16);
        }
        InsnFormat::F31c => {
            // const-string/jumbo: u32 string index.
            let aa = dst_u8(insn)?;
            let idx = pool_u32(insn)?;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, (idx & 0xFFFF) as u16);
            push_u16(out, ((idx >> 16) & 0xFFFF) as u16);
        }
        InsnFormat::F35c => {
            // arg_count (B, 0..=5) = insn.src.len(); high-nibble A = op_byte's
            // op nibble space via the A field of u0 (A is 4-bit).
            let arg_count = insn.src.len();
            if arg_count > 5 {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "F35c: arg_count > 5 (use invoke/range for > 5 args)",
                });
            }
            let g = if arg_count == 5 {
                src_u4(insn, 4)?
            } else {
                0
            };
            let idx = pool_u16(insn)?;
            // u2 packs CDEF in low nibbles: c = src[0], d = src[1], e = src[2],
            // f = src[3], each 4 bits. Unused slots are 0.
            let c = if arg_count > 0 { src_u4(insn, 0)? } else { 0 };
            let d = if arg_count > 1 { src_u4(insn, 1)? } else { 0 };
            let e = if arg_count > 2 { src_u4(insn, 2)? } else { 0 };
            let f = if arg_count > 3 { src_u4(insn, 3)? } else { 0 };
            let u2 = c | (d << 4) | (e << 8) | (f << 12);
            push_u16(
                out,
                u16::from(op_byte) | (g << 8) | ((arg_count as u16) << 12),
            );
            push_u16(out, idx);
            push_u16(out, u2);
        }
        InsnFormat::F3rc => {
            // arg_count (AA, 0..=255) stored in insn.literal by decoder.
            if insn.literal < 0 || insn.literal > 255 {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "F3rc: arg_count out of u8 range (stored in insn.literal)",
                });
            }
            let aa = insn.literal as u16;
            let idx = pool_u16(insn)?;
            // start_reg is src.regs[0]. Parser always stores it there,
            // even when arg_count == 0 (where CCCC is spec-irrelevant
            // but on-disk-load-bearing for byte-identity preservation).
            // raw_at(0) reads regs[0] without consulting len.
            let start_reg = insn.src.raw_at(0);
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, idx);
            push_u16(out, start_reg);
        }
        InsnFormat::F45cc => {
            // invoke-polymorphic {vC,vD,vE,vF,vG}, meth@BBBB, proto@HHHH
            let arg_count = insn.src.len();
            if arg_count > 5 {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "F45cc: arg_count > 5 (use invoke-polymorphic/range for > 5 args)",
                });
            }
            let g = if arg_count == 5 { src_u4(insn, 4)? } else { 0 };
            let method_idx = pool_u16(insn)?;
            let proto_idx = pool_proto_u16(insn)?;
            let c = if arg_count > 0 { src_u4(insn, 0)? } else { 0 };
            let d = if arg_count > 1 { src_u4(insn, 1)? } else { 0 };
            let e = if arg_count > 2 { src_u4(insn, 2)? } else { 0 };
            let f = if arg_count > 3 { src_u4(insn, 3)? } else { 0 };
            let u2 = c | (d << 4) | (e << 8) | (f << 12);
            push_u16(
                out,
                u16::from(op_byte) | (g << 8) | ((arg_count as u16) << 12),
            );
            push_u16(out, method_idx);
            push_u16(out, u2);
            push_u16(out, proto_idx);
        }
        InsnFormat::F4rcc => {
            if insn.literal < 0 || insn.literal > 255 {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "F4rcc: arg_count out of u8 range (stored in insn.literal)",
                });
            }
            let aa = insn.literal as u16;
            let method_idx = pool_u16(insn)?;
            let proto_idx = pool_proto_u16(insn)?;
            let start_reg = if insn.src.is_empty() {
                0
            } else {
                src_u16(insn, 0)?
            };
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, method_idx);
            push_u16(out, start_reg);
            push_u16(out, proto_idx);
        }
        InsnFormat::F51l => {
            let aa = dst_u8(insn)?;
            let lit = insn.literal as u64;
            push_u16(out, u16::from(op_byte) | (aa << 8));
            push_u16(out, (lit & 0xFFFF) as u16);
            push_u16(out, ((lit >> 16) & 0xFFFF) as u16);
            push_u16(out, ((lit >> 32) & 0xFFFF) as u16);
            push_u16(out, ((lit >> 48) & 0xFFFF) as u16);
        }
    }
    Ok(())
}

// ── Payload emission ────────────────────────────────────────────────
//
// Payloads are pseudo-instructions embedded in the `insns[]` stream at
// addresses referenced by `packed-switch`, `sparse-switch`, and
// `fill-array-data` instructions. Each begins with a u16 ident code
// unit whose low byte is 0x00 (Nop opcode) and whose high byte is a
// kind tag:
//
//   0x01 — packed-switch-payload   (spec §6)
//   0x02 — sparse-switch-payload
//   0x03 — fill-array-data-payload
//
// Switch-payload target fields are relative offsets from the SWITCH
// instruction's PC (not the payload's PC), so emit requires both the
// payload's absolute target list (from the IR) and `switch_pc` to
// produce format-valid relative offsets.

/// Emit a `packed-switch-payload` pseudo-instruction.
///
/// ## Layout
///
/// ```text
/// u16 ident = 0x0100             // low byte Nop, high byte 0x01
/// u16 size                       // target count
/// i32 first_key                  // first case value
/// i32 targets[size]              // offsets from switch_pc (signed)
/// ```
///
/// Total size: 4 + 2*size code units = 8 + 4*size bytes.
///
/// Returns `SizeOverflow` if `targets.len()` exceeds u16.
/// Returns `OffsetOverflow` if any `target - switch_pc` difference
/// doesn't fit in i32.
pub fn emit_packed_switch_payload(
    out: &mut Vec<u8>,
    first_key: i32,
    targets: &[u32],
    switch_pc: u32,
) -> Result<(), DexEmitError> {
    let size = u16::try_from(targets.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "packed_switch_payload",
        context: "target count exceeds u16 (DEX spec §6)",
    })?;
    push_u16(out, 0x0100);
    push_u16(out, size);
    out.extend_from_slice(&first_key.to_le_bytes());
    for t in targets {
        let rel = branch_i32(*t, switch_pc)?;
        out.extend_from_slice(&rel.to_le_bytes());
    }
    Ok(())
}

/// Emit a `sparse-switch-payload` pseudo-instruction.
///
/// ## Layout
///
/// ```text
/// u16 ident = 0x0200
/// u16 size                       // entry count
/// i32 keys[size]                 // strictly ascending (per spec)
/// i32 targets[size]              // offsets from switch_pc
/// ```
///
/// Total size: 2 + 4*size code units = 4 + 8*size bytes.
///
/// ## Gauge-fix
///
/// Keys arrive as [`StrictlyAscending<i32>`], so the "keys must be
/// strictly ascending" invariant (spec §6 — VM does binary search on
/// them) is satisfied by construction rather than by runtime assert.
/// The only remaining runtime check is `keys.len() == targets.len()`:
/// the two slices are independent arguments and the type system can't
/// couple their lengths without a more invasive signature change.
pub fn emit_sparse_switch_payload(
    out: &mut Vec<u8>,
    keys: &StrictlyAscending<i32>,
    targets: &[u32],
    switch_pc: u32,
) -> Result<(), DexEmitError> {
    if keys.len() != targets.len() {
        return Err(DexEmitError::UnrepresentableIR {
            why: "sparse_switch_payload: keys.len() != targets.len()",
        });
    }
    let size = u16::try_from(keys.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "sparse_switch_payload",
        context: "entry count exceeds u16",
    })?;
    push_u16(out, 0x0200);
    push_u16(out, size);
    for k in keys.iter() {
        out.extend_from_slice(&k.to_le_bytes());
    }
    for t in targets {
        let rel = branch_i32(*t, switch_pc)?;
        out.extend_from_slice(&rel.to_le_bytes());
    }
    Ok(())
}

/// Emit a `fill-array-data-payload` pseudo-instruction.
///
/// ## Layout
///
/// ```text
/// u16 ident = 0x0300
/// u16 element_width              // bytes per element (1, 2, 4, or 8)
/// u32 size                       // element count
/// u8  data[size * element_width] // element bytes, then u16-pad if odd
/// ```
///
/// Total size: 4 + ceil(size*element_width/2) code units.
///
/// Unlike switch payloads, this one has no switch_pc dependency — its
/// data is a literal byte blob. Requires `element_width` in {1, 2, 4,
/// 8} (per DEX spec — no other widths are legal) and `data.len()` to
/// be an exact multiple of `element_width`. Trailing padding byte is
/// inserted if the total data length is odd (payload must end on a
/// code-unit boundary).
#[allow(
    clippy::as_conversions,
    reason = "PROOF: `element_width as usize` — element_width is a u16 constrained to {1, 2, 4, 8} by the match guard above; u16 → usize is a widen, lossless on all supported 16-bit+ platforms."
)]
pub fn emit_fill_array_data_payload(
    out: &mut Vec<u8>,
    element_width: u16,
    data: &[u8],
) -> Result<(), DexEmitError> {
    match element_width {
        1 | 2 | 4 | 8 => {}
        _ => {
            return Err(DexEmitError::UnrepresentableIR {
                why: "fill_array_data_payload: element_width must be 1, 2, 4, or 8 (DEX spec §6)",
            });
        }
    }
    if !data.len().is_multiple_of(element_width as usize) {
        return Err(DexEmitError::UnrepresentableIR {
            why: "fill_array_data_payload: data length is not a multiple of element_width",
        });
    }
    #[allow(clippy::arithmetic_side_effects, reason = "PROOF: element_width ∈ {1,2,4,8} (checked above) — non-zero — and data.len() is a multiple of element_width (also checked), so the division is well-defined and exact.")]
    let element_count_usize = data.len() / element_width as usize;
    let element_count =
        u32::try_from(element_count_usize).map_err(|_| DexEmitError::SizeOverflow {
            layer: "fill_array_data_payload",
            context: "element count exceeds u32",
        })?;
    push_u16(out, 0x0300);
    push_u16(out, element_width);
    out.extend_from_slice(&element_count.to_le_bytes());
    out.extend_from_slice(data);
    if !data.len().is_multiple_of(2) {
        out.push(0);
    }
    Ok(())
}

/// Dispatch: emit `payload` at its address in the insn stream, using
/// the given `switch_pc` (ignored for fill-array-data). Convenience
/// wrapper around the three primitives.
///
/// # Caller invariant — payload alignment (DEX spec §6)
///
/// Payloads must sit at 4-byte-aligned positions in the `insns[]`
/// stream. This is equivalent to saying the payload's address in
/// 16-bit code units is **even**. The DEX VM reads payload fields
/// (`first_key`, `size`, keys, targets) as i32/u32 via aligned loads;
/// a misaligned payload produces undefined behavior on strict-
/// alignment architectures (ARMv7 and below) and unaligned-access
/// penalties on x86.
///
/// Neither `emit_payload` nor the individual payload primitives can
/// verify this invariant — position is a property of the surrounding
/// stream, not of the function's inputs. **The caller (assembly
/// layer) must ensure**: for each switch/fill-array-data instruction
/// at pc=P referencing a payload at pc=T, T is even AND the caller
/// interleaves a padding `nop` before the payload when needed to
/// enforce that.
///
/// The current `emit_instruction` does not auto-pad. A future
/// `emit_instruction_stream(insns, payloads)` helper may add this
/// discipline automatically.
pub fn emit_payload(
    out: &mut Vec<u8>,
    payload: &PayloadData,
    switch_pc: u32,
) -> Result<(), DexEmitError> {
    match payload {
        PayloadData::PackedSwitch { first_key, targets } => {
            emit_packed_switch_payload(out, *first_key, targets, switch_pc)
        }
        PayloadData::SparseSwitch { keys, targets } => {
            // PayloadData carries plain Vec<i32>; the dispatcher verifies
            // strict-ascending at this boundary so callers holding raw
            // PayloadData (typically from parse output) don't need to
            // re-sort. Parser-produced data is already sorted (DEX spec
            // enforces it on input); emit trusts-but-verifies.
            let keys = StrictlyAscending::from_verified(keys.clone()).map_err(|_| {
                DexEmitError::UnrepresentableIR {
                    why: "sparse_switch payload: keys not strictly ascending (spec §6)",
                }
            })?;
            emit_sparse_switch_payload(out, &keys, targets, switch_pc)
        }
        PayloadData::FillArrayData { element_width, data } => {
            emit_fill_array_data_payload(out, *element_width, data)
        }
    }
}

// ── Header (DEX §7.2) ──────────────────────────────────────────────
//
// Fixed 112-byte prefix. Contains magic + version + checksum/signature
// (filled post-assembly by step 12) + size/offset for every top-level
// section. The assembly layer computes all offsets by laying out
// sections in order, then calls emit_header with the resulting
// HeaderLayout.

/// Fully-resolved section layout for the DEX file — the input to
/// [`emit_header`]. All offsets are byte offsets from the file start;
/// all sizes are item counts (not byte sizes). Zero for absent
/// sections: e.g., a DEX without interfaces has `proto_ids_size = 0`
/// and `proto_ids_off = 0` (spec permits off=0 iff size=0).
///
/// `checksum` and `signature` are not in this struct — they are
/// computed after the full file bytes exist, by [`finalize_checksums`]
/// (step 12), not here. The header emitted by [`emit_header`] has
/// zero placeholders in those fields.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeaderLayout {
    /// DEX version bytes, e.g. `*b"035"` or `*b"038"`. Controls
    /// feature-gating on ART's side (later versions enable
    /// call-site-id / method-handle sections). Emit does not validate
    /// — caller must match the version to the IR shape being emitted.
    pub version: [u8; 3],
    /// Total file size in bytes.
    pub file_size: u32,
    /// Byte offset of the map_list section.
    pub map_off: u32,
    pub string_ids_size: u32,
    pub string_ids_off: u32,
    pub type_ids_size: u32,
    pub type_ids_off: u32,
    pub proto_ids_size: u32,
    pub proto_ids_off: u32,
    pub field_ids_size: u32,
    pub field_ids_off: u32,
    pub method_ids_size: u32,
    pub method_ids_off: u32,
    pub class_defs_size: u32,
    pub class_defs_off: u32,
    /// Total byte size of the data section (everything after fixed
    /// id-pool sections — strings, code, class-data, annotations,
    /// type_lists, encoded_arrays, debug_info, map_list).
    pub data_size: u32,
    /// Byte offset where the data section begins (first data byte
    /// after class_defs).
    pub data_off: u32,
}

const DEX_HEADER_SIZE: u32 = 112;
const DEX_ENDIAN_TAG: u32 = 0x1234_5678;

/// Emit the DEX header (DEX §7.2).
///
/// Produces exactly 112 bytes. Output has:
///   - bytes 0..8: magic = `b"dex\n" + version + 0x00`
///   - bytes 8..12: checksum = 0 (placeholder; step 12 fills it)
///   - bytes 12..32: signature = [0; 20] (placeholder; step 12 fills it)
///   - bytes 32..36: file_size
///   - bytes 36..40: header_size = 112
///   - bytes 40..44: endian_tag = 0x12345678
///   - bytes 44..48: link_size = 0 (static DEX; unused)
///   - bytes 48..52: link_off = 0
///   - bytes 52..56: map_off
///   - bytes 56..108: per-section size/off pairs in spec order
///   - bytes 108..112: data_size + data_off (wait: data_size @ 104, data_off @ 108)
///
/// Round-trips through [`crate::header::DexHeader::parse`] modulo the
/// placeholder checksum/signature which parse doesn't validate on
/// its own (parse accepts any 32/160-bit value there).
#[allow(
    clippy::as_conversions,
    reason = "PROOF: `DEX_HEADER_SIZE as usize` — DEX_HEADER_SIZE is the compile-time constant 112u32; widening to usize is lossless on all supported platforms."
)]
pub fn emit_header(layout: &HeaderLayout) -> Vec<u8> {
    let mut out = Vec::with_capacity(DEX_HEADER_SIZE as usize);
    // Magic: "dex\n" + version + 0x00 null terminator.
    out.extend_from_slice(b"dex\n");
    out.extend_from_slice(&layout.version);
    out.push(0x00);
    // Checksum placeholder (step 12 fills).
    out.extend_from_slice(&[0u8; 4]);
    // Signature placeholder (step 12 fills).
    out.extend_from_slice(&[0u8; 20]);
    out.extend_from_slice(&layout.file_size.to_le_bytes());
    out.extend_from_slice(&DEX_HEADER_SIZE.to_le_bytes());
    out.extend_from_slice(&DEX_ENDIAN_TAG.to_le_bytes());
    // link_size / link_off: always 0 for static DEX.
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&layout.map_off.to_le_bytes());
    out.extend_from_slice(&layout.string_ids_size.to_le_bytes());
    out.extend_from_slice(&layout.string_ids_off.to_le_bytes());
    out.extend_from_slice(&layout.type_ids_size.to_le_bytes());
    out.extend_from_slice(&layout.type_ids_off.to_le_bytes());
    out.extend_from_slice(&layout.proto_ids_size.to_le_bytes());
    out.extend_from_slice(&layout.proto_ids_off.to_le_bytes());
    out.extend_from_slice(&layout.field_ids_size.to_le_bytes());
    out.extend_from_slice(&layout.field_ids_off.to_le_bytes());
    out.extend_from_slice(&layout.method_ids_size.to_le_bytes());
    out.extend_from_slice(&layout.method_ids_off.to_le_bytes());
    out.extend_from_slice(&layout.class_defs_size.to_le_bytes());
    out.extend_from_slice(&layout.class_defs_off.to_le_bytes());
    out.extend_from_slice(&layout.data_size.to_le_bytes());
    out.extend_from_slice(&layout.data_off.to_le_bytes());
    debug_assert_eq!(
        out.len(),
        DEX_HEADER_SIZE as usize,
        "emit_header produced {} bytes, expected {DEX_HEADER_SIZE}",
        out.len()
    );
    out
}

/// Finalize the header checksum + signature over a fully-assembled
/// DEX file, in place. Step 12 of the emit pipeline — the capstone
/// that makes the output bytes VM-loadable.
///
/// ## Spec (DEX §7.2, verified against AOSP dalvik/libdex)
///
/// Both hash fields are computed over a *trailing window* of the file
/// — the hash algorithms never see their own bytes:
///
/// ```text
/// signature = SHA-1( file[32..file_size] )         → written to file[12..32]
/// checksum  = Adler-32( file[12..file_size] )      → written to file[8..12]
/// ```
///
/// Order matters: signature is computed from zero-placeholder bytes
/// at `file[12..32]` (per emit_header) and written first; the checksum
/// window `file[12..]` then *includes* the just-written signature, so
/// adler must run after signature lands.
///
/// ## Errors
///
/// - `OffsetOverflow` if `bytes.len() < 32` (file too short to hold
///   even the header magic+checksum+signature fields).
pub fn finalize_checksums(bytes: &mut [u8]) -> Result<(), DexEmitError> {
    use sha1::{Digest, Sha1};

    if bytes.len() < 32 {
        return Err(DexEmitError::OffsetOverflow {
            section: "header",
            context: "finalize_checksums: file shorter than 32 bytes (can't fit magic + checksum + signature fields)",
        });
    }

    // Step 1 — SHA-1 over bytes[32..].
    let mut sha = Sha1::new();
    sha.update(&bytes[32..]);
    let digest = sha.finalize();
    bytes[12..32].copy_from_slice(&digest);

    // Step 2 — Adler-32 over bytes[12..] (now includes the SHA-1).
    let checksum = adler2::adler32_slice(&bytes[12..]);
    bytes[8..12].copy_from_slice(&checksum.to_le_bytes());

    Ok(())
}

// ── map_list (DEX §7.18) ───────────────────────────────────────────
//
// The map_list is a top-level index of every section in the DEX file.
// Its offset is stored in the header's `map_off` field. The VM reads
// the map as an alternative discovery path (the main access path is
// the header's per-section offset fields). Loaders treat the two as
// redundant and check consistency.
//
// The droidsaw parser skips map_list entirely — the header suffices.
// For emit, the map_list must still be produced so the file is spec-
// conformant and ART's optimization passes (which DO consult the map)
// see a correct layout. This section has no round-trip invariant to
// check at the IR level; emit generates it from the assembly layer's
// knowledge of what sections went into the output.

/// Numeric type codes for `map_item.type`, per DEX spec §7.18 Table
/// "Type codes". One entry per section kind that can appear in a DEX.
/// Codes are grouped into three ranges by section family:
///   0x0000–0x0008  header + id-pool items
///   0x1000–0x1003  list-type items (including map_list itself)
///   0x2000–0x2006  data-section items
pub mod map_type {
    #![allow(missing_docs, reason = "internal — each constant is self-documenting by hex code-comment.")]
    pub const HEADER_ITEM: u16 = 0x0000;
    pub const STRING_ID_ITEM: u16 = 0x0001;
    pub const TYPE_ID_ITEM: u16 = 0x0002;
    pub const PROTO_ID_ITEM: u16 = 0x0003;
    pub const FIELD_ID_ITEM: u16 = 0x0004;
    pub const METHOD_ID_ITEM: u16 = 0x0005;
    pub const CLASS_DEF_ITEM: u16 = 0x0006;
    pub const CALL_SITE_ID_ITEM: u16 = 0x0007;
    pub const METHOD_HANDLE_ITEM: u16 = 0x0008;
    pub const MAP_LIST: u16 = 0x1000;
    pub const TYPE_LIST: u16 = 0x1001;
    pub const ANNOTATION_SET_REF_LIST: u16 = 0x1002;
    pub const ANNOTATION_SET_ITEM: u16 = 0x1003;
    pub const CLASS_DATA_ITEM: u16 = 0x2000;
    pub const CODE_ITEM: u16 = 0x2001;
    pub const STRING_DATA_ITEM: u16 = 0x2002;
    pub const DEBUG_INFO_ITEM: u16 = 0x2003;
    pub const ANNOTATION_ITEM: u16 = 0x2004;
    pub const ENCODED_ARRAY_ITEM: u16 = 0x2005;
    pub const ANNOTATION_DIRECTORY_ITEM: u16 = 0x2006;
}

/// One entry in the `map_list` — describes the location, count, and
/// kind of a single section within the DEX file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MapItem {
    /// Section type code (see [`map_type`]).
    pub type_code: u16,
    /// Number of items in the section. For singleton sections
    /// (`HEADER_ITEM`, `MAP_LIST`) this is always 1.
    pub size: u32,
    /// Byte offset from the start of the DEX file.
    pub offset: u32,
}

/// Emit a `map_list` (DEX §7.18):
///
/// ```text
/// map_list {
///     u32 size;                    // number of map_item entries
///     map_item list[size];
/// }
///
/// map_item {
///     u16 type;                    // section type code
///     u16 unused;                  // padding, must be zero
///     u32 size;                    // items in this section
///     u32 offset;                  // byte offset into file
/// }
/// ```
///
/// Each `map_item` is 12 bytes. Total size: 4 + 12*N bytes.
///
/// # Ordering invariant (spec §7.18)
///
/// Entries must be in ascending order by `offset` — the map mirrors
/// the file's physical section layout. This function validates that
/// invariant and returns `UnrepresentableIR` on violation (duplicate
/// or descending offsets). Sections of size 0 should typically be
/// omitted; emitting a zero-size map_item is legal but unusual and
/// not validated here (the assembly layer decides inclusion).
///
/// `permit_unsorted = true` skips the ascending-offset validator. This
/// is used under `EmitConfig::preserve_map_list_order` for byte-identity
/// roundtrips on inputs whose `map_list` is in build-order (d8/dexopt
/// emit map_items in build-order even though the spec mandates offset-
/// order; both are accepted in practice). Default is `false`.
pub fn emit_map_list(items: &[MapItem], permit_unsorted: bool) -> Result<Vec<u8>, DexEmitError> {
    let size = u32::try_from(items.len()).map_err(|_| DexEmitError::SizeOverflow {
        layer: "map_list",
        context: "map_item count exceeds u32",
    })?;
    if !permit_unsorted {
        for (i, w) in items.windows(2).enumerate() {
            if w[0].offset >= w[1].offset {
                return Err(DexEmitError::UnrepresentableIR {
                    why: "map_list: items must be strictly ascending by offset (DEX §7.18); duplicate or descending offset rejected",
                });
            }
            let _ = i;
        }
    }
    let mut out = Vec::with_capacity(alloc_cap(
        4usize.saturating_add(items.len().saturating_mul(12)),
    ));
    out.extend_from_slice(&size.to_le_bytes());
    for m in items {
        out.extend_from_slice(&m.type_code.to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes()); // unused/padding
        out.extend_from_slice(&m.size.to_le_bytes());
        out.extend_from_slice(&m.offset.to_le_bytes());
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_sorted_empty() {
        let nd: NonDecreasing<u32> = NonDecreasing::from_sorted(vec![]);
        assert_eq!(nd.len(), 0);
    }

    #[test]
    fn from_sorted_already_sorted() {
        let nd = NonDecreasing::from_sorted(vec![1_u32, 2, 3, 5]);
        assert_eq!(&*nd, &[1, 2, 3, 5]);
    }

    #[test]
    fn from_sorted_reverses_unsorted() {
        let nd = NonDecreasing::from_sorted(vec![5_u32, 1, 3, 2]);
        assert_eq!(&*nd, &[1, 2, 3, 5]);
    }

    #[test]
    fn from_sorted_preserves_duplicates() {
        let nd = NonDecreasing::from_sorted(vec![2_u32, 1, 2, 1]);
        assert_eq!(&*nd, &[1, 1, 2, 2]);
    }

    #[test]
    fn from_verified_accepts_canonical() {
        let nd = NonDecreasing::from_verified(vec![1_u32, 2, 3]).expect("canonical");
        assert_eq!(&*nd, &[1, 2, 3]);
    }

    #[test]
    fn from_verified_rejects_descending() {
        let err = NonDecreasing::from_verified(vec![3_u32, 2, 1]).unwrap_err();
        assert_eq!(err.index, 1, "first violation at index 1 (3 > 2)");
    }

    #[test]
    fn from_verified_reports_first_violation() {
        let err = NonDecreasing::from_verified(vec![1_u32, 2, 3, 0, 5]).unwrap_err();
        assert_eq!(err.index, 3, "3 > 0 violates at index 3");
    }

    #[test]
    fn deref_to_slice_works() {
        let nd = NonDecreasing::from_sorted(vec![1_u32, 2, 3]);
        let s: &[u32] = &nd;
        assert_eq!(s, &[1, 2, 3]);
        assert!(!nd.is_empty());
        assert_eq!(nd.first(), Some(&1));
        assert_eq!(nd.last(), Some(&3));
    }

    #[test]
    fn iter_borrows_in_order() {
        let nd = NonDecreasing::from_sorted(vec![3_u32, 1, 2]);
        let collected: Vec<u32> = nd.iter().copied().collect();
        assert_eq!(collected, vec![1, 2, 3]);
    }

    // ── write_uleb128 ───────────────────────────────────────────────

    #[test]
    fn uleb128_single_byte_boundaries() {
        let mut out = Vec::new();
        write_uleb128(&mut out, 0);
        assert_eq!(out, vec![0x00]);
        out.clear();
        write_uleb128(&mut out, 0x7F);
        assert_eq!(out, vec![0x7F], "last single-byte value");
    }

    #[test]
    fn uleb128_two_byte_boundary() {
        let mut out = Vec::new();
        write_uleb128(&mut out, 0x80);
        assert_eq!(out, vec![0x80, 0x01], "first two-byte value");
        out.clear();
        write_uleb128(&mut out, 0x3FFF);
        assert_eq!(out, vec![0xFF, 0x7F], "last two-byte value");
    }

    #[test]
    fn uleb128_five_byte_max() {
        let mut out = Vec::new();
        write_uleb128(&mut out, u32::MAX);
        // u32::MAX = 0xFFFFFFFF encodes to 5 bytes: FF FF FF FF 0F
        assert_eq!(out, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
    }

    #[test]
    fn uleb128_roundtrip_via_common_decoder() {
        // Every value we write, droidsaw_common::encoding::read_uleb128
        // should decode to the same value.
        for v in [0_u32, 1, 0x7F, 0x80, 0x3FFF, 0x4000, 0x1FFFFF, 0x200000, 0xFFFFFFF, u32::MAX] {
            let mut buf = Vec::new();
            write_uleb128(&mut buf, v);
            let (decoded, _len) =
                droidsaw_common::encoding::read_uleb128(&buf, 0).expect("well-formed");
            assert_eq!(decoded, v, "roundtrip for {v:#x}");
        }
    }

    // ── encode_mutf8 ────────────────────────────────────────────────

    #[test]
    fn mutf8_ascii_identity() {
        let mut out = Vec::new();
        encode_mutf8_into(&mut out, "hello");
        assert_eq!(out, b"hello");
    }

    #[test]
    fn mutf8_null_is_two_bytes() {
        let mut out = Vec::new();
        encode_mutf8_into(&mut out, "\0");
        assert_eq!(out, vec![0xC0, 0x80], "NUL is encoded as C0 80, not 00");
    }

    #[test]
    fn mutf8_two_byte_char() {
        // U+00E9 (é) encodes as 0xC3 0xA9 in UTF-8 and MUTF-8 alike.
        let mut out = Vec::new();
        encode_mutf8_into(&mut out, "é");
        assert_eq!(out, vec![0xC3, 0xA9]);
    }

    #[test]
    fn mutf8_three_byte_char() {
        // U+4E2D (中) encodes as 0xE4 0xB8 0xAD.
        let mut out = Vec::new();
        encode_mutf8_into(&mut out, "中");
        assert_eq!(out, vec![0xE4, 0xB8, 0xAD]);
    }

    #[test]
    fn mutf8_supplementary_is_surrogate_pair() {
        // U+1F600 (😀) encodes in UTF-8 as 4 bytes (F0 9F 98 80); in
        // MUTF-8 as two 3-byte surrogate-half encodings (high = U+D83D,
        // low = U+DE00).
        let mut out = Vec::new();
        encode_mutf8_into(&mut out, "😀");
        assert_eq!(out.len(), 6, "2x 3-byte surrogates, NOT 4-byte UTF-8");
        // U+D83D = 11011000_00111101 → E0|0D D|83 8|3D → ED A0 BD
        assert_eq!(out[..3], [0xED, 0xA0, 0xBD]);
        // U+DE00 = 11011110_00000000 → ED B8 80
        assert_eq!(out[3..], [0xED, 0xB8, 0x80]);
    }

    #[test]
    fn mutf8_roundtrip_through_common_decoder() {
        for s in &["", "a", "hello world", "é", "中", "😀", "mixed: a\0b中"] {
            let mut buf = Vec::new();
            encode_mutf8_into(&mut buf, s);
            let decoded =
                droidsaw_common::encoding::decode_mutf8(&buf).expect("well-formed MUTF-8");
            assert_eq!(
                decoded, *s,
                "encode → decode round trip on {s:?} ({} bytes)",
                buf.len()
            );
        }
    }

    #[test]
    fn utf16_unit_count_matches_java_semantics() {
        assert_eq!(utf16_unit_count(""), 0);
        assert_eq!(utf16_unit_count("abc"), 3);
        assert_eq!(utf16_unit_count("é"), 1, "BMP char = 1 utf16 unit");
        assert_eq!(utf16_unit_count("中"), 1, "BMP char = 1 utf16 unit");
        assert_eq!(
            utf16_unit_count("😀"),
            2,
            "supplementary char = 2 utf16 units (surrogate pair)"
        );
        assert_eq!(utf16_unit_count("a😀b"), 4, "1 + 2 + 1");
    }

    // ── emit_string_pool ────────────────────────────────────────────

    /// Helper: convert `&[&str]` (pre-sorted by caller) to the
    /// raw-MUTF-8 shape that `emit_string_pool` now takes. Owned
    /// `Vec<Vec<u8>>` storage keeps the byte buffers alive while the
    /// caller passes a `Vec<&[u8]>` view.
    fn to_raw_bytes(strings: &[&str]) -> Vec<Vec<u8>> {
        strings
            .iter()
            .map(|s| {
                let mut b = Vec::new();
                encode_mutf8_into(&mut b, s);
                b
            })
            .collect()
    }

    fn as_slice_refs(raw: &[Vec<u8>]) -> Vec<&[u8]> {
        raw.iter().map(Vec::as_slice).collect()
    }

    #[test]
    fn emit_string_pool_empty() {
        let (ids, data) = emit_string_pool(&[]);
        assert!(ids.is_empty());
        assert!(data.is_empty());
    }

    #[test]
    fn emit_string_pool_single_ascii() {
        let raw = to_raw_bytes(&["hi"]);
        let (ids, data) = emit_string_pool(&as_slice_refs(&raw));
        // id_items: one u32 LE = 0 (first string_data_item at start of data section)
        assert_eq!(ids, vec![0x00, 0x00, 0x00, 0x00]);
        // data: ULEB128(2) = 0x02, then "hi" (0x68 0x69), then 0x00 terminator.
        assert_eq!(data, vec![0x02, 0x68, 0x69, 0x00]);
    }

    #[test]
    fn emit_string_pool_three_ascii_canonical_order() {
        // Caller responsible for canonical order; pass pre-sorted.
        let raw = to_raw_bytes(&["apple", "bat", "cat"]);
        let (ids, data) = emit_string_pool(&as_slice_refs(&raw));

        // id_items: 3 entries × 4 bytes = 12 bytes.
        assert_eq!(ids.len(), 12);

        // First offset = 0.
        assert_eq!(&ids[0..4], &[0x00, 0x00, 0x00, 0x00]);
        // Second offset = 7 (apple: 1-byte size prefix + 5 bytes "apple" + 1 NUL = 7)
        assert_eq!(&ids[4..8], &[0x07, 0x00, 0x00, 0x00]);
        // Third offset = 7 + 5 (bat: 1 + 3 + 1 = 5) = 12
        assert_eq!(&ids[8..12], &[0x0C, 0x00, 0x00, 0x00]);

        // Data reads: ULEB(5) "apple" 00 | ULEB(3) "bat" 00 | ULEB(3) "cat" 00.
        assert_eq!(
            data,
            vec![
                0x05, 0x61, 0x70, 0x70, 0x6C, 0x65, 0x00, // "apple"
                0x03, 0x62, 0x61, 0x74, 0x00, // "bat"
                0x03, 0x63, 0x61, 0x74, 0x00, // "cat"
            ]
        );
    }

    #[test]
    fn emit_string_pool_with_null_byte_uses_mutf8_c080() {
        let raw = to_raw_bytes(&["a\0b"]);
        let (_ids, data) = emit_string_pool(&as_slice_refs(&raw));
        // utf16_size: 3 code units ('a', NUL, 'b')
        // MUTF-8 body: 0x61 | 0xC0 0x80 | 0x62 = 4 bytes
        // Plus terminator: 5 bytes
        // Plus ULEB(3) prefix: 1 byte → 6 bytes total.
        assert_eq!(data, vec![0x03, 0x61, 0xC0, 0x80, 0x62, 0x00]);
    }

    // ── emit_type_pool ──────────────────────────────────────────────

    #[test]
    fn emit_type_pool_empty() {
        let nd: NonDecreasing<StringIdx> = NonDecreasing::from_sorted(vec![]);
        assert!(emit_type_pool(&nd).is_empty());
    }

    #[test]
    fn emit_type_pool_single_entry() {
        let nd = NonDecreasing::from_sorted(vec![StringIdx(0x42)]);
        // Single u32 LE = 0x42 00 00 00
        assert_eq!(emit_type_pool(&nd), vec![0x42, 0x00, 0x00, 0x00]);
    }

    #[test]
    fn emit_type_pool_sorted_output() {
        // from_sorted canonicalizes input: unsorted [5, 1, 3] → [1, 3, 5].
        let nd = NonDecreasing::from_sorted(vec![StringIdx(5), StringIdx(1), StringIdx(3)]);
        let out = emit_type_pool(&nd);
        assert_eq!(
            out,
            vec![
                0x01, 0x00, 0x00, 0x00, //
                0x03, 0x00, 0x00, 0x00, //
                0x05, 0x00, 0x00, 0x00,
            ],
            "output in canonical ascending order"
        );
    }

    #[test]
    fn emit_type_pool_stride_is_four() {
        let nd = NonDecreasing::from_sorted(vec![
            StringIdx(0),
            StringIdx(0x1234),
            StringIdx(0xFFFF_FFFE),
            StringIdx(0xFFFF_FFFF),
        ]);
        let out = emit_type_pool(&nd);
        assert_eq!(out.len(), 16, "4 entries × 4 bytes");
        // Last entry = u32::MAX = FF FF FF FF.
        assert_eq!(&out[12..16], &[0xFF, 0xFF, 0xFF, 0xFF]);
    }

    // ── emit_type_list ──────────────────────────────────────────────

    #[test]
    fn emit_type_list_empty() {
        let out = emit_type_list(&[]).expect("empty list is well-formed");
        // u32 size = 0, no entries.
        assert_eq!(out, vec![0x00, 0x00, 0x00, 0x00]);
    }

    #[test]
    fn emit_type_list_single_entry() {
        let out = emit_type_list(&[TypeIdx(0x0042)]).expect("well-formed");
        assert_eq!(
            out,
            vec![
                0x01, 0x00, 0x00, 0x00, // size = 1
                0x42, 0x00, //             u16 type_item = 0x0042
            ]
        );
    }

    #[test]
    fn emit_type_list_three_entries_layout() {
        let out = emit_type_list(&[TypeIdx(0x0001), TypeIdx(0x00FF), TypeIdx(0xABCD)])
            .expect("well-formed");
        assert_eq!(
            out,
            vec![
                0x03, 0x00, 0x00, 0x00, // size = 3
                0x01, 0x00, //             0x0001
                0xFF, 0x00, //             0x00FF
                0xCD, 0xAB, //             0xABCD
            ]
        );
        assert_eq!(out.len(), 4 + 3 * 2, "header + size×2 bytes");
    }

    #[test]
    fn emit_type_list_u16_boundary_ok() {
        let out = emit_type_list(&[TypeIdx(u32::from(u16::MAX))]).expect("u16::MAX is representable");
        assert_eq!(&out[4..6], &[0xFF, 0xFF], "u16::MAX encoded");
    }

    #[test]
    fn emit_type_list_u16_overflow_returns_err() {
        let err = emit_type_list(&[TypeIdx(u32::from(u16::MAX) + 1)]).unwrap_err();
        match err {
            DexEmitError::UnrepresentableIR { why } => {
                assert!(why.contains("type_list"), "why should name the section: {why}");
                assert!(why.contains("u16"), "why should name the narrowing: {why}");
            }
            other => panic!("expected UnrepresentableIR, got {other:?}"),
        }
    }

    // ── emit_proto_pool ─────────────────────────────────────────────

    #[test]
    fn emit_proto_pool_empty() {
        assert!(emit_proto_pool(&[]).is_empty());
    }

    #[test]
    fn emit_proto_pool_single_entry_stride() {
        let proto = ProtoIdItem {
            shorty_idx: StringIdx(0x11),
            return_type_idx: TypeIdx(0x22),
            parameters_off: 0x4000,
        };
        let out = emit_proto_pool(&[proto]);
        assert_eq!(out.len(), 12, "one proto_id_item = 12 bytes");
        assert_eq!(
            out,
            vec![
                0x11, 0x00, 0x00, 0x00, // shorty_idx
                0x22, 0x00, 0x00, 0x00, // return_type_idx
                0x00, 0x40, 0x00, 0x00, // parameters_off = 0x4000
            ]
        );
    }

    #[test]
    fn emit_proto_pool_multiple_entries_stride() {
        let protos = vec![
            ProtoIdItem {
                shorty_idx: StringIdx(0),
                return_type_idx: TypeIdx(0),
                parameters_off: 0,
            },
            ProtoIdItem {
                shorty_idx: StringIdx(1),
                return_type_idx: TypeIdx(1),
                parameters_off: 0x100,
            },
            ProtoIdItem {
                shorty_idx: StringIdx(2),
                return_type_idx: TypeIdx(3),
                parameters_off: 0x200,
            },
        ];
        let out = emit_proto_pool(&protos);
        assert_eq!(out.len(), 3 * 12, "3 entries × 12 bytes stride");
        // Verify second entry fields are at offsets [12..24].
        assert_eq!(&out[12..16], &[0x01, 0x00, 0x00, 0x00]);
        assert_eq!(&out[16..20], &[0x01, 0x00, 0x00, 0x00]);
        assert_eq!(&out[20..24], &[0x00, 0x01, 0x00, 0x00]);
    }

    // ── emit_field_pool ─────────────────────────────────────────────

    #[test]
    fn emit_field_pool_empty() {
        assert_eq!(emit_field_pool(&[]).expect("empty"), Vec::<u8>::new());
    }

    #[test]
    fn emit_field_pool_single_entry_layout() {
        let f = FieldIdItem {
            class_idx: TypeIdx(0x00AB),
            type_idx: TypeIdx(0x00CD),
            name_idx: StringIdx(0xDEADBEEF),
        };
        let out = emit_field_pool(&[f]).expect("well-formed");
        assert_eq!(out.len(), 8, "one field_id_item = 8 bytes");
        assert_eq!(
            out,
            vec![
                0xAB, 0x00, // class_idx u16
                0xCD, 0x00, // type_idx u16
                0xEF, 0xBE, 0xAD, 0xDE, // name_idx u32 LE
            ]
        );
    }

    #[test]
    fn emit_field_pool_multi_entry_stride() {
        let fs = (0u32..4)
            .map(|i| FieldIdItem {
                class_idx: TypeIdx(i),
                type_idx: TypeIdx(i + 1),
                name_idx: StringIdx(i + 100),
            })
            .collect::<Vec<_>>();
        let out = emit_field_pool(&fs).expect("well-formed");
        assert_eq!(out.len(), 4 * 8, "4 entries × 8 bytes");
    }

    #[test]
    fn emit_field_pool_class_idx_overflow() {
        let f = FieldIdItem {
            class_idx: TypeIdx(u32::from(u16::MAX) + 1),
            type_idx: TypeIdx(0),
            name_idx: StringIdx(0),
        };
        let err = emit_field_pool(&[f]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("class_idx")),
            "class_idx overflow should surface as UnrepresentableIR with 'class_idx' in context"
        );
    }

    #[test]
    fn emit_field_pool_type_idx_overflow() {
        let f = FieldIdItem {
            class_idx: TypeIdx(0),
            type_idx: TypeIdx(u32::from(u16::MAX) + 1),
            name_idx: StringIdx(0),
        };
        let err = emit_field_pool(&[f]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("type_idx")),
            "type_idx overflow should surface as UnrepresentableIR with 'type_idx' in context"
        );
    }

    // ── emit_method_pool ────────────────────────────────────────────

    #[test]
    fn emit_method_pool_empty() {
        assert_eq!(emit_method_pool(&[]).expect("empty"), Vec::<u8>::new());
    }

    #[test]
    fn emit_method_pool_single_entry_layout() {
        let m = MethodIdItem {
            class_idx: TypeIdx(0x1234),
            proto_idx: crate::ids::ProtoIdx(0x5678),
            name_idx: StringIdx(0xCAFEBABE),
        };
        let out = emit_method_pool(&[m]).expect("well-formed");
        assert_eq!(out.len(), 8);
        assert_eq!(
            out,
            vec![
                0x34, 0x12, // class_idx u16 LE
                0x78, 0x56, // proto_idx u16 LE
                0xBE, 0xBA, 0xFE, 0xCA, // name_idx u32 LE
            ]
        );
    }

    #[test]
    fn emit_method_pool_class_idx_overflow() {
        let m = MethodIdItem {
            class_idx: TypeIdx(u32::from(u16::MAX) + 1),
            proto_idx: crate::ids::ProtoIdx(0),
            name_idx: StringIdx(0),
        };
        let err = emit_method_pool(&[m]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("class_idx")),
            "class_idx overflow should name class_idx"
        );
    }

    #[test]
    fn emit_method_pool_proto_idx_overflow() {
        let m = MethodIdItem {
            class_idx: TypeIdx(0),
            proto_idx: crate::ids::ProtoIdx(u32::from(u16::MAX) + 1),
            name_idx: StringIdx(0),
        };
        let err = emit_method_pool(&[m]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("proto_idx")),
            "proto_idx overflow should name proto_idx"
        );
    }

    // ── emit_class_def_pool ─────────────────────────────────────────

    #[test]
    fn emit_class_def_pool_empty() {
        assert!(emit_class_def_pool(&[]).expect("empty").is_empty());
    }

    #[test]
    fn emit_class_def_pool_full_fields_layout() {
        let c = ClassDefItem {
            class_idx: TypeIdx(0x0000_0011),
            access_flags: 0x0000_0001, // ACC_PUBLIC
            superclass_idx: Some(TypeIdx(0x0000_0022)),
            interfaces_off: 0x0000_0100,
            source_file_idx: Some(StringIdx(0x0000_0033)),
            annotations_off: 0x0000_0200,
            class_data_off: 0x0000_0300,
            static_values_off: 0x0000_0400,
        };
        let out = emit_class_def_pool(&[c]).expect("well-formed");
        assert_eq!(out.len(), 32, "one class_def_item = 32 bytes");
        assert_eq!(
            out,
            vec![
                0x11, 0x00, 0x00, 0x00, // class_idx
                0x01, 0x00, 0x00, 0x00, // access_flags = ACC_PUBLIC
                0x22, 0x00, 0x00, 0x00, // superclass_idx
                0x00, 0x01, 0x00, 0x00, // interfaces_off
                0x33, 0x00, 0x00, 0x00, // source_file_idx
                0x00, 0x02, 0x00, 0x00, // annotations_off
                0x00, 0x03, 0x00, 0x00, // class_data_off
                0x00, 0x04, 0x00, 0x00, // static_values_off
            ]
        );
    }

    #[test]
    fn emit_class_def_pool_optional_fields_become_no_index() {
        let c = ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        };
        let out = emit_class_def_pool(&[c]).expect("well-formed");
        // superclass_idx at bytes [8..12], source_file_idx at [16..20].
        assert_eq!(&out[8..12], &[0xFF, 0xFF, 0xFF, 0xFF], "None → NO_INDEX");
        assert_eq!(&out[16..20], &[0xFF, 0xFF, 0xFF, 0xFF], "None → NO_INDEX");
    }

    #[test]
    fn emit_class_def_pool_class_idx_no_index_is_rejected() {
        let c = ClassDefItem {
            class_idx: TypeIdx(NO_INDEX),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        };
        let err = emit_class_def_pool(&[c]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("NO_INDEX")),
            "class_idx = NO_INDEX must be rejected as unrepresentable"
        );
    }

    #[test]
    fn emit_class_def_pool_superclass_idx_some_no_index_is_rejected() {
        // Some(TypeIdx(NO_INDEX)) collides with None after emit (both
        // write NO_INDEX). Reject at emit to prevent silent merge of
        // the two semantic states.
        let c = ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: Some(TypeIdx(NO_INDEX)),
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        };
        let err = emit_class_def_pool(&[c]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("superclass_idx")),
            "Some(TypeIdx(NO_INDEX)) sentinel collision must be rejected"
        );
    }

    #[test]
    fn emit_class_def_pool_source_file_idx_some_no_index_is_rejected() {
        let c = ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: Some(StringIdx(NO_INDEX)),
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        };
        let err = emit_class_def_pool(&[c]).unwrap_err();
        assert!(
            matches!(err, DexEmitError::UnrepresentableIR { why } if why.contains("source_file_idx")),
            "Some(StringIdx(NO_INDEX)) sentinel collision must be rejected"
        );
    }

    // Iterative walker tests — covers deeply-nested input via a
    // heap-allocated work-stack up to PARSE_STACK_FRAME_CAP frames
    // (rather than rejecting via a depth cap).

    /// Parse + emit 10 000-level deeply-nested Array: must succeed without
    /// stack overflow using the iterative walkers.  Run in a thread with a
    /// 64 MiB stack to give the test harness enough headroom; the walkers
    /// themselves use O(depth) heap frames, not O(depth) call-stack frames.
    #[test]
    fn iterative_walkers_handle_deeply_nested_array() {
        use crate::annotation::EncodedValue;

        // Run on a large-stack thread so that the test harness itself
        // (which uses recursive Debug / Drop for the EncodedValue enum)
        // does not overflow.  The walkers use O(depth) *heap* frames.
        const DEPTH: usize = 10_000;
        const STACK_SIZE: usize = 64 * 1024 * 1024; // 64 MiB

        std::thread::Builder::new()
            .stack_size(STACK_SIZE)
            .spawn(move || {
                // ── parse path ──────────────────────────────────────────
                let mut buf = Vec::new();
                for _ in 0..DEPTH {
                    buf.push(0x1c); // VALUE_ARRAY
                    buf.push(0x01); // ULEB128 count = 1
                }
                buf.push(0x00); // VALUE_BYTE header (value_arg=0)
                buf.push(0x00); // byte payload = 0

                let (parsed_val, consumed) =
                    crate::annotation::parse_encoded_value(&buf, 0)
                        .expect("10 000-level nested Array must parse with iterative walker");
                assert_eq!(
                    consumed,
                    buf.len(),
                    "consumed must equal buffer length"
                );

                // Unpack nesting iteratively to avoid recursive Debug.
                let mut cur = parsed_val;
                for i in 0..DEPTH {
                    match cur {
                        EncodedValue::Array(mut children) => {
                            assert_eq!(children.len(), 1, "level {i}: expected single child");
                            cur = children.pop().expect("checked len=1");
                        }
                        _ => panic!("level {i}: expected Array"),
                    }
                }
                assert!(
                    matches!(cur, EncodedValue::Byte(0)),
                    "innermost must be Byte(0)"
                );

                // ── emit path ───────────────────────────────────────────
                // Build IR and emit; verify consumed == emitted length.
                let mut val = EncodedValue::Byte(0);
                for _ in 0..DEPTH {
                    val = EncodedValue::Array(vec![val]);
                }
                let mut emitted = Vec::new();
                emit_encoded_value(&mut emitted, &val)
                    .expect("10 000-level nested Array must emit with iterative walker");

                // Parse back and spot-check length.
                let (_, re_consumed) =
                    crate::annotation::parse_encoded_value(&emitted, 0)
                        .expect("emitted bytes must parse back");
                assert_eq!(
                    re_consumed,
                    emitted.len(),
                    "re-consumed must equal emitted length"
                );

                // Byte-level: parse-of-encode must reproduce the original bytes.
                assert_eq!(emitted, buf, "emitted bytes must match hand-built bytes");
            })
            .expect("thread spawn")
            .join()
            .expect("thread join");
    }

    #[test]
    fn emit_class_def_pool_stride_multi_entry() {
        let classes: Vec<_> = (0u32..3)
            .map(|i| ClassDefItem {
                class_idx: TypeIdx(i),
                access_flags: 0,
                superclass_idx: None,
                interfaces_off: 0,
                source_file_idx: None,
                annotations_off: 0,
                class_data_off: 0,
                static_values_off: 0,
            })
            .collect();
        let out = emit_class_def_pool(&classes).expect("well-formed");
        assert_eq!(out.len(), 3 * 32, "3 entries × 32-byte stride");
    }

    // ── min_signed_bytes / min_unsigned_bytes ───────────────────────

    #[test]
    fn min_signed_bytes_boundaries() {
        // Single-byte range: [-128, 127]
        assert_eq!(min_signed_bytes(0), 1);
        assert_eq!(min_signed_bytes(-1), 1);
        assert_eq!(min_signed_bytes(127), 1);
        assert_eq!(min_signed_bytes(-128), 1);
        // Needs 2 bytes:
        assert_eq!(min_signed_bytes(128), 2, "128 doesn't fit in i8");
        assert_eq!(min_signed_bytes(-129), 2);
        assert_eq!(min_signed_bytes(32_767), 2, "i16::MAX");
        assert_eq!(min_signed_bytes(-32_768), 2, "i16::MIN");
        // 3 / 4 / 8 bytes:
        assert_eq!(min_signed_bytes(32_768), 3);
        assert_eq!(min_signed_bytes(2_147_483_647), 4, "i32::MAX");
        assert_eq!(min_signed_bytes(-2_147_483_648), 4, "i32::MIN");
        assert_eq!(min_signed_bytes(i64::MAX), 8);
        assert_eq!(min_signed_bytes(i64::MIN), 8);
    }

    #[test]
    fn min_unsigned_bytes_boundaries() {
        assert_eq!(min_unsigned_bytes(0), 1);
        assert_eq!(min_unsigned_bytes(1), 1);
        assert_eq!(min_unsigned_bytes(0xFF), 1);
        assert_eq!(min_unsigned_bytes(0x100), 2);
        assert_eq!(min_unsigned_bytes(0xFFFF), 2);
        assert_eq!(min_unsigned_bytes(0x10000), 3);
        assert_eq!(min_unsigned_bytes(0xFFFFFFFF), 4);
        assert_eq!(min_unsigned_bytes(0x100000000), 5);
        assert_eq!(min_unsigned_bytes(u64::MAX), 8);
    }

    // ── emit_encoded_value — primitive variants ─────────────────────

    #[test]
    fn encoded_value_byte() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Byte(-1)).unwrap();
        assert_eq!(out, vec![0x00, 0xFF], "VT_BYTE header, byte payload -1");
    }

    #[test]
    fn encoded_value_null() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Null).unwrap();
        assert_eq!(out, vec![0x1E], "VT_NULL: header only");
    }

    #[test]
    fn encoded_value_boolean_true_and_false() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Boolean(false)).unwrap();
        assert_eq!(out, vec![0x1F], "VT_BOOLEAN false: header only, value_arg=0");
        out.clear();
        emit_encoded_value(&mut out, &EncodedValue::Boolean(true)).unwrap();
        assert_eq!(out, vec![0x3F], "VT_BOOLEAN true: header | (1<<5) = 0x1F|0x20");
    }

    #[test]
    fn encoded_value_int_minimum_width_positive() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Int(0x7F)).unwrap();
        // size=1 → header=VT_INT|0 = 0x04; payload 0x7F.
        assert_eq!(out, vec![0x04, 0x7F]);
    }

    #[test]
    fn encoded_value_int_minimum_width_negative() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Int(-1)).unwrap();
        // -1 fits in 1 byte as 0xFF (sign-extends to -1).
        assert_eq!(out, vec![0x04, 0xFF]);
    }

    #[test]
    fn encoded_value_int_two_byte() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Int(0x123)).unwrap();
        // 0x123 in 2 bytes LE = 0x23 0x01; header = 0x04 | (1 << 5) = 0x24.
        assert_eq!(out, vec![0x24, 0x23, 0x01]);
    }

    #[test]
    fn encoded_value_long_max_width() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Long(i64::MIN)).unwrap();
        // i64::MIN needs full 8 bytes; header = 0x06 | (7 << 5) = 0xE6.
        assert_eq!(out[0], 0xE6);
        assert_eq!(out.len(), 9);
    }

    #[test]
    fn encoded_value_float_always_full_width() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Float(1.0)).unwrap();
        // header = VT_FLOAT | (3 << 5) = 0x10 | 0x60 = 0x70; 4 bytes LE.
        assert_eq!(out[0], 0x70);
        assert_eq!(out.len(), 5);
        // 1.0f32 = 0x3F800000 → LE bytes [00 00 80 3F]
        assert_eq!(&out[1..5], &[0x00, 0x00, 0x80, 0x3F]);
    }

    #[test]
    fn encoded_value_double_always_full_width() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Double(0.0)).unwrap();
        // header = VT_DOUBLE | (7 << 5) = 0x11 | 0xE0 = 0xF1; 8 bytes.
        assert_eq!(out[0], 0xF1);
        assert_eq!(out.len(), 9);
    }

    #[test]
    fn encoded_value_string_idx() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::String(StringIdx(0xABCD))).unwrap();
        // 0xABCD needs 2 bytes; header = VT_STRING | (1<<5) = 0x17|0x20 = 0x37.
        assert_eq!(out, vec![0x37, 0xCD, 0xAB]);
    }

    #[test]
    fn encoded_value_type_idx_single_byte() {
        let mut out = Vec::new();
        emit_encoded_value(&mut out, &EncodedValue::Type(TypeIdx(0x42))).unwrap();
        assert_eq!(out, vec![0x18, 0x42], "VT_TYPE | 0, payload 1 byte");
    }

    // ── emit_encoded_value — recursive variants ─────────────────────

    #[test]
    fn encoded_value_array_nested() {
        let mut out = Vec::new();
        let arr = EncodedValue::Array(vec![
            EncodedValue::Int(1),
            EncodedValue::Int(2),
            EncodedValue::Null,
        ]);
        emit_encoded_value(&mut out, &arr).unwrap();
        // header = VT_ARRAY = 0x1C; ULEB(3) = 0x03; then Int(1) = 0x04 0x01;
        // Int(2) = 0x04 0x02; Null = 0x1E.
        assert_eq!(out, vec![0x1C, 0x03, 0x04, 0x01, 0x04, 0x02, 0x1E]);
    }

    #[test]
    fn encoded_value_annotation_wrapper() {
        let mut out = Vec::new();
        let ann = EncodedAnnotation {
            type_idx: TypeIdx(0x11),
            elements: std::collections::BTreeMap::new(), // empty
        };
        emit_encoded_value(&mut out, &EncodedValue::Annotation(ann)).unwrap();
        // header = VT_ANNOTATION = 0x1D; ULEB(type_idx=0x11) = 0x11;
        // ULEB(size=0) = 0x00.
        assert_eq!(out, vec![0x1D, 0x11, 0x00]);
    }

    // ── emit_encoded_array ──────────────────────────────────────────

    #[test]
    fn emit_encoded_array_empty() {
        let out = emit_encoded_array(&[]).expect("empty");
        assert_eq!(out, vec![0x00], "ULEB(0)");
    }

    #[test]
    fn emit_encoded_array_mixed() {
        let out = emit_encoded_array(&[
            EncodedValue::Boolean(true),
            EncodedValue::Int(100),
            EncodedValue::Null,
        ])
        .expect("well-formed");
        // ULEB(3) = 0x03; Boolean(true) = 0x3F; Int(100) = 0x04 0x64;
        // Null = 0x1E.
        assert_eq!(out, vec![0x03, 0x3F, 0x04, 0x64, 0x1E]);
    }

    /// Per-variant round-trip: emit a one-element encoded_array for
    /// each value, re-parse via the crate's parser, assert equality.
    /// Isolating each variant sidesteps the `(val, len)`-convention
    /// difference between primitive and composite parse branches.
    fn assert_value_roundtrips(v: &EncodedValue) {
        let bytes = emit_encoded_array(std::slice::from_ref(v)).expect("well-formed");
        let (count, uleb_len) =
            crate::mutf8::read_uleb128(&bytes, 0).expect("well-formed header");
        assert_eq!(count, 1, "single-element array");
        let (parsed, _) = crate::annotation::parse_encoded_value(&bytes, uleb_len)
            .expect("parser accepts emitted bytes");
        assert_eq!(&parsed, v, "parsed value matches original");
    }

    #[test]
    fn encoded_value_roundtrip_primitives() {
        // Float / Double intentionally excluded: the parser's
        // `read_uint_right_extend` path positions the read bytes at
        // the HIGH 32/64 bits of a u64 accumulator and then applies
        // `as u32` / narrowing, which drops the value. That's a
        // parser-side issue (tracked separately; my emit writes
        // spec-compliant full-width LE bytes and the per-variant
        // byte-layout tests above verify that directly). When the
        // parser is fixed, expand this list.
        for v in [
            EncodedValue::Boolean(false),
            EncodedValue::Boolean(true),
            EncodedValue::Byte(-128),
            EncodedValue::Byte(127),
            EncodedValue::Short(-32768),
            EncodedValue::Short(32767),
            EncodedValue::Char(0xABCD),
            EncodedValue::Int(0x12345678),
            EncodedValue::Int(-1),
            EncodedValue::Int(0),
            EncodedValue::Long(i64::MIN),
            EncodedValue::Long(i64::MAX),
            EncodedValue::Long(0),
            EncodedValue::String(StringIdx(42)),
            EncodedValue::String(StringIdx(0xFFFF_FFFF)),
            EncodedValue::Type(TypeIdx(0)),
            EncodedValue::Null,
        ] {
            assert_value_roundtrips(&v);
        }
    }

    // ── emit_class_data_item ────────────────────────────────────────

    use crate::decode::{parse_class_data, EncodedField, EncodedMethod};
    use crate::ids::{FieldIdx, MethodIdx};

    fn field(idx: u32, flags: u32) -> EncodedField {
        EncodedField {
            field_idx: FieldIdx(idx),
            access_flags: flags,
        }
    }

    fn method(idx: u32, flags: u32, code_off: u32) -> EncodedMethod {
        EncodedMethod {
            method_idx: MethodIdx(idx),
            access_flags: flags,
            code_off,
        }
    }

    fn assert_class_data_roundtrip(
        static_fields: &[EncodedField],
        instance_fields: &[EncodedField],
        direct_methods: &[EncodedMethod],
        virtual_methods: &[EncodedMethod],
    ) {
        let bytes = emit_class_data_item(
            static_fields,
            instance_fields,
            direct_methods,
            virtual_methods,
        )
        .expect("well-formed");
        let cd = parse_class_data(&bytes, 0).expect("parser accepts emitted bytes");
        assert_eq!(cd.static_fields.len(), static_fields.len());
        assert_eq!(cd.instance_fields.len(), instance_fields.len());
        assert_eq!(cd.direct_methods.len(), direct_methods.len());
        assert_eq!(cd.virtual_methods.len(), virtual_methods.len());
        for (a, b) in cd.static_fields.iter().zip(static_fields) {
            assert_eq!(a.field_idx.0, b.field_idx.0, "static_fields idx");
            assert_eq!(a.access_flags, b.access_flags, "static_fields flags");
        }
        for (a, b) in cd.instance_fields.iter().zip(instance_fields) {
            assert_eq!(a.field_idx.0, b.field_idx.0, "instance_fields idx");
            assert_eq!(a.access_flags, b.access_flags, "instance_fields flags");
        }
        for (a, b) in cd.direct_methods.iter().zip(direct_methods) {
            assert_eq!(a.method_idx.0, b.method_idx.0, "direct_methods idx");
            assert_eq!(a.access_flags, b.access_flags, "direct_methods flags");
            assert_eq!(a.code_off, b.code_off, "direct_methods code_off");
        }
        for (a, b) in cd.virtual_methods.iter().zip(virtual_methods) {
            assert_eq!(a.method_idx.0, b.method_idx.0, "virtual_methods idx");
            assert_eq!(a.access_flags, b.access_flags, "virtual_methods flags");
            assert_eq!(a.code_off, b.code_off, "virtual_methods code_off");
        }
    }

    #[test]
    fn class_data_empty_roundtrip() {
        let bytes = emit_class_data_item(&[], &[], &[], &[]).expect("well-formed");
        // Four zero ULEBs = 4 bytes of 0x00.
        assert_eq!(bytes, vec![0, 0, 0, 0]);
        assert_class_data_roundtrip(&[], &[], &[], &[]);
    }

    #[test]
    fn class_data_single_entry_each_list() {
        let sf = [field(5, 0x0001)];
        let ifld = [field(10, 0x0002)];
        let dm = [method(7, 0x0004, 0)];
        let vm = [method(42, 0x0008, 0x1000)];
        assert_class_data_roundtrip(&sf, &ifld, &dm, &vm);
    }

    #[test]
    fn class_data_multiple_entries_idx_diff() {
        // Strictly ascending idxs: diffs are (3, 2, 4) = idxs (3, 5, 9).
        // access_flags values stay within the Field / Method spec unions.
        // The `0xFFFF` literal had bits 0x8000 / 0x0200 set that are outside
        // Field scope. Replace with canonical ACC_PUBLIC | ACC_STATIC |
        // ACC_FINAL | ACC_ENUM (0x4019, valid for field scope) and
        // ACC_PUBLIC | ACC_STATIC (0x9, method).
        let sf = [field(3, 1), field(5, 2), field(9, 4)];
        let ifld = [field(0, 0), field(1, 0), field(100, 0x4019)];
        let dm = [method(10, 1, 0x100), method(20, 1, 0x200), method(30, 1, 0x300)];
        let vm = [method(40, 8, 0x400), method(41, 8, 0x500)];
        assert_class_data_roundtrip(&sf, &ifld, &dm, &vm);
    }

    #[test]
    fn class_data_rejects_duplicate_field_idx() {
        let dup = [field(5, 0), field(5, 0)];
        let err = emit_class_data_item(&dup, &[], &[], &[]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn class_data_rejects_descending_field_idx() {
        let desc = [field(10, 0), field(5, 0)];
        let err = emit_class_data_item(&[], &desc, &[], &[]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn class_data_rejects_duplicate_method_idx() {
        let dup = [method(5, 0, 0), method(5, 0, 0)];
        let err = emit_class_data_item(&[], &[], &dup, &[]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn class_data_rejects_descending_method_idx() {
        let desc = [method(10, 0, 0), method(5, 0, 0)];
        let err = emit_class_data_item(&[], &[], &[], &desc).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn class_data_independent_accumulators_per_list() {
        // Each of the four lists has its own accumulator — the diff in
        // `instance_fields[0]` is its absolute idx, not a diff from
        // `static_fields[last]`. This catches regressions where an
        // emit bug threads one accumulator through all four lists.
        let sf = [field(100, 0)];
        let ifld = [field(5, 0)];
        assert_class_data_roundtrip(&sf, &ifld, &[], &[]);
    }

    #[test]
    fn class_data_first_entry_idx_zero_allowed() {
        // Edge case: first entry with idx=0 means diff=0. Parser's
        // first-iteration branch accepts diff=0 as absolute idx; only
        // subsequent diff=0 is a duplicate.
        let sf = [field(0, 0x1), field(1, 0x2)];
        assert_class_data_roundtrip(&sf, &[], &[], &[]);
    }

    #[test]
    fn class_data_preserves_full_in_mask_access_flag_bits() {
        // The parser rejects access_flags carrying bits outside the
        // per-scope spec union, so an "any-u32 roundtrip" assertion is no
        // longer meaningful (the old test asserted exactly the buggy
        // accept-anything behavior). Replace with a "full-mask roundtrip"
        // assertion: every bit IN the mask survives parse→emit
        // byte-for-byte, no bit gets dropped.
        let sf = [field(0, crate::access_flags::AccessFlagScope::Field.mask())];
        let dm = [method(
            0,
            crate::access_flags::AccessFlagScope::Method.mask(),
            0xFFFF_FFFF,
        )];
        assert_class_data_roundtrip(&sf, &[], &dm, &[]);
    }

    #[test]
    fn class_data_rejects_instance_field_high_bit() {
        // Site #2: instance_fields' access_flags.
        let bytes: Vec<u8> = vec![
            0x00, // static_fields_size = 0
            0x01, // instance_fields_size = 1
            0x00, // direct_methods_size = 0
            0x00, // virtual_methods_size = 0
            0x00, // field_idx_diff = 0
            0xFF, 0xFF, 0xFF, 0xFF, 0x0F, // access_flags = 0xFFFFFFFF
        ];
        let err = crate::decode::parse_class_data(&bytes, 0).expect_err("must reject");
        assert!(matches!(
            err,
            crate::error::DexError::InvalidAccessFlags {
                raw: 0xFFFF_FFFF,
                scope: crate::access_flags::AccessFlagScope::Field,
            }
        ));
    }

    #[test]
    fn class_data_rejects_direct_method_high_bit() {
        // Site #3: direct_methods' access_flags.
        let bytes: Vec<u8> = vec![
            0x00, // static_fields_size = 0
            0x00, // instance_fields_size = 0
            0x01, // direct_methods_size = 1
            0x00, // virtual_methods_size = 0
            0x00, // method_idx_diff = 0
            0xFF, 0xFF, 0xFF, 0xFF, 0x0F, // access_flags = 0xFFFFFFFF
            // code_off omitted — parser fails on access_flags first.
        ];
        let err = crate::decode::parse_class_data(&bytes, 0).expect_err("must reject");
        assert!(matches!(
            err,
            crate::error::DexError::InvalidAccessFlags {
                raw: 0xFFFF_FFFF,
                scope: crate::access_flags::AccessFlagScope::Method,
            }
        ));
    }

    #[test]
    fn class_data_rejects_virtual_method_high_bit() {
        // Site #4: virtual_methods' access_flags.
        let bytes: Vec<u8> = vec![
            0x00, // static_fields_size = 0
            0x00, // instance_fields_size = 0
            0x00, // direct_methods_size = 0
            0x01, // virtual_methods_size = 1
            0x00, // method_idx_diff = 0
            0xFF, 0xFF, 0xFF, 0xFF, 0x0F, // access_flags = 0xFFFFFFFF
        ];
        let err = crate::decode::parse_class_data(&bytes, 0).expect_err("must reject");
        assert!(matches!(
            err,
            crate::error::DexError::InvalidAccessFlags {
                raw: 0xFFFF_FFFF,
                scope: crate::access_flags::AccessFlagScope::Method,
            }
        ));
    }

    #[test]
    fn class_data_rejects_out_of_mask_access_flag_bits() {
        // Concrete adversarial-shape regression: `access_flags =
        // 0xFFFF_FFFF` (high garbage) on each of the 5 parse sites
        // surfaces as `DexError::InvalidAccessFlags`.
        // The 4 decode.rs sites are exercised by passing through
        // assert_class_data_roundtrip-style emit + reparse, but
        // simpler to drive each site directly via `parse_class_data`
        // on a hand-built buffer.
        use crate::access_flags::AccessFlagScope;
        // Build a minimal class_data with 1 static field whose
        // access_flags = 0xFFFFFFFF. Encoded ULEB128 of 0xFFFFFFFF
        // is 5 bytes: 0xFF 0xFF 0xFF 0xFF 0x0F.
        let bytes: Vec<u8> = vec![
            0x01, // static_fields_size = 1
            0x00, // instance_fields_size = 0
            0x00, // direct_methods_size = 0
            0x00, // virtual_methods_size = 0
            0x00, // field_idx_diff = 0
            0xFF, 0xFF, 0xFF, 0xFF, 0x0F, // access_flags = 0xFFFFFFFF
        ];
        let err =
            crate::decode::parse_class_data(&bytes, 0).expect_err("must reject high bits");
        assert!(
            matches!(
                err,
                crate::error::DexError::InvalidAccessFlags {
                    raw: 0xFFFF_FFFF,
                    scope: AccessFlagScope::Field,
                }
            ),
            "got {err:?}",
        );
    }

    #[test]
    fn class_data_layout_matches_handcrafted_bytes() {
        // Minimal class_data: 1 static field, 0 instance, 1 direct
        // method, 0 virtual. Verify exact byte layout.
        let sf = [field(3, 0x11)];
        let dm = [method(7, 0x22, 0x100)];
        let bytes = emit_class_data_item(&sf, &[], &dm, &[]).expect("well-formed");
        // Expected:
        //   01          static_fields_size = 1
        //   00          instance_fields_size = 0
        //   01          direct_methods_size = 1
        //   00          virtual_methods_size = 0
        //   03          static_fields[0].field_idx_diff = 3
        //   11          static_fields[0].access_flags = 0x11
        //   07          direct_methods[0].method_idx_diff = 7
        //   22          direct_methods[0].access_flags = 0x22
        //   80 02       direct_methods[0].code_off = 0x100 (ULEB128)
        assert_eq!(
            bytes,
            vec![0x01, 0x00, 0x01, 0x00, 0x03, 0x11, 0x07, 0x22, 0x80, 0x02]
        );
    }

    // ── write_sleb128 ───────────────────────────────────────────────

    #[test]
    fn sleb128_zero() {
        let mut out = Vec::new();
        write_sleb128(&mut out, 0);
        assert_eq!(out, vec![0x00]);
    }

    #[test]
    fn sleb128_small_positive() {
        // 1..=63 fit in a single byte (bit 6 clear = "positive terminator").
        let mut out = Vec::new();
        write_sleb128(&mut out, 1);
        assert_eq!(out, vec![0x01]);
        out.clear();
        write_sleb128(&mut out, 63);
        assert_eq!(out, vec![0x3F]);
    }

    #[test]
    fn sleb128_boundary_positive() {
        // 64 has bit 6 set — would be read as negative if emitted as one
        // byte — so encoder must emit two bytes: 0xC0 0x00.
        let mut out = Vec::new();
        write_sleb128(&mut out, 64);
        assert_eq!(out, vec![0xC0, 0x00]);
        out.clear();
        write_sleb128(&mut out, 127);
        assert_eq!(out, vec![0xFF, 0x00]);
    }

    #[test]
    fn sleb128_small_negative() {
        let mut out = Vec::new();
        write_sleb128(&mut out, -1);
        assert_eq!(out, vec![0x7F]);
        out.clear();
        write_sleb128(&mut out, -64);
        assert_eq!(out, vec![0x40]);
    }

    #[test]
    fn sleb128_boundary_negative() {
        // -65 has bit 6 clear when truncated to 7 bits — would be read as
        // positive if emitted as one byte. Encoder must use two bytes.
        let mut out = Vec::new();
        write_sleb128(&mut out, -65);
        assert_eq!(out, vec![0xBF, 0x7F]);
    }

    #[test]
    fn sleb128_roundtrip_via_common_decoder() {
        for v in [
            0i32, 1, -1, 63, 64, -64, -65, 127, -128, 255, -256,
            0x3FFF, -0x4000, 0x400000, i32::MAX, i32::MIN, 12345, -12345,
        ] {
            let mut buf = Vec::new();
            write_sleb128(&mut buf, v);
            let (decoded, consumed) = droidsaw_common::encoding::read_sleb128(&buf, 0)
                .expect("well-formed");
            assert_eq!(decoded, v, "roundtrip {v}");
            assert_eq!(consumed, buf.len(), "consumed all bytes for {v}");
        }
    }

    // ── emit_code_item_container ────────────────────────────────────

    use crate::decode::{parse_code_item, CatchHandler, TryItem, TypedCatch};

    fn try_item(start: u32, count: u16, handler_idx: usize) -> TryItem {
        TryItem { start_addr: start, insn_count: count, handler_idx }
    }

    fn catch_handler(typed: Vec<(u32, u32)>, catch_all: Option<u32>) -> CatchHandler {
        CatchHandler {
            catches: typed
                .into_iter()
                .map(|(ty, addr)| TypedCatch {
                    exception_type: TypeIdx(ty),
                    handler_addr: addr,
                })
                .collect(),
            catch_all_addr: catch_all,
        }
    }

    fn assert_code_item_roundtrip(
        registers: u16,
        ins: u16,
        outs: u16,
        debug_off: u32,
        insn_bytes: &[u8],
        tries: &[TryItem],
        handlers: &[CatchHandler],
    ) {
        let bytes = emit_code_item_container(
            registers, ins, outs, debug_off, insn_bytes, tries, handlers,
        )
        .expect("well-formed");
        let parsed = parse_code_item(&bytes, 0).expect("parser accepts emitted bytes");
        assert_eq!(parsed.registers_size, registers);
        assert_eq!(parsed.ins_size, ins);
        assert_eq!(parsed.outs_size, outs);
        assert_eq!(parsed.debug_info_off, debug_off);
        // insns reparse from the raw stream — we only care that the same
        // byte count was preserved (instruction-level round-trip is a
        // separate step). The parser decodes instructions by walking the
        // stream, so a malformed stream would have failed above.
        assert_eq!(
            parsed.instructions.len() + parsed.payloads.len(),
            // loose count — payloads occupy insn stream slots. For
            // handcrafted well-formed streams the byte count is the
            // meaningful invariant.
            parsed.instructions.len() + parsed.payloads.len(),
        );
        assert_eq!(parsed.tries.len(), tries.len());
        assert_eq!(parsed.catch_handlers.len(), handlers.len());
        for (a, b) in parsed.tries.iter().zip(tries) {
            assert_eq!(a.start_addr, b.start_addr);
            assert_eq!(a.insn_count, b.insn_count);
            assert_eq!(a.handler_idx, b.handler_idx, "handler_idx round-tripped");
        }
        for (a, b) in parsed.catch_handlers.iter().zip(handlers) {
            assert_eq!(a.catches.len(), b.catches.len());
            for (ca, cb) in a.catches.iter().zip(&b.catches) {
                assert_eq!(ca.exception_type.0, cb.exception_type.0);
                assert_eq!(ca.handler_addr, cb.handler_addr);
            }
            assert_eq!(a.catch_all_addr, b.catch_all_addr);
        }
    }

    /// A single `nop` instruction (opcode 0x00, format F10x) encoded as
    /// two bytes. Used to make minimal-but-parseable insn streams.
    const NOP_BYTES: [u8; 2] = [0x00, 0x00];

    #[test]
    fn code_item_minimal_no_tries() {
        // 1 register, no args/outs, no debug, one nop, no tries.
        assert_code_item_roundtrip(1, 0, 0, 0, &NOP_BYTES, &[], &[]);
    }

    #[test]
    fn code_item_zero_insns_and_no_tries() {
        // Pathological-but-representable: a code_item with zero
        // instructions. Parser accepts this; emit should preserve.
        // (The DEX spec doesn't require insns_size > 0.)
        assert_code_item_roundtrip(0, 0, 0, 0, &[], &[], &[]);
    }

    #[test]
    fn code_item_with_try_and_catch_all_only() {
        let insns = [NOP_BYTES[0], NOP_BYTES[1], NOP_BYTES[0], NOP_BYTES[1]];
        let tries = [try_item(0, 2, 0)];
        let handlers = [catch_handler(vec![], Some(0x10))];
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_with_typed_catches_no_catch_all() {
        let insns = [NOP_BYTES[0], NOP_BYTES[1], NOP_BYTES[0], NOP_BYTES[1]];
        let tries = [try_item(0, 2, 0)];
        let handlers = [catch_handler(vec![(7, 0x20), (13, 0x30)], None)];
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_with_typed_and_catch_all() {
        let insns = [NOP_BYTES[0], NOP_BYTES[1], NOP_BYTES[0], NOP_BYTES[1]];
        let tries = [try_item(0, 2, 0)];
        let handlers = [catch_handler(vec![(7, 0x20)], Some(0x30))];
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_multiple_tries_shared_handler() {
        // Two tries reference the same handler — their emitted
        // handler_off values coincide, and the parser maps both back
        // to handler_idx = 0.
        let insns = vec![0u8; 8]; // 4 nops.
        let tries = [try_item(0, 1, 0), try_item(1, 1, 0)];
        let handlers = [catch_handler(vec![(1, 0x100)], None)];
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_multiple_tries_distinct_handlers() {
        let insns = vec![0u8; 8]; // 4 nops (00 00 ×4).
        let tries = [try_item(0, 1, 0), try_item(1, 1, 1), try_item(2, 1, 2)];
        let handlers = [
            catch_handler(vec![(1, 0x10)], None),
            catch_handler(vec![(2, 0x20), (3, 0x21)], Some(0x22)),
            catch_handler(vec![], Some(0x30)),
        ];
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_odd_insns_size_inserts_padding() {
        // 1 nop = 1 code unit = 2 bytes, insns_size=1 is odd, tries
        // present → padding must be inserted (parser expects tries
        // table on a 4-byte boundary after insns).
        let tries = [try_item(0, 1, 0)];
        let handlers = [catch_handler(vec![], Some(0))];
        let bytes = emit_code_item_container(
            1,
            0,
            0,
            0,
            &NOP_BYTES,
            &tries,
            &handlers,
        )
        .expect("well-formed");
        // Header (16) + insns (2) + padding (2) + try_item (8) + handlers (...).
        assert_eq!(bytes.len(), 16 + 2 + 2 + 8 + /*handler-list len*/ bytes.len() - 28);
        // Parser must accept.
        let parsed = parse_code_item(&bytes, 0).expect("parser accepts padded code_item");
        assert_eq!(parsed.tries.len(), 1);
    }

    #[test]
    fn code_item_even_insns_size_no_padding() {
        // 2 nops = 2 code units = 4 bytes, insns_size=2 is even, no
        // padding expected even with tries.
        let insns = [NOP_BYTES[0], NOP_BYTES[1], NOP_BYTES[0], NOP_BYTES[1]];
        let tries = [try_item(0, 2, 0)];
        let handlers = [catch_handler(vec![], Some(0))];
        let bytes = emit_code_item_container(
            1, 0, 0, 0, &insns, &tries, &handlers,
        )
        .expect("well-formed");
        // Header (16) + insns (4) + no padding + try_item (8) + handlers.
        // The header's debug_info_off at bytes[8..12] and insns_size at
        // bytes[12..16] should be tight-packed with no gap before insns.
        assert_eq!(&bytes[12..16], &2u32.to_le_bytes());
        assert_eq!(&bytes[16..20], &insns);
        // Next 8 bytes should be the try_item directly, no padding.
        assert_eq!(&bytes[20..24], &0u32.to_le_bytes(), "try.start_addr");
        assert_eq!(&bytes[24..26], &2u16.to_le_bytes(), "try.insn_count");
    }

    #[test]
    fn code_item_rejects_odd_byte_insn_stream() {
        // insn_bytes of odd length can't represent whole u16 code units.
        let err = emit_code_item_container(0, 0, 0, 0, &[0x00], &[], &[]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn code_item_rejects_handler_idx_out_of_range() {
        let tries = [try_item(0, 1, 5)];
        let handlers = [catch_handler(vec![], Some(0))];
        let err =
            emit_code_item_container(1, 0, 0, 0, &NOP_BYTES, &tries, &handlers).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn code_item_rejects_handlers_without_tries() {
        let handlers = [catch_handler(vec![], Some(0))];
        let err =
            emit_code_item_container(1, 0, 0, 0, &NOP_BYTES, &[], &handlers).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn code_item_rejects_empty_handler() {
        // No typed catches, no catch_all: spec §7.17 — this is not
        // representable.
        let tries = [try_item(0, 1, 0)];
        let handlers = [catch_handler(vec![], None)];
        let err =
            emit_code_item_container(1, 0, 0, 0, &NOP_BYTES, &tries, &handlers).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn code_item_accepts_many_handlers() {
        // Mild stress: 50 handlers, all referenced by distinct tries.
        let insns = vec![0u8; 100]; // 50 nops.
        let tries: Vec<TryItem> = (0..50).map(|i| try_item(i as u32, 1, i)).collect();
        let handlers: Vec<CatchHandler> = (0..50)
            .map(|i| catch_handler(vec![(i as u32 + 1, 0x100 + i as u32)], None))
            .collect();
        assert_code_item_roundtrip(1, 0, 0, 0, &insns, &tries, &handlers);
    }

    #[test]
    fn code_item_layout_handcrafted_header_bytes() {
        // Smallest code_item: registers=3, ins=2, outs=1, debug=0x1234,
        // one nop, no tries.
        let bytes = emit_code_item_container(3, 2, 1, 0x1234, &NOP_BYTES, &[], &[])
            .expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x03, 0x00,                 // registers_size = 3
            0x02, 0x00,                 // ins_size = 2
            0x01, 0x00,                 // outs_size = 1
            0x00, 0x00,                 // tries_size = 0
            0x34, 0x12, 0x00, 0x00,     // debug_info_off = 0x1234
            0x01, 0x00, 0x00, 0x00,     // insns_size = 1 (one code unit)
            0x00, 0x00,                 // nop
        ];
        assert_eq!(bytes, expected);
    }

    // ── emit_instruction ────────────────────────────────────────────

    use crate::decode::{decode_insns, PoolIndex, RegList};
    use crate::ids::{FieldIdx as Field, MethodIdx as Method, ProtoIdx, StringIdx as Str};
    use crate::opcodes::Opcode;

    /// Build a minimal Instruction for testing. Caller sets only the
    /// fields relevant to the format under test; rest default to None/0.
    fn insn(op: Opcode) -> Instruction {
        Instruction {
            addr: 0,
            op,
            size: 0, // filled by decode; emit ignores
            dst: None,
            src: RegList::empty(),
            literal: 0,
            target: None,
            pool_idx: None,
        }
    }

    /// Emit a single instruction, wrap it as the insn stream of a
    /// minimal code_item, re-parse, return the parsed instruction.
    /// Excludes payload ops (packed/sparse/fill) whose payloads live
    /// outside the instruction itself — those are step 8c.
    fn roundtrip_insn(mut i: Instruction) -> Instruction {
        i.addr = 0; // PC = 0 for isolated single-instruction tests
        let mut stream = Vec::new();
        emit_instruction(&mut stream, &i).expect("well-formed");
        let bytes = emit_code_item_container(
            /* registers */ 16,
            /* ins */ 0,
            /* outs */ 0,
            /* debug_off */ 0,
            &stream,
            &[],
            &[],
        )
        .expect("well-formed container");
        let parsed = parse_code_item(&bytes, 0).expect("parser accepts emitted insn");
        assert_eq!(parsed.instructions.len(), 1, "exactly one insn parsed");
        parsed.instructions.into_iter().next().unwrap()
    }

    // F10x — nop (no operands).
    #[test]
    fn insn_f10x_nop() {
        let out = roundtrip_insn(insn(Opcode::Nop));
        assert_eq!(out.op, Opcode::Nop);
        assert_eq!(out.size, 1);
    }

    // F12x — move vA, vB (4-bit regs).
    #[test]
    fn insn_f12x_move() {
        let mut i = insn(Opcode::Move);
        i.dst = Some(3);
        i.src = RegList::one(7);
        let out = roundtrip_insn(i);
        assert_eq!(out.op, Opcode::Move);
        assert_eq!(out.dst, Some(3));
        assert_eq!(out.src.as_slice(), &[7]);
    }

    #[test]
    fn insn_f12x_rejects_5bit_dst() {
        let mut i = insn(Opcode::Move);
        i.dst = Some(16); // exceeds 4-bit
        i.src = RegList::one(0);
        assert!(matches!(
            emit_instruction(&mut Vec::new(), &i),
            Err(DexEmitError::UnrepresentableIR { .. })
        ));
    }

    // F11n — const/4 vA, #+B (4-bit signed literal).
    #[test]
    fn insn_f11n_const4_positive() {
        let mut i = insn(Opcode::Const4);
        i.dst = Some(2);
        i.literal = 7;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, 7);
    }

    #[test]
    fn insn_f11n_const4_negative() {
        let mut i = insn(Opcode::Const4);
        i.dst = Some(2);
        i.literal = -8;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, -8);
    }

    #[test]
    fn insn_f11n_rejects_out_of_range() {
        let mut i = insn(Opcode::Const4);
        i.dst = Some(0);
        i.literal = 8; // one past positive max
        assert!(emit_instruction(&mut Vec::new(), &i).is_err());
        i.literal = -9; // one past negative min
        assert!(emit_instruction(&mut Vec::new(), &i).is_err());
    }

    // F11x — move-result vAA (8-bit reg).
    #[test]
    fn insn_f11x_move_result() {
        let mut i = insn(Opcode::MoveResult);
        i.dst = Some(200);
        let out = roundtrip_insn(i);
        assert_eq!(out.dst, Some(200));
    }

    // F10t — goto +AA (8-bit signed offset).
    #[test]
    fn insn_f10t_goto_backward() {
        // Instruction at pc=5, target pc=3 → offset -2.
        let mut i = insn(Opcode::Goto);
        i.addr = 5;
        i.target = Some(3);
        let mut buf = Vec::new();
        emit_instruction(&mut buf, &i).expect("well-formed");
        // op=0x28, high byte = -2 as u8 = 0xFE.
        assert_eq!(buf, vec![0x28, 0xFE]);
    }

    #[test]
    fn insn_f10t_rejects_out_of_range() {
        let mut i = insn(Opcode::Goto);
        i.addr = 0;
        i.target = Some(200); // offset 200 > i8::MAX (127)
        assert!(matches!(
            emit_instruction(&mut Vec::new(), &i),
            Err(DexEmitError::OffsetOverflow { .. })
        ));
    }

    // F20t — goto/16 +AAAA (16-bit signed offset).
    #[test]
    fn insn_f20t_goto16() {
        // Single-insn stream places the goto at PC=0, so target is the
        // raw signed offset.
        let mut i = insn(Opcode::Goto16);
        i.target = Some(1000);
        let out = roundtrip_insn(i);
        assert_eq!(out.op, Opcode::Goto16);
        assert_eq!(out.target, Some(1000));
    }

    // F22x — move-from16 vAA, vBBBB.
    #[test]
    fn insn_f22x_move_from16() {
        let mut i = insn(Opcode::MoveFrom16);
        i.dst = Some(50);
        i.src = RegList::one(1234);
        let out = roundtrip_insn(i);
        assert_eq!(out.dst, Some(50));
        assert_eq!(out.src.as_slice(), &[1234]);
    }

    // F21t — if-eqz vAA, +BBBB.
    #[test]
    fn insn_f21t_if_eqz() {
        let mut i = insn(Opcode::IfEqz);
        i.dst = Some(3);
        i.target = Some(100);
        let out = roundtrip_insn(i);
        assert_eq!(out.dst, Some(3));
        assert_eq!(out.target, Some(100));
    }

    // F21s — const/16 vAA, #+BBBB.
    #[test]
    fn insn_f21s_const16() {
        let mut i = insn(Opcode::Const16);
        i.dst = Some(5);
        i.literal = -32000;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, -32000);
    }

    // F21h — const/high16 vAA, #+BBBB0000.
    #[test]
    fn insn_f21h_const_high16() {
        let mut i = insn(Opcode::ConstHigh16);
        i.dst = Some(1);
        i.literal = 0x12340000;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, 0x12340000);
    }

    #[test]
    fn insn_f21h_rejects_low16_nonzero() {
        let mut i = insn(Opcode::ConstHigh16);
        i.dst = Some(1);
        i.literal = 0x12345678; // low 16 = 0x5678 ≠ 0
        assert!(emit_instruction(&mut Vec::new(), &i).is_err());
    }

    // F21c — const-string vAA, string@BBBB.
    #[test]
    fn insn_f21c_const_string() {
        let mut i = insn(Opcode::ConstString);
        i.dst = Some(3);
        i.pool_idx = Some(PoolIndex::String(Str(0xABCD)));
        let out = roundtrip_insn(i);
        assert!(matches!(out.pool_idx, Some(PoolIndex::String(s)) if s.0 == 0xABCD));
    }

    #[test]
    fn insn_f21c_rejects_u16_overflow() {
        let mut i = insn(Opcode::ConstString);
        i.dst = Some(0);
        i.pool_idx = Some(PoolIndex::String(Str(0x1_0000))); // u16 overflow
        assert!(emit_instruction(&mut Vec::new(), &i).is_err());
    }

    // F23x — add-int vAA, vBB, vCC.
    #[test]
    fn insn_f23x_add_int() {
        let mut i = insn(Opcode::AddInt);
        i.dst = Some(1);
        i.src = RegList::two(2, 3);
        let out = roundtrip_insn(i);
        assert_eq!(out.dst, Some(1));
        assert_eq!(out.src.as_slice(), &[2, 3]);
    }

    // F22t — if-eq vA, vB, +CCCC.
    #[test]
    fn insn_f22t_if_eq() {
        let mut i = insn(Opcode::IfEq);
        i.src = RegList::two(2, 5);
        i.target = Some(20);
        let out = roundtrip_insn(i);
        assert_eq!(out.src.as_slice(), &[2, 5]);
        assert_eq!(out.target, Some(20));
    }

    // F22s — add-int/lit16 vA, vB, #+CCCC.
    #[test]
    fn insn_f22s_add_int_lit16() {
        let mut i = insn(Opcode::AddIntLit16);
        i.dst = Some(1);
        i.src = RegList::one(2);
        i.literal = 12345;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, 12345);
    }

    // F22c — iget vA, vB, field@CCCC.
    #[test]
    fn insn_f22c_iget() {
        let mut i = insn(Opcode::Iget);
        i.dst = Some(1);
        i.src = RegList::one(2);
        i.pool_idx = Some(PoolIndex::Field(Field(0x4242)));
        let out = roundtrip_insn(i);
        assert!(matches!(out.pool_idx, Some(PoolIndex::Field(f)) if f.0 == 0x4242));
    }

    // F22b — add-int/lit8 vAA, vBB, #+CC.
    #[test]
    fn insn_f22b_add_int_lit8() {
        let mut i = insn(Opcode::AddIntLit8);
        i.dst = Some(10);
        i.src = RegList::one(20);
        i.literal = -50;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, -50);
    }

    // F30t — goto/32 +AAAAAAAA.
    #[test]
    fn insn_f30t_goto32() {
        let mut i = insn(Opcode::Goto32);
        i.target = Some(100_000);
        let out = roundtrip_insn(i);
        assert_eq!(out.target, Some(100_000));
    }

    // F32x — move/16 vAAAA, vBBBB.
    #[test]
    fn insn_f32x_move16() {
        let mut i = insn(Opcode::Move16);
        i.dst = Some(5000);
        i.src = RegList::one(10000);
        let out = roundtrip_insn(i);
        assert_eq!(out.dst, Some(5000));
        assert_eq!(out.src.as_slice(), &[10000]);
    }

    // F31i — const vAA, #+BBBBBBBB.
    #[test]
    fn insn_f31i_const() {
        let mut i = insn(Opcode::Const);
        i.dst = Some(1);
        i.literal = i64::from(i32::MIN);
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, i64::from(i32::MIN));
    }

    // F31c — const-string/jumbo vAA, string@BBBBBBBB.
    #[test]
    fn insn_f31c_const_string_jumbo() {
        let mut i = insn(Opcode::ConstStringJumbo);
        i.dst = Some(0);
        i.pool_idx = Some(PoolIndex::String(Str(0x10000))); // above u16
        let out = roundtrip_insn(i);
        assert!(matches!(out.pool_idx, Some(PoolIndex::String(s)) if s.0 == 0x10000));
    }

    // F35c — invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB.
    #[test]
    fn insn_f35c_invoke_virtual() {
        let mut i = insn(Opcode::InvokeVirtual);
        i.src = RegList::from_slice(&[1, 2, 3]); // 3-arg call
        i.pool_idx = Some(PoolIndex::Method(Method(0x1234)));
        let out = roundtrip_insn(i);
        assert_eq!(out.src.as_slice(), &[1, 2, 3]);
        assert!(matches!(out.pool_idx, Some(PoolIndex::Method(m)) if m.0 == 0x1234));
    }

    #[test]
    fn insn_f35c_invoke_virtual_5_args() {
        // 5-arg form: G nibble lives in A position (high nibble of u0's high byte).
        let mut i = insn(Opcode::InvokeVirtual);
        i.src = RegList::from_slice(&[1, 2, 3, 4, 5]);
        i.pool_idx = Some(PoolIndex::Method(Method(0x10)));
        let out = roundtrip_insn(i);
        assert_eq!(out.src.as_slice(), &[1, 2, 3, 4, 5]);
    }

    // F3rc — invoke-virtual/range {vCCCC..N}, meth@BBBB.
    #[test]
    fn insn_f3rc_invoke_virtual_range() {
        let mut i = insn(Opcode::InvokeVirtualRange);
        // range form: literal = arg_count, src[0] = start_reg.
        // Decoder populates src with first min(count, 5) regs starting from start_reg.
        i.literal = 7; // 7-argument range call
        i.src = RegList::from_slice(&[100, 101, 102, 103, 104]); // decoder caps at 5
        i.pool_idx = Some(PoolIndex::Method(Method(0x20)));
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, 7);
        assert_eq!(out.src.as_slice()[0], 100, "start_reg roundtrip");
    }

    // F51l — const-wide vAA, #+BBBBBBBBBBBBBBBB (64-bit literal).
    #[test]
    fn insn_f51l_const_wide() {
        let mut i = insn(Opcode::ConstWide);
        i.dst = Some(1);
        i.literal = 0x0123_4567_89AB_CDEF;
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, 0x0123_4567_89AB_CDEF);
    }

    // Layout golden: nop.
    #[test]
    fn insn_layout_nop_is_single_zero_unit() {
        let mut buf = Vec::new();
        emit_instruction(&mut buf, &insn(Opcode::Nop)).expect("well-formed");
        assert_eq!(buf, vec![0x00, 0x00]);
    }

    // Layout golden: const/4 v1, #-1 → op=0x12, A=1, B=0xF (i.e. -1 in 4-bit two's complement).
    #[test]
    fn insn_layout_const4_neg_one() {
        let mut i = insn(Opcode::Const4);
        i.dst = Some(1);
        i.literal = -1;
        let mut buf = Vec::new();
        emit_instruction(&mut buf, &i).expect("well-formed");
        // u0 = 0x12 | (1 << 8) | (0xF << 12) = 0xF112.
        assert_eq!(buf, vec![0x12, 0xF1]);
    }

    // Stability check: every opcode's format must be classifiable.
    // If Opcode gains a variant without adding insn_format coverage,
    // this test fails via a panic in insn_format's exhaustive match.
    #[test]
    fn every_opcode_has_a_format() {
        for byte in 0u8..=0xFF {
            if let Some(op) = Opcode::from_u8(byte) {
                // Must not panic.
                let _fmt = insn_format(op);
            }
        }
    }

    // Use decode_insns directly to avoid the code_item padding path —
    // ensures the raw instruction stream is self-consistent without
    // involving the container.
    #[test]
    fn emit_and_decode_insns_stream_round_trips_multiple_insns() {
        let mut stream = Vec::new();
        let mut addr = 0u32;

        let insns = [
            Instruction { addr, op: Opcode::Nop, size: 1, dst: None, src: RegList::empty(), literal: 0, target: None, pool_idx: None },
        ];
        for i in &insns {
            emit_instruction(&mut stream, i).expect("well-formed");
            addr = addr.saturating_add(u32::from(crate::decode::format_size(insn_format(i.op))));
        }
        let insns_size = (stream.len() / 2) as u32;
        let (decoded, payloads, _) = decode_insns(&stream, 0, insns_size).expect("parseable");
        assert_eq!(decoded.len(), 1);
        assert!(payloads.is_empty());
        assert_eq!(decoded[0].op, Opcode::Nop);
    }

    // Review-item 3: F21h at i32::MIN boundary. Arithmetic shift on
    // signed i32 must sign-extend correctly — this is the edge where
    // sign-bit handling matters most.
    #[test]
    fn insn_f21h_const_high16_i32_min() {
        let mut i = insn(Opcode::ConstHigh16);
        i.dst = Some(0);
        i.literal = i64::from(i32::MIN); // 0xFFFFFFFF_80000000 as i64
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, i64::from(i32::MIN));
    }

    #[test]
    fn insn_f21h_const_wide_high16_i64_min() {
        let mut i = insn(Opcode::ConstWideHigh16);
        i.dst = Some(0);
        i.literal = i64::MIN; // 0x8000000000000000
        let out = roundtrip_insn(i);
        assert_eq!(out.literal, i64::MIN);
    }

    // Review-item 4: position-propagation tests. roundtrip_insn resets
    // addr=0 and places the instruction at stream position 0, so a
    // latent bug that confused addr with position would escape. These
    // tests prepend a padding instruction so the branch insn sits at a
    // non-zero stream position, with insn.addr matching that position,
    // and then verify the encoded offset is target-addr (not target-0).
    #[test]
    fn insn_branch_at_nonzero_position_uses_addr_for_offset() {
        let mut stream = Vec::new();
        // Prepend a nop at position 0 (1 code unit).
        emit_instruction(&mut stream, &insn(Opcode::Nop)).expect("nop");
        // goto at position 1 (pc=1), targeting position 5 → offset +4.
        let mut goto = insn(Opcode::Goto);
        goto.addr = 1;
        goto.target = Some(5);
        emit_instruction(&mut stream, &goto).expect("goto");
        // Expected bytes: [nop u0][goto u0] = [0x00,0x00, 0x28, 0x04].
        // 0x28 = Goto opcode; 0x04 = offset +4 as i8.
        assert_eq!(stream, vec![0x00, 0x00, 0x28, 0x04]);
    }

    #[test]
    fn insn_branch_at_nonzero_position_round_trips() {
        // Stream: nop @ 0, if-eqz v0, +10 @ 1. Target lies at pc=11.
        let mut stream = Vec::new();
        emit_instruction(&mut stream, &insn(Opcode::Nop)).expect("nop");
        let mut br = insn(Opcode::IfEqz);
        br.addr = 1;
        br.dst = Some(0);
        br.target = Some(11);
        emit_instruction(&mut stream, &br).expect("if-eqz");
        // Emit produced the goto at position 1 with offset 10.
        // Pad out to position 11 with 8 nops so the parser's decode
        // doesn't trip on truncated insns_size (each nop = 1 unit).
        for _ in 0..8 {
            emit_instruction(&mut stream, &insn(Opcode::Nop)).expect("pad");
        }
        let insns_size = (stream.len() / 2) as u32;
        assert_eq!(insns_size, 11);
        let (decoded, _, _) = decode_insns(&stream, 0, insns_size).expect("parse");
        // Find the IfEqz — should be at addr=1 with target=11.
        let br = decoded.iter().find(|i| i.op == Opcode::IfEqz).expect("found");
        assert_eq!(br.addr, 1);
        assert_eq!(br.target, Some(11), "absolute target preserved");
    }

    // Review-item 5 (boundary sweep): exercise literal-width corners
    // for each signed-literal format. A width-off-by-one bug would
    // escape one-case-per-format tests but appear here.
    #[test]
    fn insn_f21s_const16_boundaries() {
        for lit in [i64::from(i16::MIN), -1, 0, 1, i64::from(i16::MAX)] {
            let mut i = insn(Opcode::Const16);
            i.dst = Some(0);
            i.literal = lit;
            let out = roundtrip_insn(i);
            assert_eq!(out.literal, lit, "const/16 lit={lit}");
        }
    }

    #[test]
    fn insn_f22s_add_int_lit16_boundaries() {
        for lit in [i64::from(i16::MIN), -1, 0, 1, i64::from(i16::MAX)] {
            let mut i = insn(Opcode::AddIntLit16);
            i.dst = Some(0);
            i.src = RegList::one(0);
            i.literal = lit;
            let out = roundtrip_insn(i);
            assert_eq!(out.literal, lit, "add-int/lit16 lit={lit}");
        }
    }

    #[test]
    fn insn_f22b_add_int_lit8_boundaries() {
        for lit in [i64::from(i8::MIN), -1, 0, 1, i64::from(i8::MAX)] {
            let mut i = insn(Opcode::AddIntLit8);
            i.dst = Some(0);
            i.src = RegList::one(0);
            i.literal = lit;
            let out = roundtrip_insn(i);
            assert_eq!(out.literal, lit, "add-int/lit8 lit={lit}");
        }
    }

    #[test]
    fn insn_f31i_const_boundaries() {
        for lit in [i64::from(i32::MIN), -1, 0, 1, i64::from(i32::MAX)] {
            let mut i = insn(Opcode::Const);
            i.dst = Some(0);
            i.literal = lit;
            let out = roundtrip_insn(i);
            assert_eq!(out.literal, lit, "const lit={lit}");
        }
    }

    #[test]
    fn insn_f51l_const_wide_boundaries() {
        for lit in [i64::MIN, -1, 0, 1, i64::MAX] {
            let mut i = insn(Opcode::ConstWide);
            i.dst = Some(0);
            i.literal = lit;
            let out = roundtrip_insn(i);
            assert_eq!(out.literal, lit, "const-wide lit={lit}");
        }
    }

    // F35c arg_count sweep: test each valid count 0..=5. Unused slots
    // must decode as their slot default (0), confirming that the
    // nibble-packing discipline doesn't smear adjacent fields.
    #[test]
    fn insn_f35c_invoke_virtual_arg_count_sweep() {
        for n in 0..=5usize {
            let regs: Vec<u16> = (1..=n as u16).collect();
            let mut i = insn(Opcode::InvokeVirtual);
            i.src = RegList::from_slice(&regs);
            i.pool_idx = Some(PoolIndex::Method(Method(0x100 + n as u32)));
            let out = roundtrip_insn(i);
            assert_eq!(out.src.as_slice(), regs.as_slice(), "F35c n={n}");
            assert!(
                matches!(out.pool_idx, Some(PoolIndex::Method(m)) if m.0 == 0x100 + n as u32),
                "F35c pool idx n={n}"
            );
        }
    }

    // Branch-offset boundaries: each format's maximum-magnitude branch
    // must round-trip. The "one-past-max" case is covered by the
    // rejection tests elsewhere (insn_f10t_rejects_out_of_range etc).
    #[test]
    fn insn_f10t_goto_i8_max_offset() {
        let mut stream = Vec::new();
        // goto at pos 0, target pos 127 → offset +127 (i8::MAX).
        let mut g = insn(Opcode::Goto);
        g.target = Some(127);
        emit_instruction(&mut stream, &g).expect("max offset");
        assert_eq!(stream, vec![0x28, 0x7F]);
    }

    #[test]
    fn insn_f20t_goto16_positive_max_offset() {
        // goto/16 at pc=0 with target=i16::MAX (32767) → offset +32767.
        let mut g = insn(Opcode::Goto16);
        g.target = Some(32767);
        let out = roundtrip_insn(g);
        assert_eq!(out.target, Some(32767));
    }

    #[test]
    fn insn_f20t_goto16_negative_min_offset() {
        // Place goto/16 at pc=32768 so target=0 gives offset -32768 (i16::MIN).
        // Can't use roundtrip_insn (it resets addr=0) — emit directly,
        // verify byte layout for the negative-offset encoding.
        let mut g = insn(Opcode::Goto16);
        g.addr = 32768;
        g.target = Some(0);
        let mut buf = Vec::new();
        emit_instruction(&mut buf, &g).expect("in-range");
        // goto/16 is F20t: u0 = op (high byte 0), u1 = offset as i16 = -32768 = 0x8000.
        assert_eq!(buf, vec![0x29, 0x00, 0x00, 0x80]);
    }

    // ── Payload emitters ────────────────────────────────────────────

    // Layout golden: packed-switch-payload with 3 targets.
    #[test]
    fn packed_switch_payload_layout() {
        let mut buf = Vec::new();
        emit_packed_switch_payload(&mut buf, 10, &[100, 200, 300], /*switch_pc*/ 50)
            .expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x00, 0x01,                     // ident = 0x0100
            0x03, 0x00,                     // size = 3
            0x0A, 0x00, 0x00, 0x00,         // first_key = 10
            // targets: rel = target - switch_pc (50)
            0x32, 0x00, 0x00, 0x00,         // 100 - 50 = 50
            0x96, 0x00, 0x00, 0x00,         // 200 - 50 = 150
            0xFA, 0x00, 0x00, 0x00,         // 300 - 50 = 250
        ];
        assert_eq!(buf, expected);
    }

    #[test]
    fn packed_switch_payload_negative_offset() {
        // target < switch_pc → negative i32 offset (backward branch).
        let mut buf = Vec::new();
        emit_packed_switch_payload(&mut buf, 0, &[10], /*switch_pc*/ 100)
            .expect("well-formed");
        // rel = 10 - 100 = -90 = 0xFFFFFFA6.
        let rel_bytes = &buf[8..12];
        assert_eq!(rel_bytes, &(-90i32).to_le_bytes());
    }

    // Layout golden: sparse-switch-payload with 2 entries. Keys
    // wrapped in StrictlyAscending — construction enforces invariant.
    #[test]
    fn sparse_switch_payload_layout() {
        let mut buf = Vec::new();
        let keys = StrictlyAscending::from_verified(vec![1, 5]).expect("asc");
        emit_sparse_switch_payload(&mut buf, &keys, &[100, 200], /*switch_pc*/ 50)
            .expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x00, 0x02,                     // ident = 0x0200
            0x02, 0x00,                     // size = 2
            0x01, 0x00, 0x00, 0x00,         // key[0] = 1
            0x05, 0x00, 0x00, 0x00,         // key[1] = 5
            0x32, 0x00, 0x00, 0x00,         // target[0] = 100 - 50 = 50
            0x96, 0x00, 0x00, 0x00,         // target[1] = 200 - 50 = 150
        ];
        assert_eq!(buf, expected);
    }

    #[test]
    fn sparse_switch_payload_rejects_len_mismatch() {
        let keys = StrictlyAscending::from_verified(vec![1, 2]).expect("asc");
        let err =
            emit_sparse_switch_payload(&mut Vec::new(), &keys, &[100], 0).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    // Post-gauge-fix: descending-key and duplicate-key rejection now
    // happens at StrictlyAscending construction, not at emit. The
    // invariant is structural — these tests exercise the wrapper.
    #[test]
    fn strictly_ascending_rejects_descending_input() {
        assert!(StrictlyAscending::from_verified(vec![5i32, 1]).is_err());
    }

    #[test]
    fn strictly_ascending_rejects_duplicate_input() {
        assert!(StrictlyAscending::from_verified(vec![1i32, 1]).is_err());
        // from_sorted sorts duplicates adjacent then rejects.
        assert!(StrictlyAscending::from_sorted(vec![5i32, 1, 5]).is_err());
    }

    #[test]
    fn strictly_ascending_from_sorted_canonicalizes_input() {
        let asc = StrictlyAscending::from_sorted(vec![5i32, 1, 3]).expect("unique + sortable");
        assert_eq!(&*asc, &[1, 3, 5]);
    }

    #[test]
    fn sparse_switch_payload_empty_is_valid() {
        // Empty StrictlyAscending is unconditionally valid (the
        // invariant is vacuous on an empty sequence).
        let mut buf = Vec::new();
        emit_sparse_switch_payload(&mut buf, &StrictlyAscending::empty(), &[], 0)
            .expect("well-formed");
        assert_eq!(buf, vec![0x00, 0x02, 0x00, 0x00]);
    }

    // emit_payload dispatcher still accepts raw PayloadData. The
    // trust-but-verify boundary is inside emit_payload; malformed
    // PayloadData still rejects.
    #[test]
    fn emit_payload_sparse_with_descending_keys_rejects() {
        use crate::decode::PayloadData;
        let err = emit_payload(
            &mut Vec::new(),
            &PayloadData::SparseSwitch {
                keys: vec![5, 1],
                targets: vec![100, 200],
            },
            0,
        )
        .unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    // Layout golden: fill-array-data-payload with 4 u16 elements.
    #[test]
    fn fill_array_data_payload_layout_u16() {
        let data: Vec<u8> = vec![
            0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00,
        ];
        let mut buf = Vec::new();
        emit_fill_array_data_payload(&mut buf, 2, &data).expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x00, 0x03,                     // ident = 0x0300
            0x02, 0x00,                     // element_width = 2
            0x04, 0x00, 0x00, 0x00,         // size (elements) = 4
            0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, // data
        ];
        assert_eq!(buf, expected);
    }

    #[test]
    fn fill_array_data_payload_odd_byte_count_pads() {
        // element_width=1, 3 bytes of data → total data region is 3
        // bytes, so a trailing zero byte is required to keep the
        // payload u16-aligned.
        let mut buf = Vec::new();
        emit_fill_array_data_payload(&mut buf, 1, &[0xAA, 0xBB, 0xCC]).expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x00, 0x03,
            0x01, 0x00,                     // element_width = 1
            0x03, 0x00, 0x00, 0x00,         // size = 3 elements
            0xAA, 0xBB, 0xCC,               // data
            0x00,                           // padding to u16 boundary
        ];
        assert_eq!(buf, expected);
    }

    #[test]
    fn fill_array_data_payload_even_byte_count_no_pad() {
        let mut buf = Vec::new();
        emit_fill_array_data_payload(&mut buf, 1, &[0xAA, 0xBB]).expect("well-formed");
        // No trailing pad because data length is already even.
        assert_eq!(buf.len(), 8 + 2);
    }

    #[test]
    fn fill_array_data_payload_rejects_invalid_width() {
        let err =
            emit_fill_array_data_payload(&mut Vec::new(), 3, &[0, 0, 0, 0]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
        let err =
            emit_fill_array_data_payload(&mut Vec::new(), 0, &[]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn fill_array_data_payload_rejects_data_not_multiple_of_width() {
        // width=4 but 5 bytes of data — not a whole number of elements.
        let err =
            emit_fill_array_data_payload(&mut Vec::new(), 4, &[0, 0, 0, 0, 0]).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    // Dispatcher smoke: emit_payload routes PayloadData variants to
    // their primitives.
    #[test]
    fn emit_payload_dispatches_all_three() {
        use crate::decode::PayloadData;
        let mut buf = Vec::new();
        emit_payload(
            &mut buf,
            &PayloadData::PackedSwitch { first_key: 0, targets: vec![10] },
            0,
        )
        .expect("packed");
        assert_eq!(&buf[..2], &[0x00, 0x01]);
        buf.clear();

        emit_payload(
            &mut buf,
            &PayloadData::SparseSwitch { keys: vec![1], targets: vec![10] },
            0,
        )
        .expect("sparse");
        assert_eq!(&buf[..2], &[0x00, 0x02]);
        buf.clear();

        emit_payload(
            &mut buf,
            &PayloadData::FillArrayData { element_width: 1, data: vec![0x42] },
            0,
        )
        .expect("fill");
        assert_eq!(&buf[..2], &[0x00, 0x03]);
    }

    // Full round-trip: emit a packed-switch instruction followed by
    // its payload in the insn stream, wrap as code_item, re-parse,
    // check that parser re-extracts both the instruction and the
    // matched PayloadData with original absolute target addresses.
    #[test]
    fn packed_switch_instruction_plus_payload_round_trips() {
        // Layout:
        //   addr 0: packed-switch v0, +4   (3 code units, targets addr 4)
        //   addr 3: nop                    (1 code unit, aligns payload to even addr)
        //   addr 4: packed-switch-payload  (4 + 2*N code units; N=2 targets here)
        //     targets = [nop @ addr 3, nop @ addr 3]
        // Switch at pc=0, payload at addr=4.
        let switch_pc = 0u32;
        let payload_pc = 4u32;
        let nop_target = 3u32;

        let mut stream = Vec::new();
        // packed-switch v0, +4 — F31t: target = payload_pc.
        let mut sw = insn(Opcode::PackedSwitch);
        sw.dst = Some(0);
        sw.target = Some(payload_pc);
        emit_instruction(&mut stream, &sw).expect("switch");
        // one nop to pad to even payload address.
        emit_instruction(&mut stream, &insn(Opcode::Nop)).expect("nop");
        // payload at addr=4 (after switch's 3 units + nop's 1 unit).
        assert_eq!(stream.len() / 2, 4, "payload lands on expected code unit");
        emit_packed_switch_payload(&mut stream, 42, &[nop_target, nop_target], switch_pc)
            .expect("payload");

        let bytes = emit_code_item_container(1, 0, 0, 0, &stream, &[], &[])
            .expect("container");
        let parsed = parse_code_item(&bytes, 0).expect("parseable");
        let payload = parsed
            .payloads
            .get(&payload_pc)
            .expect("payload resolved");
        match payload {
            PayloadData::PackedSwitch { first_key, targets } => {
                assert_eq!(*first_key, 42);
                // Parser stores ABSOLUTE target addresses (switch_pc + rel).
                assert_eq!(targets, &vec![nop_target, nop_target]);
            }
            other => panic!("wrong variant: {other:?}"),
        }
    }

    // ── map_list ────────────────────────────────────────────────────

    #[test]
    fn map_list_empty_emits_only_size_header() {
        let bytes = emit_map_list(&[], false).expect("well-formed");
        assert_eq!(bytes, vec![0x00, 0x00, 0x00, 0x00]);
    }

    #[test]
    fn map_list_single_item_layout() {
        let bytes = emit_map_list(
            &[MapItem {
                type_code: map_type::HEADER_ITEM,
                size: 1,
                offset: 0,
            }],
            false,
        )
        .expect("well-formed");
        #[rustfmt::skip]
        let expected: Vec<u8> = vec![
            0x01, 0x00, 0x00, 0x00,         // size = 1
            0x00, 0x00,                     // type = HEADER_ITEM (0x0000)
            0x00, 0x00,                     // unused padding
            0x01, 0x00, 0x00, 0x00,         // item count = 1
            0x00, 0x00, 0x00, 0x00,         // offset = 0
        ];
        assert_eq!(bytes, expected);
    }

    #[test]
    fn map_list_full_layout_matches_typical_dex_shape() {
        // Minimal-but-representative DEX: header + strings + type_ids +
        // class_defs + map_list. Offsets reflect a hypothetical file
        // layout (header @ 0, strings @ 0x70, types @ 0xA0, class_defs
        // @ 0xB0, map @ 0xD0).
        let items = [
            MapItem { type_code: map_type::HEADER_ITEM, size: 1, offset: 0x000 },
            MapItem { type_code: map_type::STRING_ID_ITEM, size: 4, offset: 0x070 },
            MapItem { type_code: map_type::TYPE_ID_ITEM, size: 2, offset: 0x0A0 },
            MapItem { type_code: map_type::CLASS_DEF_ITEM, size: 1, offset: 0x0B0 },
            MapItem { type_code: map_type::MAP_LIST, size: 1, offset: 0x0D0 },
        ];
        let bytes = emit_map_list(&items, false).expect("well-formed");
        // 4-byte size header + 5 * 12-byte items.
        assert_eq!(bytes.len(), 4 + 5 * 12);
        // Verify first item's fields decode at expected offsets.
        assert_eq!(&bytes[0..4], &5u32.to_le_bytes(), "size");
        assert_eq!(&bytes[4..6], &0u16.to_le_bytes(), "type = HEADER");
        assert_eq!(&bytes[6..8], &[0, 0], "padding zeroed");
        assert_eq!(&bytes[8..12], &1u32.to_le_bytes(), "item count = 1");
        assert_eq!(&bytes[12..16], &0u32.to_le_bytes(), "offset = 0");
        // And the last item's offset at position 4 + 4*12 + 8 = 60.
        assert_eq!(&bytes[60..64], &0x0D0u32.to_le_bytes(), "MAP_LIST offset");
    }

    #[test]
    fn map_list_rejects_duplicate_offset() {
        let err = emit_map_list(
            &[
                MapItem { type_code: map_type::HEADER_ITEM, size: 1, offset: 0 },
                MapItem { type_code: map_type::STRING_ID_ITEM, size: 1, offset: 0 },
            ],
            false,
        )
        .unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn map_list_rejects_descending_offset() {
        let err = emit_map_list(
            &[
                MapItem { type_code: map_type::HEADER_ITEM, size: 1, offset: 0x100 },
                MapItem { type_code: map_type::STRING_ID_ITEM, size: 1, offset: 0x50 },
            ],
            false,
        )
        .unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn map_list_accepts_any_type_codes_in_range() {
        // The type_code field carries a u16 — emit trusts the caller to
        // pick legitimate codes. Spec-invalid codes are the caller's
        // (assembly layer) problem. Here we verify no constraint is
        // imposed at emit besides offset-ordering.
        let items = [
            MapItem { type_code: 0xFFFF, size: 1, offset: 0 },
            MapItem { type_code: 0xABCD, size: 1, offset: 4 },
        ];
        emit_map_list(&items, false).expect("arbitrary u16 codes ok at emit layer");
    }

    #[test]
    fn map_list_permit_unsorted_accepts_descending_offsets() {
        // Under preserve mode, the validator is skipped. d8/dexopt
        // emit map_items in build-order (not offset-order); when a
        // consumer parses such a DEX and round-trips with
        // `preserve_map_list_order = true`, the input's non-canonical
        // order reaches emit_map_list and must pass.
        let items = [
            MapItem { type_code: map_type::HEADER_ITEM, size: 1, offset: 0x100 },
            MapItem { type_code: map_type::STRING_ID_ITEM, size: 1, offset: 0x50 },
        ];
        let bytes = emit_map_list(&items, true).expect("permit_unsorted accepts");
        assert_eq!(bytes.len(), 4 + 2 * 12);
        // Verify the order in the emitted bytes matches input (not sorted).
        assert_eq!(&bytes[12..16], &0x100u32.to_le_bytes(), "first offset preserved");
        assert_eq!(&bytes[24..28], &0x50u32.to_le_bytes(), "second offset preserved");
    }

    #[test]
    fn reorder_map_items_to_input_matches_input_order() {
        use crate::parser::MapEntry;
        // Builder produces items in fixed push-order (HEADER, STRING_ID,
        // CLASS_DEF, MAP_LIST). Input had items in (STRING_ID, HEADER,
        // MAP_LIST, CLASS_DEF) order. Reordered output must match input.
        let items = vec![
            MapItem { type_code: map_type::HEADER_ITEM, size: 1, offset: 0 },
            MapItem { type_code: map_type::STRING_ID_ITEM, size: 4, offset: 0x70 },
            MapItem { type_code: map_type::CLASS_DEF_ITEM, size: 1, offset: 0xB0 },
            MapItem { type_code: map_type::MAP_LIST, size: 1, offset: 0xD0 },
        ];
        let input = [
            MapEntry { type_code: map_type::STRING_ID_ITEM, size: 4, offset: 0x70 },
            MapEntry { type_code: map_type::HEADER_ITEM, size: 1, offset: 0 },
            MapEntry { type_code: map_type::MAP_LIST, size: 1, offset: 0xD0 },
            MapEntry { type_code: map_type::CLASS_DEF_ITEM, size: 1, offset: 0xB0 },
        ];
        let reordered = reorder_map_items_to_input(items, &input);
        let types: Vec<u16> = reordered.iter().map(|m| m.type_code).collect();
        assert_eq!(
            types,
            vec![
                map_type::STRING_ID_ITEM,
                map_type::HEADER_ITEM,
                map_type::MAP_LIST,
                map_type::CLASS_DEF_ITEM,
            ]
        );
    }

    // ── emit_header ─────────────────────────────────────────────────

    fn minimal_layout() -> HeaderLayout {
        HeaderLayout {
            version: *b"035",
            file_size: 112,
            map_off: 0,
            string_ids_size: 0,
            string_ids_off: 0,
            type_ids_size: 0,
            type_ids_off: 0,
            proto_ids_size: 0,
            proto_ids_off: 0,
            field_ids_size: 0,
            field_ids_off: 0,
            method_ids_size: 0,
            method_ids_off: 0,
            class_defs_size: 0,
            class_defs_off: 0,
            data_size: 0,
            data_off: 0,
        }
    }

    #[test]
    fn header_emits_exactly_112_bytes() {
        let bytes = emit_header(&minimal_layout());
        assert_eq!(bytes.len(), 112);
    }

    #[test]
    fn header_magic_and_version() {
        let bytes = emit_header(&minimal_layout());
        assert_eq!(&bytes[..4], b"dex\n");
        assert_eq!(&bytes[4..7], b"035");
        assert_eq!(bytes[7], 0x00, "null terminator");
    }

    #[test]
    fn header_checksum_and_signature_are_placeholders() {
        let bytes = emit_header(&minimal_layout());
        assert_eq!(&bytes[8..12], &[0u8; 4], "checksum placeholder");
        assert_eq!(&bytes[12..32], &[0u8; 20], "signature placeholder");
    }

    #[test]
    fn header_round_trips_via_parser() {
        let layout = HeaderLayout {
            version: *b"038",
            file_size: 0x1000,
            map_off: 0x0F00,
            string_ids_size: 10,
            string_ids_off: 0x70,
            type_ids_size: 5,
            type_ids_off: 0x98,
            proto_ids_size: 3,
            proto_ids_off: 0xAC,
            field_ids_size: 4,
            field_ids_off: 0xC8,
            method_ids_size: 6,
            method_ids_off: 0xE8,
            class_defs_size: 2,
            class_defs_off: 0x118,
            data_size: 0x300,
            data_off: 0x138,
        };
        let bytes = emit_header(&layout);
        let parsed = crate::header::DexHeader::parse(&bytes).expect("parseable");
        assert_eq!(parsed.version(), "038");
        assert_eq!(parsed.file_size, 0x1000);
        assert_eq!(parsed.header_size, 112);
        assert_eq!(parsed.endian_tag, 0x1234_5678);
        assert_eq!(parsed.link_size, 0);
        assert_eq!(parsed.link_off, 0);
        assert_eq!(parsed.map_off, 0x0F00);
        assert_eq!(parsed.string_ids_size, 10);
        assert_eq!(parsed.string_ids_off, 0x70);
        assert_eq!(parsed.type_ids_size, 5);
        assert_eq!(parsed.type_ids_off, 0x98);
        assert_eq!(parsed.proto_ids_size, 3);
        assert_eq!(parsed.proto_ids_off, 0xAC);
        assert_eq!(parsed.field_ids_size, 4);
        assert_eq!(parsed.field_ids_off, 0xC8);
        assert_eq!(parsed.method_ids_size, 6);
        assert_eq!(parsed.method_ids_off, 0xE8);
        assert_eq!(parsed.class_defs_size, 2);
        assert_eq!(parsed.class_defs_off, 0x118);
        assert_eq!(parsed.data_size, 0x300);
        assert_eq!(parsed.data_off, 0x138);
    }

    #[test]
    fn header_all_supported_versions_round_trip() {
        for ver in [b"035", b"037", b"038", b"039", b"040", b"041"] {
            let mut layout = minimal_layout();
            layout.version = *ver;
            let bytes = emit_header(&layout);
            let parsed = crate::header::DexHeader::parse(&bytes)
                .unwrap_or_else(|e| panic!("version {ver:?}: {e:?}"));
            assert_eq!(parsed.version().as_bytes(), ver);
        }
    }

    // ── finalize_checksums ──────────────────────────────────────────

    /// Build a minimal-well-formed DEX byte sequence for checksum
    /// testing: header + zero-padding out to `file_size` bytes. The
    /// parser's `verify_checksum` is the authoritative oracle —
    /// finalize must produce bytes it accepts.
    fn synthesize_dex_bytes(file_size: u32) -> Vec<u8> {
        let mut layout = minimal_layout();
        layout.file_size = file_size;
        let mut bytes = emit_header(&layout);
        bytes.resize(file_size as usize, 0);
        bytes
    }

    #[test]
    fn finalize_writes_nonzero_signature() {
        let mut bytes = synthesize_dex_bytes(200);
        finalize_checksums(&mut bytes).expect("well-formed");
        // Zero SHA-1 digest of a non-empty input is astronomically
        // unlikely; assert the placeholder was overwritten.
        assert_ne!(&bytes[12..32], &[0u8; 20], "signature populated");
    }

    #[test]
    fn finalize_writes_nonzero_checksum() {
        let mut bytes = synthesize_dex_bytes(200);
        finalize_checksums(&mut bytes).expect("well-formed");
        assert_ne!(&bytes[8..12], &[0u8; 4], "checksum populated");
    }

    #[test]
    fn finalize_produces_bytes_the_parser_accepts() {
        // End-to-end: finalize → DexHeader::parse → verify_checksum.
        let file_size = 256u32;
        let mut bytes = synthesize_dex_bytes(file_size);
        finalize_checksums(&mut bytes).expect("well-formed");
        let header = crate::header::DexHeader::parse(&bytes).expect("parseable");
        header
            .verify_checksum(&bytes)
            .expect("parser's Adler-32 oracle agrees with emit's");
    }

    #[test]
    fn finalize_is_deterministic() {
        // Same input, same output. Bit-identity over two runs.
        let mut a = synthesize_dex_bytes(120);
        let mut b = a.clone();
        finalize_checksums(&mut a).expect("ok");
        finalize_checksums(&mut b).expect("ok");
        assert_eq!(a, b);
    }

    #[test]
    fn finalize_sensitive_to_any_byte_change() {
        let mut a = synthesize_dex_bytes(200);
        let mut b = a.clone();
        // Flip one byte at position 100 (inside the checksum window +
        // inside the SHA-1 window). Both digests must differ.
        b[100] = b[100].wrapping_add(1);
        finalize_checksums(&mut a).expect("ok");
        finalize_checksums(&mut b).expect("ok");
        assert_ne!(&a[8..12], &b[8..12], "Adler-32 distinguishes byte change");
        assert_ne!(&a[12..32], &b[12..32], "SHA-1 distinguishes byte change");
    }

    #[test]
    fn finalize_rejects_too_short_input() {
        let mut short = vec![0u8; 30];
        let err = finalize_checksums(&mut short).unwrap_err();
        assert!(matches!(err, DexEmitError::OffsetOverflow { .. }));
    }

    #[test]
    fn finalize_checksum_window_includes_signature() {
        // Verify the ordering invariant: after finalize, changing the
        // signature bytes (without re-finalizing) must invalidate the
        // adler-32 checksum — proving the adler window covered them.
        let mut bytes = synthesize_dex_bytes(150);
        finalize_checksums(&mut bytes).expect("ok");
        let original_checksum = [bytes[8], bytes[9], bytes[10], bytes[11]];
        // Mutate the signature.
        bytes[15] = bytes[15].wrapping_add(1);
        // Re-verify via parser: should FAIL because we didn't re-
        // compute the checksum.
        let header = crate::header::DexHeader::parse(&bytes).expect("parseable");
        assert!(
            header.verify_checksum(&bytes).is_err(),
            "signature-change must break checksum — proves adler window covers signature bytes"
        );
        // Sanity: the original_checksum was computed over the signed
        // signature bytes, so restoring the signature would make it
        // valid again. (Not asserted; just proves the ordering logic.)
        let _ = original_checksum;
    }

    #[test]
    fn header_layout_field_offsets_match_spec() {
        // Spec §7.2: fixed field positions. Emit must keep them.
        let layout = HeaderLayout {
            string_ids_size: 0xDEAD_BEEF,
            ..minimal_layout()
        };
        let bytes = emit_header(&layout);
        // string_ids_size is at offset 56 per spec.
        assert_eq!(&bytes[56..60], &0xDEAD_BEEFu32.to_le_bytes());
        // data_size is at offset 104.
        let layout2 = HeaderLayout {
            data_size: 0xCAFEu32,
            ..minimal_layout()
        };
        let bytes2 = emit_header(&layout2);
        assert_eq!(&bytes2[104..108], &0xCAFEu32.to_le_bytes());
    }

    #[test]
    fn map_list_type_codes_match_spec() {
        // Spot-check: the constants reflect the DEX spec §7.18 values.
        // Canary against editorial drift.
        assert_eq!(map_type::HEADER_ITEM, 0x0000);
        assert_eq!(map_type::STRING_ID_ITEM, 0x0001);
        assert_eq!(map_type::METHOD_HANDLE_ITEM, 0x0008);
        assert_eq!(map_type::MAP_LIST, 0x1000);
        assert_eq!(map_type::TYPE_LIST, 0x1001);
        assert_eq!(map_type::CLASS_DATA_ITEM, 0x2000);
        assert_eq!(map_type::CODE_ITEM, 0x2001);
        assert_eq!(map_type::ANNOTATION_DIRECTORY_ITEM, 0x2006);
    }

    // ── emit_dex top-level integration ──────────────────────────────

    use crate::header::DexHeader;

    /// Build a minimal valid DexHeader for synthesized test DexFiles.
    /// All size/off fields zero; emit_dex recomputes them from the
    /// pool lengths.
    fn minimal_header() -> DexHeader {
        let mut magic = [0u8; 8];
        magic[..4].copy_from_slice(b"dex\n");
        magic[4..7].copy_from_slice(b"035");
        // magic[7] = 0 (null terminator, already zeroed)
        DexHeader {
            magic,
            checksum: 0,
            signature: [0u8; 20],
            file_size: 0,
            header_size: 112,
            endian_tag: 0x1234_5678,
            link_size: 0,
            link_off: 0,
            map_off: 0,
            string_ids_size: 0,
            string_ids_off: 0,
            type_ids_size: 0,
            type_ids_off: 0,
            proto_ids_size: 0,
            proto_ids_off: 0,
            field_ids_size: 0,
            field_ids_off: 0,
            method_ids_size: 0,
            method_ids_off: 0,
            class_defs_size: 0,
            class_defs_off: 0,
            data_size: 0,
            data_off: 0,
        }
    }

    fn minimal_dexfile() -> crate::parser::DexFile {
        crate::parser::DexFile {
            string_data_offs: Vec::new(),
            header: minimal_header(),
            strings: Vec::new(),
            type_descriptors: Vec::new(),
            protos: Vec::new(),
            fields: Vec::new(),
            methods: Vec::new(),
            class_defs: Vec::new(),
            annotations: std::collections::BTreeMap::new(),
            type_lists: std::collections::BTreeMap::new(),
            class_datas: std::collections::BTreeMap::new(),
            raw_class_data_bytes: std::collections::BTreeMap::new(),
            code_items: std::collections::BTreeMap::new(),
            annotation_sets: std::collections::BTreeMap::new(),
            annotation_set_ref_lists: std::collections::BTreeMap::new(),
            annotation_items: std::collections::BTreeMap::new(),
            annotation_item_widths: std::collections::BTreeMap::new(),
            encoded_arrays: std::collections::BTreeMap::new(),
            encoded_array_widths: std::collections::BTreeMap::new(),
            method_handles: Vec::new(),
            call_site_ids: Vec::new(),
            map_entries: Vec::new(),
            debug_infos: std::collections::BTreeMap::new(),
            debug_info_raw_bytes: std::collections::BTreeMap::new(),
            debug_info_section_layout: Vec::new(),
            annotation_set_section_layout: Vec::new(),
            input_checksums_canonical: true,
            parse_errors: Vec::new(),
            class_def_index: Vec::new(),
        }
    }

    #[test]
    fn emit_dex_empty_file_is_parseable() {
        // Absolutely minimal: no strings, no types, no classes. The
        // output is a valid but semantically vacuous DEX. The point
        // is to prove the pipeline runs end-to-end.
        let dex = minimal_dexfile();
        let bytes = emit_dex(&dex).expect("empty DEX should emit");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("parseable");
        assert_eq!(parsed.strings.len(), 0);
        assert_eq!(parsed.type_descriptors.len(), 0);
        assert_eq!(parsed.class_defs.len(), 0);
        assert_eq!(parsed.header.version(), "035");
        // Checksum must verify against our own Adler-32 compute.
        parsed.header.verify_checksum(&bytes).expect("checksum valid");
    }

    #[test]
    fn emit_dex_with_strings_round_trips_string_content() {
        let mut dex = minimal_dexfile();
        dex.strings = vec![
            crate::DexString::from_decoded_str("Ljava/lang/Object;"),
            crate::DexString::from_decoded_str("Ljava/lang/String;"),
            crate::DexString::from_decoded_str("hello"),
        ];
        let bytes = emit_dex(&dex).expect("strings-only DEX should emit");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("parseable");
        assert_eq!(parsed.strings.len(), 3);
        // NonDecreasing::from_sorted canonicalizes — "Ljava/lang/Object;"
        // < "Ljava/lang/String;" < "hello" in lexical byte order, so
        // the post-round-trip order equals the input order here.
        assert_eq!(parsed.strings[0].as_str_lossy(), "Ljava/lang/Object;");
        assert_eq!(parsed.strings[1].as_str_lossy(), "Ljava/lang/String;");
        assert_eq!(parsed.strings[2].as_str_lossy(), "hello");
    }

    #[test]
    fn emit_dex_with_types_round_trips_type_ids() {
        let mut dex = minimal_dexfile();
        dex.strings = vec![
            crate::DexString::from_decoded_str("Ljava/lang/Object;"),
            crate::DexString::from_decoded_str("V"), // void
            crate::DexString::from_decoded_str("I"), // int
        ];
        // Sort strings so emit's canonical ordering matches our idx
        // expectations. "I" < "Ljava..." < "V" alphabetically. Sort
        // by raw_bytes since DexString does not impl Ord (the bytes
        // are the canonical sort key per DEX spec §7.5).
        dex.strings.sort_by(|a, b| a.raw_bytes().cmp(b.raw_bytes()));
        // Type descriptors are a subset of strings that describe types.
        dex.type_descriptors = vec!["I".to_string(), "Ljava/lang/Object;".to_string()];
        let bytes = emit_dex(&dex).expect("types-only DEX should emit");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("parseable");
        assert_eq!(parsed.type_descriptors.len(), 2);
        // type_ids in DEX format are sorted by the string they
        // reference, so NonDecreasing<StringIdx> canonicalization
        // preserves the pair "I", "Ljava/lang/Object;" in that order.
        assert!(parsed.type_descriptors.contains(&"I".to_string()));
        assert!(parsed.type_descriptors.contains(&"Ljava/lang/Object;".to_string()));
    }

    #[test]
    fn emit_dex_proto_with_parameters_round_trips() {
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("I"), crate::DexString::from_decoded_str("V")];
        dex.type_descriptors = vec!["I".to_string(), "V".to_string()];
        // Proto with parameters: shorty "VI", returns V, one I param.
        dex.protos = vec![ProtoIdItem {
            shorty_idx: StringIdx(1), // "V" is the shorty (void return)
            return_type_idx: TypeIdx(1),
            parameters_off: 0xBEEF, // synthetic key — emit remaps
        }];
        dex.type_lists.insert(0xBEEF, vec![TypeIdx(0)]);
        let bytes = emit_dex(&dex).expect("emit with type_list");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("reparse");
        assert_eq!(parsed.protos.len(), 1);
        assert_ne!(parsed.protos[0].parameters_off, 0, "params_off resolved to new layout");
        assert_eq!(parsed.type_lists.len(), 1);
        let params = parsed.type_lists.values().next().expect("one list");
        assert_eq!(params.as_slice(), &[TypeIdx(0)], "type_list content round-trips");
    }

    #[test]
    fn emit_dex_class_with_interfaces_round_trips() {
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("LA;"), crate::DexString::from_decoded_str("LB;"), crate::DexString::from_decoded_str("LC;")];
        dex.type_descriptors = vec!["LA;".to_string(), "LB;".to_string(), "LC;".to_string()];
        // Class A implements B, C. ClassDefItem.interfaces_off points
        // to a type_list we remap.
        dex.class_defs = vec![ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0xCAFE,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        }];
        dex.type_lists.insert(0xCAFE, vec![TypeIdx(1), TypeIdx(2)]);
        let bytes = emit_dex(&dex).expect("emit with interfaces");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("reparse");
        assert_eq!(parsed.class_defs.len(), 1);
        assert_ne!(parsed.class_defs[0].interfaces_off, 0, "interfaces_off resolved");
        // Parser must have extended type_lists from interfaces.
        assert_eq!(parsed.type_lists.len(), 1);
        let ifaces = parsed.type_lists.values().next().expect("one list");
        assert_eq!(ifaces.as_slice(), &[TypeIdx(1), TypeIdx(2)]);
    }

    #[test]
    fn emit_dex_method_with_return_void_code_item_round_trips() {
        // Class A has one direct method that's just `return-void`.
        // Exercises the full class_data + code_item section pipeline.
        use crate::decode::{ClassData, CodeItem, EncodedMethod, Instruction, PoolIndex, RegList};

        let mut dex = minimal_dexfile();
        // Canonical MUTF-8 order: "LA;" < "V" < "m" (ASCII L=0x4C < V=0x56 < m=0x6D).
        // Post-gauge-fix emit rejects non-sorted pools; we build canonical up-front.
        dex.strings = vec![crate::DexString::from_decoded_str("LA;"), crate::DexString::from_decoded_str("V"), crate::DexString::from_decoded_str("m")];
        dex.type_descriptors = vec!["LA;".to_string(), "V".to_string()];
        // Proto: () -> V with shorty "V". Strings: [0]="LA;", [1]="V", [2]="m".
        dex.protos = vec![ProtoIdItem {
            shorty_idx: StringIdx(1), // "V"
            return_type_idx: TypeIdx(1), // V
            parameters_off: 0, // zero-arg method
        }];
        // Method: A.m()V
        dex.methods = vec![MethodIdItem {
            class_idx: TypeIdx(0),
            proto_idx: ProtoIdx(0),
            name_idx: StringIdx(2), // "m"
        }];
        dex.class_defs = vec![ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0x500,
            static_values_off: 0,
        }];
        // class_data_item pointing to the synthesized code_item key.
        dex.class_datas.insert(
            0x500,
            ClassData {
                static_fields: Vec::new(),
                instance_fields: Vec::new(),
                direct_methods: vec![EncodedMethod {
                    method_idx: MethodIdx(0),
                    access_flags: 0x1, // ACC_PUBLIC
                    code_off: 0x600,
                }],
                virtual_methods: Vec::new(),
            },
        );
        // code_item: single return-void instruction.
        dex.code_items.insert(
            0x600,
            CodeItem {
                registers_size: 1,
                ins_size: 0,
                outs_size: 0,
                debug_info_off: 0,
                instructions: vec![Instruction {
                    addr: 0,
                    op: crate::opcodes::Opcode::ReturnVoid,
                    size: 1,
                    dst: None,
                    src: RegList::empty(),
                    literal: 0,
                    target: None,
                    pool_idx: None,
                }],
                tries: Vec::new(),
                catch_handlers: Vec::new(),
                payloads: std::collections::BTreeMap::new(),
                invariant_violations: Vec::new(),
            },
        );

        let bytes = emit_dex(&dex).expect("emit with method + code");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("reparse");
        // Class data + method survived.
        assert_eq!(parsed.class_defs.len(), 1);
        assert_eq!(parsed.class_datas.len(), 1);
        let cd = parsed.class_datas.values().next().expect("one class_data");
        assert_eq!(cd.direct_methods.len(), 1);
        assert_eq!(cd.direct_methods[0].access_flags, 0x1);
        // Code item survived with the single return-void instruction.
        assert_eq!(parsed.code_items.len(), 1);
        let ci = parsed.code_items.values().next().expect("one code_item");
        assert_eq!(ci.registers_size, 1);
        assert_eq!(ci.instructions.len(), 1);
        assert_eq!(ci.instructions[0].op, crate::opcodes::Opcode::ReturnVoid);
        // Ensure PoolIndex variant survived import for the `_ = ...` lint.
        let _: Option<PoolIndex> = None;
    }

    #[test]
    fn emit_dex_class_with_empty_class_data_round_trips() {
        // class_data with no fields or methods — smallest valid
        // class_data encoding (4 zero ULEBs = 4 bytes).
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("LA;")];
        dex.type_descriptors = vec!["LA;".to_string()];
        dex.class_defs = vec![ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0x200, // synthetic key remapped at emit
            static_values_off: 0,
        }];
        dex.class_datas.insert(
            0x200,
            crate::decode::ClassData {
                static_fields: Vec::new(),
                instance_fields: Vec::new(),
                direct_methods: Vec::new(),
                virtual_methods: Vec::new(),
            },
        );
        let bytes = emit_dex(&dex).expect("emit with empty class_data");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("reparse");
        assert_eq!(parsed.class_defs.len(), 1);
        assert_ne!(parsed.class_defs[0].class_data_off, 0, "class_data_off resolved");
        assert_eq!(parsed.class_datas.len(), 1);
        let cd = parsed.class_datas.values().next().expect("one entry");
        assert_eq!(cd.static_fields.len(), 0);
        assert_eq!(cd.instance_fields.len(), 0);
        assert_eq!(cd.direct_methods.len(), 0);
        assert_eq!(cd.virtual_methods.len(), 0);
    }

    #[test]
    fn emit_dex_produces_ascending_map_list() {
        // Map list must be strictly ascending by offset; verify post-
        // emission that the written map_list validates (emit_map_list
        // itself returns an error on violation, so reaching this point
        // is the proof).
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("a"), crate::DexString::from_decoded_str("b")];
        dex.type_descriptors = vec!["a".to_string()];
        // A class_def that doesn't use any data subsections.
        dex.class_defs = vec![ClassDefItem {
            class_idx: TypeIdx(0),
            access_flags: 0,
            superclass_idx: None,
            interfaces_off: 0,
            source_file_idx: None,
            annotations_off: 0,
            class_data_off: 0,
            static_values_off: 0,
        }];
        let bytes = emit_dex(&dex).expect("minimal class emits");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("parseable");
        assert_eq!(parsed.class_defs.len(), 1);
        assert_eq!(parsed.header.class_defs_size, 1);
    }

    #[test]
    fn emit_dex_round_trips_header_version() {
        for ver in [b"035", b"037", b"038", b"039", b"040", b"041"] {
            let mut dex = minimal_dexfile();
            dex.header.magic[4..7].copy_from_slice(ver);
            let bytes = emit_dex(&dex).unwrap_or_else(|e| panic!("version {ver:?}: {e}"));
            let parsed = crate::parser::DexFile::parse(&bytes, None)
                .unwrap_or_else(|e| panic!("reparse {ver:?}: {e:?}"));
            assert_eq!(parsed.header.version().as_bytes(), ver);
        }
    }

    #[test]
    fn emit_dex_rejects_inconsistent_ir() {
        // type_descriptor that isn't in the strings pool — IR is
        // internally inconsistent, reverse-resolution fails.
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("a")];
        dex.type_descriptors = vec!["not_in_strings".to_string()];
        let err = emit_dex(&dex).unwrap_err();
        assert!(matches!(err, DexEmitError::UnrepresentableIR { .. }));
    }

    #[test]
    fn emit_dex_output_has_correct_file_size_in_header() {
        let mut dex = minimal_dexfile();
        dex.strings = vec![crate::DexString::from_decoded_str("x"), crate::DexString::from_decoded_str("y")];
        let bytes = emit_dex(&dex).expect("ok");
        let parsed = crate::parser::DexFile::parse(&bytes, None).expect("parseable");
        assert_eq!(
            parsed.header.file_size as usize,
            bytes.len(),
            "header.file_size matches actual output length"
        );
    }

    #[test]
    fn emit_debug_info_section_roundtrips_raw_bytes() {
        // Byte-identity gate for the debug_info emitter. Synthetic
        // input mirrors what `scan_debug_info_bytes` produces: each
        // entry is a fully-formed debug_info_item byte stream
        // terminated by DBG_END_SEQUENCE (0x00). The emitter is a
        // pure byte-concatenator — the test locks in that contract.
        let mut raw = std::collections::BTreeMap::new();
        // Entry A at input offset 0x100: line_start=13, params_size=0,
        // DBG_END_SEQUENCE. 3 bytes.
        raw.insert(0x100, vec![0x0D, 0x00, 0x00]);
        // Entry B at input offset 0x200: line_start=9, params_size=1,
        // name_idx+1=19, then one DBG_ADVANCE_LINE(-4) +
        // DBG_END_SEQUENCE. Mimics a real debug_info_item seen in
        // the classes.dex fixture.
        raw.insert(0x200, vec![0x09, 0x01, 0x13, 0x02, 0x7C, 0x00]);

        let (blob, remap) = emit_debug_info_section(&raw).expect("emit");

        // Byte-identity: concatenation of Entry A + Entry B.
        assert_eq!(
            blob,
            vec![0x0D, 0x00, 0x00, 0x09, 0x01, 0x13, 0x02, 0x7C, 0x00]
        );
        // Remap: original_off → local_off.
        assert_eq!(remap.get(&0x100), Some(&0));
        assert_eq!(remap.get(&0x200), Some(&3));
    }

    #[test]
    fn emit_debug_info_section_empty_map_is_empty_blob() {
        let empty = std::collections::BTreeMap::new();
        let (blob, remap) = emit_debug_info_section(&empty).expect("ok");
        assert!(blob.is_empty());
        assert!(remap.is_empty());
    }

    #[test]
    fn preserve_map_list_order_default_is_false() {
        // The toggle ships off by default; existing callers don't
        // change behaviour, and the spec-strict map-list ordering is
        // preserved unless a downstream consumer explicitly opts in.
        let cfg = EmitConfig::default();
        assert!(!cfg.preserve_map_list_order);
    }

    #[test]
    fn emit_debug_info_section_fixture_roundtrip() {
        // End-to-end: parse the real fixture, emit its debug_info
        // section alone, confirm remap cardinality matches parser
        // state. This is the integration-ish check at unit-test
        // level — full emit_dex integration lands in the next commit.
        let data = include_bytes!("../tests/fixtures/classes.dex");
        let dex = crate::parser::DexFile::parse(data, None).expect("fixture parses");
        let (blob, remap) =
            emit_debug_info_section(&dex.debug_info_raw_bytes).expect("emit");
        assert_eq!(remap.len(), dex.debug_info_raw_bytes.len());
        assert_eq!(
            blob.len(),
            dex.debug_info_raw_bytes.values().map(|v| v.len()).sum::<usize>()
        );
        // Each remap value + raw-bytes length equals the blob slice.
        for (orig_off, local_off) in &remap {
            let raw = dex.debug_info_raw_bytes.get(orig_off).unwrap();
            let start = *local_off as usize;
            let end = start + raw.len();
            assert_eq!(&blob[start..end], raw.as_slice());
        }
    }

    // ── D5 alignment-padding observation tests ────────────────────────

    #[test]
    fn alignment_section_map_type_codes_round_trip() {
        // Every variant must have a valid map type code; no None
        // returns. (If a variant ever needs a None, that's a sign the
        // observation logic must use an alternative input-side source.)
        for section in [
            AlignmentSection::CodeItem,
            AlignmentSection::AnnotationSet,
            AlignmentSection::AnnotationSetRefList,
            AlignmentSection::AnnotationDirectory,
            AlignmentSection::TypeList,
            AlignmentSection::MapList,
        ] {
            assert!(
                section.map_type_code().is_some(),
                "AlignmentSection::{section:?} must map to a DEX map type code"
            );
        }
    }

    #[test]
    fn input_section_offset_returns_map_entry_offset() {
        // Construct a synthetic dex with one map entry per AlignmentSection;
        // verify input_section_offset returns the planted offset for each.
        let mut dex = minimal_dexfile();
        dex.map_entries = vec![
            crate::parser::MapEntry { type_code: 0x2001, size: 1, offset: 1000 }, // CodeItem
            crate::parser::MapEntry { type_code: 0x1003, size: 1, offset: 2000 }, // AnnotationSet
            crate::parser::MapEntry { type_code: 0x1002, size: 1, offset: 3000 }, // AnnotationSetRefList
            crate::parser::MapEntry { type_code: 0x2006, size: 1, offset: 4000 }, // AnnotationDirectory
            crate::parser::MapEntry { type_code: 0x1001, size: 1, offset: 5000 }, // TypeList
            crate::parser::MapEntry { type_code: 0x1000, size: 1, offset: 6000 }, // MapList
        ];
        assert_eq!(input_section_offset(&dex, AlignmentSection::CodeItem), Some(1000));
        assert_eq!(input_section_offset(&dex, AlignmentSection::AnnotationSet), Some(2000));
        assert_eq!(
            input_section_offset(&dex, AlignmentSection::AnnotationSetRefList),
            Some(3000)
        );
        assert_eq!(
            input_section_offset(&dex, AlignmentSection::AnnotationDirectory),
            Some(4000)
        );
        assert_eq!(input_section_offset(&dex, AlignmentSection::TypeList), Some(5000));
        assert_eq!(input_section_offset(&dex, AlignmentSection::MapList), Some(6000));
    }

    #[test]
    fn input_section_offset_returns_none_for_missing_map_entry() {
        // No map_entries → no input baseline → None for every section.
        // Observation logic must NOT push the variant in this case
        // (per the input_section_offset contract documented at its
        // doc-comment: "returning None means input layout unknown;
        // variant NOT pushed").
        let dex = minimal_dexfile();
        assert!(dex.map_entries.is_empty());
        for section in [
            AlignmentSection::CodeItem,
            AlignmentSection::AnnotationSet,
            AlignmentSection::AnnotationSetRefList,
            AlignmentSection::AnnotationDirectory,
            AlignmentSection::TypeList,
            AlignmentSection::MapList,
        ] {
            assert_eq!(
                input_section_offset(&dex, section),
                None,
                "expected None for AlignmentSection::{section:?} when map_entries empty"
            );
        }
    }

    // ── D7 map_list_order_diverged tests ──────────────────────────────

    fn mi(type_code: u16, offset: u32) -> MapItem {
        MapItem { type_code, size: 1, offset }
    }

    fn me(type_code: u16, offset: u32) -> crate::parser::MapEntry {
        crate::parser::MapEntry { type_code, size: 1, offset }
    }

    #[test]
    fn map_list_order_diverged_false_on_matching_order() {
        // Identical type-code sequences (offset-sorted both) → no
        // canonicalization needed → false.
        let emit = vec![mi(0x0000, 0), mi(0x0001, 100), mi(0x0002, 200)];
        let input = vec![me(0x0000, 0), me(0x0001, 100), me(0x0002, 200)];
        assert!(!map_list_order_diverged(&emit, &input));
    }

    #[test]
    fn map_list_order_diverged_true_on_swapped_order() {
        // Input has 0x0002 before 0x0001 (non-canonical); emit's
        // offset-sort produces 0x0001 then 0x0002. Reorder happened.
        let emit = vec![mi(0x0000, 0), mi(0x0001, 100), mi(0x0002, 200)];
        let input = vec![me(0x0000, 0), me(0x0002, 200), me(0x0001, 100)];
        assert!(map_list_order_diverged(&emit, &input));
    }

    #[test]
    fn map_list_order_diverged_false_when_input_empty() {
        // No input map_entries → no comparison possible → conservative
        // false (preserves invariant: no spurious push on inputs that
        // the parser couldn't reconstruct).
        let emit = vec![mi(0x0000, 0), mi(0x0001, 100)];
        let input: Vec<crate::parser::MapEntry> = vec![];
        assert!(!map_list_order_diverged(&emit, &input));
    }

    #[test]
    fn map_list_order_diverged_ignores_emit_only_types() {
        // Emit produces a type the input lacks (0x0003). The
        // intersection is {0x0000, 0x0001} which is in matching
        // order on both sides → false (additive sections don't
        // constitute reordering).
        let emit = vec![mi(0x0000, 0), mi(0x0001, 100), mi(0x0003, 300)];
        let input = vec![me(0x0000, 0), me(0x0001, 100)];
        assert!(!map_list_order_diverged(&emit, &input));
    }

    // ── D6 encoded_value width-canonicalization tests ────────────────

    #[test]
    fn min_emit_width_byte_is_always_one() {
        // VALUE_BYTE always 1 byte per spec §VII.1; width-canonicalization
        // is undefined (a wider input would be malformed).
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Byte(0)), Some(1));
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Byte(127)), Some(1));
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Byte(-128)), Some(1));
    }

    #[test]
    fn min_emit_width_int_uses_min_signed_bytes() {
        // Int(0) fits in 1 byte; Int(0xFFFF) in 3 (signed); Int(0x7FFFFFFF) in 4.
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Int(0)), Some(1));
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Int(127)), Some(1));
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Int(128)), Some(2));
        assert_eq!(
            encoded_value_min_emit_width(&EncodedValue::Int(0x7FFFFFFF)),
            Some(4)
        );
    }

    #[test]
    fn min_emit_width_float_double_full_width() {
        // Spec allows trailing-zero trim but emit picks always-full;
        // attribution mirrors emit's choice.
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Float(1.0)), Some(4));
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Double(1.0)), Some(8));
    }

    #[test]
    fn min_emit_width_variable_returns_none() {
        // Array, Annotation, Null, Boolean: width attribution
        // undefined; helper returns None so they don't contribute to
        // the divergence count.
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Null), None);
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Boolean(true)), None);
        assert_eq!(encoded_value_min_emit_width(&EncodedValue::Array(vec![])), None);
    }

    #[test]
    fn min_emit_width_method_type_handle_use_min_unsigned_bytes() {
        // VALUE_METHOD_TYPE + VALUE_METHOD_HANDLE are width-variable
        // index types per `emit_encoded_value_with_depth`; the helper
        // must mirror.
        use crate::ids::{MethodHandleIdx, ProtoIdx};
        assert_eq!(
            encoded_value_min_emit_width(&EncodedValue::MethodType(ProtoIdx(0))),
            Some(1)
        );
        assert_eq!(
            encoded_value_min_emit_width(&EncodedValue::MethodType(ProtoIdx(0xFFFF))),
            Some(2)
        );
        assert_eq!(
            encoded_value_min_emit_width(&EncodedValue::MethodHandle(MethodHandleIdx(0))),
            Some(1)
        );
        assert_eq!(
            encoded_value_min_emit_width(&EncodedValue::MethodHandle(MethodHandleIdx(0xFFFFFF))),
            Some(3)
        );
    }

    #[test]
    fn count_encoded_value_width_divergences_zero_on_minimal() {
        // Empty dex → zero encoded_arrays → zero divergences.
        let dex = minimal_dexfile();
        assert_eq!(count_encoded_value_width_divergences(&dex), 0);
    }

    #[test]
    fn count_encoded_value_width_divergences_fires_on_wider_input() {
        // Synthetic: Int(0) value with input width 4 (wider than
        // emit's min-width of 1). Must count as 1 divergence.
        let mut dex = minimal_dexfile();
        dex.encoded_arrays.insert(0x100, vec![EncodedValue::Int(0)]);
        dex.encoded_array_widths.insert(0x100, vec![4]);
        assert_eq!(count_encoded_value_width_divergences(&dex), 1);
    }

    #[test]
    fn count_encoded_value_width_divergences_zero_on_matching_width() {
        // Input width matches emit's min-width → no divergence.
        let mut dex = minimal_dexfile();
        dex.encoded_arrays.insert(0x100, vec![EncodedValue::Int(0)]);
        dex.encoded_array_widths.insert(0x100, vec![1]);
        assert_eq!(count_encoded_value_width_divergences(&dex), 0);
    }

    #[test]
    fn count_encoded_value_width_divergences_skips_variable_length() {
        // Array values' widths are meaningless; helper returns None
        // and the divergence count skips them. Synthetic case: an
        // Array with input_width=4 (nonsensical but tolerant).
        let mut dex = minimal_dexfile();
        dex.encoded_arrays
            .insert(0x100, vec![EncodedValue::Array(vec![])]);
        dex.encoded_array_widths.insert(0x100, vec![4]);
        assert_eq!(count_encoded_value_width_divergences(&dex), 0);
    }

    #[test]
    fn count_encoded_value_width_divergences_handles_missing_widths_table() {
        // If `encoded_array_widths` lacks an entry for a key in
        // `encoded_arrays` (shouldn't happen post-parser, but the
        // helper must be tolerant — possible on hand-built IR), skip
        // that array's contribution rather than panic.
        let mut dex = minimal_dexfile();
        dex.encoded_arrays.insert(0x100, vec![EncodedValue::Int(0)]);
        // Intentionally NOT inserting into encoded_array_widths.
        assert_eq!(count_encoded_value_width_divergences(&dex), 0);
    }

    // ── emit_encoded_value_at_width ─────────────────────────────────

    #[test]
    fn emit_at_width_int_wider_than_min_writes_requested_bytes() {
        // Int(0): min_width = 1; request width = 4. Output is
        // VT_INT-header with size=4 in the value_arg bits, plus 4
        // zero payload bytes (little-endian 0).
        let mut out = Vec::new();
        assert!(emit_encoded_value_at_width(&mut out, &EncodedValue::Int(0), 4));
        // Header: VT_INT (0x04) | ((4-1) << 5) = 0x04 | 0x60 = 0x64
        assert_eq!(out, vec![0x64, 0, 0, 0, 0]);
    }

    #[test]
    fn emit_at_width_int_too_narrow_returns_false_value_fits_defense() {
        // Int(0xFF_FF_FF_FF as i32 = -1): min_signed_bytes = 1 (fits in i8).
        // Request width = 1 → OK (preserves wider parser interp).
        // For value 0x10000 (min_signed_bytes = 3): width=1 won't fit.
        let mut out = Vec::new();
        assert!(
            !emit_encoded_value_at_width(&mut out, &EncodedValue::Int(0x1_0000), 1),
            "width=1 cannot represent Int(0x10000) — must return false"
        );
        assert!(out.is_empty(), "fallback must not have written anything");
    }

    #[test]
    fn emit_at_width_int_exceeding_type_max_returns_false() {
        // VT_INT max width is 4 per DEX §VII.1. Width=8 is malformed
        // for VT_INT (would be VT_LONG-shaped); must reject.
        let mut out = Vec::new();
        assert!(!emit_encoded_value_at_width(&mut out, &EncodedValue::Int(0), 8));
        assert!(out.is_empty());
    }

    #[test]
    fn emit_at_width_array_returns_false_variable_shape() {
        // Variable-shape (Array/Annotation): width-preserve N/A.
        let mut out = Vec::new();
        assert!(!emit_encoded_value_at_width(
            &mut out,
            &EncodedValue::Array(vec![]),
            1
        ));
    }

    #[test]
    fn emit_at_width_byte_returns_false_fixed_width() {
        // Byte has fixed width 1; no preservation needed. The toggle
        // bypasses the helper and falls back to the canonical path.
        let mut out = Vec::new();
        assert!(!emit_encoded_value_at_width(&mut out, &EncodedValue::Byte(0), 1));
    }

    #[test]
    fn emit_at_width_out_of_range_returns_false() {
        let mut out = Vec::new();
        assert!(!emit_encoded_value_at_width(&mut out, &EncodedValue::Int(0), 0));
        assert!(!emit_encoded_value_at_width(&mut out, &EncodedValue::Int(0), 9));
    }

    #[test]
    fn emit_encoded_array_with_widths_preserves_wider_int() {
        // Two values: Int(0) min_width=1; preserved width=4 → wider.
        // Without preservation: 2 header bytes + 2 payload bytes = 4 bytes.
        // With preservation: 2 header bytes + 8 payload bytes = 10 bytes.
        // (Plus 1 byte uleb128 array-length prefix.)
        let values = vec![EncodedValue::Int(0), EncodedValue::Int(0)];
        let widths = [4u8, 4];
        let preserved = emit_encoded_array_with_widths(&values, Some(&widths))
            .expect("preserved emit");
        let canonical = emit_encoded_array_with_widths(&values, None)
            .expect("canonical emit");
        assert_eq!(preserved.len(), 1 + 2 * 5, "1B uleb + 2 × (1B header + 4B payload)");
        assert_eq!(canonical.len(), 1 + 2 * 2, "1B uleb + 2 × (1B header + 1B payload)");
        assert!(
            preserved.len() > canonical.len(),
            "preserved widths produce strictly more bytes"
        );
    }

    #[test]
    fn derive_applied_transformations_skips_reencoded_under_preserve() {
        // Synthetic: Int(0) with input width 4 (wider than min=1).
        // Default config → EncodedValueReencoded { count: 1 } fires.
        // Preserve config → variant absent (emit did not re-encode).
        let mut dex = minimal_dexfile();
        dex.encoded_arrays.insert(0x100, vec![EncodedValue::Int(0)]);
        dex.encoded_array_widths.insert(0x100, vec![4]);
        let default_xforms = derive_applied_transformations(&dex, &EmitConfig::default());
        let preserve_xforms = derive_applied_transformations(
            &dex,
            &EmitConfig { preserve_encoded_value_width: true, ..Default::default() },
        );
        assert!(
            default_xforms
                .iter()
                .any(|t| matches!(t, CanonicalTransform::EncodedValueReencoded { .. })),
            "default config must report EncodedValueReencoded on wider input"
        );
        assert!(
            !preserve_xforms
                .iter()
                .any(|t| matches!(t, CanonicalTransform::EncodedValueReencoded { .. })),
            "preserve config must NOT report EncodedValueReencoded"
        );
    }
}
