//! Bitcoin transaction type with wire format and EF format serialization.

use std::io::{Cursor, Read, Write};

use crate::primitives::hash::hash256;
use crate::primitives::transaction_signature::{
    SIGHASH_ANYONECANPAY, SIGHASH_FORKID, SIGHASH_NONE, SIGHASH_SINGLE,
};
use crate::script::locking_script::LockingScript;
use crate::script::templates::ScriptTemplateUnlock;
use crate::transaction::error::TransactionError;
use crate::transaction::merkle_path::MerklePath;
use crate::transaction::transaction_input::TransactionInput;
use crate::transaction::transaction_output::TransactionOutput;
use crate::transaction::{
    read_u32_le, read_u64_le, read_varint, write_u32_le, write_u64_le, write_varint,
};

/// EF format marker bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0xEF]
const EF_MARKER: [u8; 6] = [0x00, 0x00, 0x00, 0x00, 0x00, 0xEF];

/// A Bitcoin transaction with inputs, outputs, and optional merkle proof.
///
/// Supports standard binary and Extended Format (EF) serialization,
/// BEEF/Atomic BEEF packaging, and BIP-143 sighash preimage computation
/// for signing. Translates the TS SDK Transaction.ts.
#[derive(Debug, Clone)]
pub struct Transaction {
    /// Transaction version number.
    pub version: u32,
    /// Transaction inputs.
    pub inputs: Vec<TransactionInput>,
    /// Transaction outputs.
    pub outputs: Vec<TransactionOutput>,
    /// Lock time.
    pub lock_time: u32,
    /// Merkle path for SPV verification (populated from BEEF).
    pub merkle_path: Option<MerklePath>,
}

impl Transaction {
    /// Create a new empty transaction with default values.
    pub fn new() -> Self {
        Self {
            version: 1,
            inputs: Vec::new(),
            outputs: Vec::new(),
            lock_time: 0,
            merkle_path: None,
        }
    }

    /// Deserialize a transaction from binary wire format.
    pub fn from_binary(reader: &mut impl Read) -> Result<Self, TransactionError> {
        let version = read_u32_le(reader)?;

        let input_count = read_varint(reader)? as usize;
        let mut inputs = Vec::with_capacity(input_count);
        for _ in 0..input_count {
            inputs.push(TransactionInput::from_binary(reader)?);
        }

        let output_count = read_varint(reader)? as usize;
        let mut outputs = Vec::with_capacity(output_count);
        for _ in 0..output_count {
            outputs.push(TransactionOutput::from_binary(reader)?);
        }

        let lock_time = read_u32_le(reader)?;

        Ok(Transaction {
            version,
            inputs,
            outputs,
            lock_time,
            merkle_path: None,
        })
    }

    /// Deserialize a transaction from a hex string.
    pub fn from_hex(hex: &str) -> Result<Self, TransactionError> {
        let bytes = hex_to_bytes(hex)
            .map_err(|e| TransactionError::InvalidFormat(format!("invalid hex: {}", e)))?;
        let mut cursor = Cursor::new(bytes);
        Self::from_binary(&mut cursor)
    }

    /// Parse a transaction from a BEEF hex string, returning the subject transaction.
    ///
    /// Decodes the hex to bytes, parses the BEEF structure, and extracts the
    /// subject transaction (the last tx, or the atomic txid target).
    pub fn from_beef(beef_hex: &str) -> Result<Self, TransactionError> {
        let beef = crate::transaction::beef::Beef::from_hex(beef_hex)?;
        beef.into_transaction()
    }

    /// Serialize a transaction to binary wire format.
    pub fn to_binary(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
        write_u32_le(writer, self.version)?;

        write_varint(writer, self.inputs.len() as u64)?;
        for input in &self.inputs {
            input.to_binary(writer)?;
        }

        write_varint(writer, self.outputs.len() as u64)?;
        for output in &self.outputs {
            output.to_binary(writer)?;
        }

        write_u32_le(writer, self.lock_time)?;
        Ok(())
    }

    /// Serialize a transaction to a hex string.
    pub fn to_hex(&self) -> Result<String, TransactionError> {
        let bytes = self.to_bytes()?;
        Ok(bytes_to_hex(&bytes))
    }

    /// Serialize a transaction to a byte vector.
    pub fn to_bytes(&self) -> Result<Vec<u8>, TransactionError> {
        let mut buf = Vec::new();
        self.to_binary(&mut buf)?;
        Ok(buf)
    }

    /// Serialize this transaction and its input chain to BEEF (BRC-62) format.
    ///
    /// Walks the `source_transaction` tree recursively to collect all ancestor
    /// transactions and their merkle paths, then delegates to `Beef` for
    /// deduplication, topological sorting, and serialization.
    /// Matches the TS SDK's `Transaction.toBEEF()`.
    pub fn to_beef(&self) -> Result<Vec<u8>, TransactionError> {
        use crate::transaction::beef::{Beef, BEEF_V1};
        use crate::transaction::beef_tx::BeefTx;
        use std::collections::HashSet;

        let mut beef = Beef::new(BEEF_V1);
        let mut seen = HashSet::new();

        fn collect(
            tx: &Transaction,
            beef: &mut Beef,
            seen: &mut HashSet<String>,
        ) -> Result<(), TransactionError> {
            let txid = tx.id()?;
            if seen.contains(&txid) {
                return Ok(());
            }
            seen.insert(txid.clone());

            if let Some(ref mp) = tx.merkle_path {
                // Proven tx: merge its bump and add with the resulting index.
                let bump_idx = beef.merge_bump(mp)?;
                let btx = BeefTx::from_tx(tx.clone(), Some(bump_idx))?;
                beef.remove_existing_txid(&btx.txid);
                beef.txs.push(btx);
            } else {
                // Unproven tx: recurse into source transactions first.
                for input in &tx.inputs {
                    if let Some(ref source_tx) = input.source_transaction {
                        collect(source_tx, beef, seen)?;
                    } else {
                        return Err(TransactionError::BeefError(format!(
                            "input spending {}:{} has no source transaction and tx {} has no merkle proof — cannot build BEEF",
                            input.source_txid.as_deref().unwrap_or("unknown"),
                            input.source_output_index,
                            txid,
                        )));
                    }
                }
                let btx = BeefTx::from_tx(tx.clone(), None)?;
                beef.remove_existing_txid(&btx.txid);
                beef.txs.push(btx);
            }

            Ok(())
        }

        collect(self, &mut beef, &mut seen)?;

        if beef.bumps.is_empty() {
            return Err(TransactionError::BeefError(
                "cannot produce BEEF: no merkle proofs found in the transaction chain".to_string(),
            ));
        }

        beef.sort_txs();

        let mut buf = Vec::new();
        beef.to_binary(&mut buf)?;
        Ok(buf)
    }

    /// Compute the transaction hash (double SHA-256).
    ///
    /// Returns the hash in internal byte order (LE).
    pub fn hash(&self) -> Result<[u8; 32], TransactionError> {
        let bytes = self.to_bytes()?;
        Ok(hash256(&bytes))
    }

    /// Compute the transaction ID (hash reversed, hex-encoded).
    ///
    /// Returns the txid in display format (BE hex).
    pub fn id(&self) -> Result<String, TransactionError> {
        let mut h = self.hash()?;
        h.reverse();
        Ok(bytes_to_hex(&h))
    }

    /// Add an input to the transaction.
    pub fn add_input(&mut self, input: TransactionInput) {
        self.inputs.push(input);
    }

    /// Add an output to the transaction.
    pub fn add_output(&mut self, output: TransactionOutput) {
        self.outputs.push(output);
    }

    /// Deserialize a transaction from EF format (BRC-30).
    ///
    /// EF format: version(4) + EF_MARKER(6) + inputs_with_source_info + outputs + locktime(4)
    /// Each input additionally includes source satoshis (u64 LE) and source locking script.
    pub fn from_ef(reader: &mut impl Read) -> Result<Self, TransactionError> {
        let version = read_u32_le(reader)?;

        // Read and verify the 6-byte EF marker
        let mut marker = [0u8; 6];
        reader.read_exact(&mut marker)?;
        if marker != EF_MARKER {
            return Err(TransactionError::InvalidFormat(
                "invalid EF marker".to_string(),
            ));
        }

        let input_count = read_varint(reader)? as usize;
        let mut inputs = Vec::with_capacity(input_count);
        for _ in 0..input_count {
            // Read standard input fields
            let mut input = TransactionInput::from_binary(reader)?;

            // Read source satoshis (u64 LE)
            let source_satoshis = read_u64_le(reader)?;

            // Read source locking script (varint + bytes)
            let script_len = read_varint(reader)? as usize;
            let mut script_bytes = vec![0u8; script_len];
            if script_len > 0 {
                reader.read_exact(&mut script_bytes)?;
            }
            let source_locking_script = LockingScript::from_binary(&script_bytes);

            // Create a minimal source transaction with one output at the referenced index
            let mut source_tx = Transaction::new();
            // Pad outputs up to the referenced index
            for _ in 0..input.source_output_index {
                source_tx.outputs.push(TransactionOutput::default());
            }
            source_tx.outputs.push(TransactionOutput {
                satoshis: Some(source_satoshis),
                locking_script: source_locking_script,
                change: false,
            });
            input.source_transaction = Some(Box::new(source_tx));

            inputs.push(input);
        }

        let output_count = read_varint(reader)? as usize;
        let mut outputs = Vec::with_capacity(output_count);
        for _ in 0..output_count {
            outputs.push(TransactionOutput::from_binary(reader)?);
        }

        let lock_time = read_u32_le(reader)?;

        Ok(Transaction {
            version,
            inputs,
            outputs,
            lock_time,
            merkle_path: None,
        })
    }

    /// Deserialize a transaction from an EF format hex string.
    pub fn from_hex_ef(hex: &str) -> Result<Self, TransactionError> {
        let bytes = hex_to_bytes(hex)
            .map_err(|e| TransactionError::InvalidFormat(format!("invalid hex: {}", e)))?;
        let mut cursor = Cursor::new(bytes);
        Self::from_ef(&mut cursor)
    }

    /// Serialize a transaction to EF format (BRC-30).
    pub fn to_ef(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
        write_u32_le(writer, self.version)?;

        // Write EF marker
        writer.write_all(&EF_MARKER)?;

        write_varint(writer, self.inputs.len() as u64)?;
        for input in &self.inputs {
            // Write standard input fields
            input.to_binary(writer)?;

            // Write source satoshis and locking script from source transaction
            if let Some(ref source_tx) = input.source_transaction {
                let idx = input.source_output_index as usize;
                if idx < source_tx.outputs.len() {
                    let source_output = &source_tx.outputs[idx];
                    write_u64_le(writer, source_output.satoshis.unwrap_or(0))?;
                    let script_bin = source_output.locking_script.to_binary();
                    write_varint(writer, script_bin.len() as u64)?;
                    writer.write_all(&script_bin)?;
                } else {
                    return Err(TransactionError::MissingSourceTransaction);
                }
            } else {
                return Err(TransactionError::MissingSourceTransaction);
            }
        }

        write_varint(writer, self.outputs.len() as u64)?;
        for output in &self.outputs {
            output.to_binary(writer)?;
        }

        write_u32_le(writer, self.lock_time)?;
        Ok(())
    }

    /// Serialize a transaction to an EF format hex string.
    pub fn to_hex_ef(&self) -> Result<String, TransactionError> {
        let mut buf = Vec::new();
        self.to_ef(&mut buf)?;
        Ok(bytes_to_hex(&buf))
    }

    // -- Sighash preimage computation -----------------------------------------

    /// Resolve the txid bytes (internal/LE byte order) for the input at `input_index`.
    fn resolve_input_txid_bytes(&self, input_index: usize) -> Result<[u8; 32], TransactionError> {
        let input = &self.inputs[input_index];
        if let Some(ref txid) = input.source_txid {
            let mut bytes = hex_to_bytes(txid)
                .map_err(|e| TransactionError::InvalidFormat(format!("invalid txid hex: {}", e)))?;
            bytes.reverse(); // display (BE) -> internal (LE)
            let mut arr = [0u8; 32];
            if bytes.len() == 32 {
                arr.copy_from_slice(&bytes);
            }
            Ok(arr)
        } else if let Some(ref source_tx) = input.source_transaction {
            source_tx.hash()
        } else {
            Err(TransactionError::InvalidFormat(
                "input has neither source_txid nor source_transaction".to_string(),
            ))
        }
    }

    /// Compute the BIP143/ForkID sighash preimage for the input at `input_index`.
    ///
    /// This is the standard BSV post-fork sighash format. The `scope` flags
    /// should include SIGHASH_FORKID for normal BSV transactions.
    ///
    /// Parameters:
    /// - `input_index`: index of the input being signed
    /// - `scope`: sighash flags (e.g., SIGHASH_ALL | SIGHASH_FORKID)
    /// - `source_satoshis`: value of the UTXO being spent
    /// - `source_locking_script`: locking script of the UTXO being spent
    pub fn sighash_preimage(
        &self,
        input_index: usize,
        scope: u32,
        source_satoshis: u64,
        source_locking_script: &LockingScript,
    ) -> Result<Vec<u8>, TransactionError> {
        if input_index >= self.inputs.len() {
            return Err(TransactionError::InvalidSighash(format!(
                "input_index {} out of range (tx has {} inputs)",
                input_index,
                self.inputs.len()
            )));
        }

        let base_type = scope & 0x1f;
        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;

        let mut preimage = Vec::with_capacity(256);

        // 1. nVersion (4 bytes LE)
        preimage.extend_from_slice(&self.version.to_le_bytes());

        // 2. hashPrevouts
        if !anyone_can_pay {
            let mut prevouts = Vec::new();
            for (i, input) in self.inputs.iter().enumerate() {
                let txid_bytes = self.resolve_input_txid_bytes(i)?;
                prevouts.extend_from_slice(&txid_bytes);
                prevouts.extend_from_slice(&input.source_output_index.to_le_bytes());
            }
            preimage.extend_from_slice(&hash256(&prevouts));
        } else {
            preimage.extend_from_slice(&[0u8; 32]);
        }

        // 3. hashSequence
        if !anyone_can_pay && base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
            let mut sequences = Vec::new();
            for input in &self.inputs {
                sequences.extend_from_slice(&input.sequence.to_le_bytes());
            }
            preimage.extend_from_slice(&hash256(&sequences));
        } else {
            preimage.extend_from_slice(&[0u8; 32]);
        }

        // 4. outpoint: this input's txid (LE) + output_index (4 bytes LE)
        let this_txid = self.resolve_input_txid_bytes(input_index)?;
        preimage.extend_from_slice(&this_txid);
        preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());

        // 5. scriptCode: varint-prefixed source_locking_script bytes
        let script_bytes = source_locking_script.to_binary();
        write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
        preimage.extend_from_slice(&script_bytes);

        // 6. value: source_satoshis (8 bytes LE)
        preimage.extend_from_slice(&source_satoshis.to_le_bytes());

        // 7. nSequence: this input's sequence (4 bytes LE)
        preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());

        // 8. hashOutputs
        if base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
            // ALL: hash of all outputs serialized
            let mut outputs_data = Vec::new();
            for output in &self.outputs {
                outputs_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
                let script_bytes = output.locking_script.to_binary();
                write_varint_to_vec(&mut outputs_data, script_bytes.len() as u64);
                outputs_data.extend_from_slice(&script_bytes);
            }
            preimage.extend_from_slice(&hash256(&outputs_data));
        } else if base_type == SIGHASH_SINGLE && input_index < self.outputs.len() {
            // SINGLE: hash of the output at input_index
            let output = &self.outputs[input_index];
            let mut out_data = Vec::new();
            out_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
            let script_bytes = output.locking_script.to_binary();
            write_varint_to_vec(&mut out_data, script_bytes.len() as u64);
            out_data.extend_from_slice(&script_bytes);
            preimage.extend_from_slice(&hash256(&out_data));
        } else {
            // NONE or SINGLE out-of-range: 32 zero bytes
            preimage.extend_from_slice(&[0u8; 32]);
        }

        // 9. nLockTime (4 bytes LE)
        preimage.extend_from_slice(&self.lock_time.to_le_bytes());

        // 10. sighash type (4 bytes LE) -- scope with FORKID bit
        preimage.extend_from_slice(&(scope | SIGHASH_FORKID).to_le_bytes());

        Ok(preimage)
    }

    /// Compute the legacy OTDA sighash preimage for the input at `input_index`.
    ///
    /// Used when SIGHASH_FORKID is NOT set (pre-fork transactions or Chronicle mode).
    /// This is the original Bitcoin sighash algorithm.
    ///
    /// The `sub_script` is the scriptCode bytes. OP_CODESEPARATOR opcodes will be
    /// stripped automatically before inclusion in the preimage.
    pub fn sighash_preimage_legacy(
        &self,
        input_index: usize,
        scope: u32,
        sub_script: &[u8],
    ) -> Result<Vec<u8>, TransactionError> {
        if input_index >= self.inputs.len() {
            return Err(TransactionError::InvalidSighash(format!(
                "input_index {} out of range (tx has {} inputs)",
                input_index,
                self.inputs.len()
            )));
        }

        // Strip OP_CODESEPARATOR (0xab) opcodes from the script.
        // Must parse properly to avoid removing 0xab bytes that appear as push data.
        let sub_script = strip_codeseparator(sub_script);

        let base_type = scope & 0x1f;
        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;
        let is_none = base_type == SIGHASH_NONE;
        let is_single = base_type == SIGHASH_SINGLE;

        // SIGHASH_SINGLE bug: if input_index >= outputs, return [1, 0, 0, ..., 0]
        if is_single && input_index >= self.outputs.len() {
            let mut result = vec![0u8; 32];
            result[0] = 1;
            return Ok(result);
        }

        let empty_script: Vec<u8> = Vec::new();

        let mut preimage = Vec::with_capacity(512);

        // Version
        preimage.extend_from_slice(&self.version.to_le_bytes());

        // Inputs
        if anyone_can_pay {
            // Only the current input
            write_varint_to_vec(&mut preimage, 1);
            let txid_bytes = self.resolve_input_txid_bytes(input_index)?;
            preimage.extend_from_slice(&txid_bytes);
            preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());
            write_varint_to_vec(&mut preimage, sub_script.len() as u64);
            preimage.extend_from_slice(&sub_script);
            preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());
        } else {
            write_varint_to_vec(&mut preimage, self.inputs.len() as u64);
            for (i, input) in self.inputs.iter().enumerate() {
                let txid_bytes = self.resolve_input_txid_bytes(i)?;
                preimage.extend_from_slice(&txid_bytes);
                preimage.extend_from_slice(&input.source_output_index.to_le_bytes());

                // Script: only include sub_script for the input being signed
                if i == input_index {
                    write_varint_to_vec(&mut preimage, sub_script.len() as u64);
                    preimage.extend_from_slice(&sub_script);
                } else {
                    write_varint_to_vec(&mut preimage, empty_script.len() as u64);
                }

                // Sequence: for SINGLE and NONE, zero out other inputs' sequences
                if i == input_index || (!is_single && !is_none) {
                    preimage.extend_from_slice(&input.sequence.to_le_bytes());
                } else {
                    preimage.extend_from_slice(&0u32.to_le_bytes());
                }
            }
        }

        // Outputs
        if is_none {
            write_varint_to_vec(&mut preimage, 0);
        } else if is_single {
            write_varint_to_vec(&mut preimage, (input_index + 1) as u64);
            for i in 0..input_index {
                // Blank outputs before the matching one: satoshis = -1 (0xFFFFFFFFFFFFFFFF), empty script
                preimage.extend_from_slice(&u64::MAX.to_le_bytes());
                write_varint_to_vec(&mut preimage, 0);
                let _ = i;
            }
            // The output at input_index
            let output = &self.outputs[input_index];
            preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
            let script_bytes = output.locking_script.to_binary();
            write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
            preimage.extend_from_slice(&script_bytes);
        } else {
            // ALL: serialize all outputs
            write_varint_to_vec(&mut preimage, self.outputs.len() as u64);
            for output in &self.outputs {
                preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
                let script_bytes = output.locking_script.to_binary();
                write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
                preimage.extend_from_slice(&script_bytes);
            }
        }

        // Locktime
        preimage.extend_from_slice(&self.lock_time.to_le_bytes());

        // Sighash type (4 bytes LE)
        preimage.extend_from_slice(&scope.to_le_bytes());

        Ok(preimage)
    }

    // -- Transaction signing --------------------------------------------------

    /// Sign the input at `input_index` using a ScriptTemplateUnlock implementation.
    ///
    /// Computes the sighash preimage (BIP143/ForkID format) and passes it to the
    /// template's sign() method, then sets the resulting unlocking script on the input.
    pub fn sign(
        &mut self,
        input_index: usize,
        template: &dyn ScriptTemplateUnlock,
        scope: u32,
        source_satoshis: u64,
        source_locking_script: &LockingScript,
    ) -> Result<(), TransactionError> {
        let preimage =
            self.sighash_preimage(input_index, scope, source_satoshis, source_locking_script)?;
        let unlocking_script = template
            .sign(&preimage)
            .map_err(|e| TransactionError::SigningFailed(format!("{}", e)))?;
        self.inputs[input_index].unlocking_script = Some(unlocking_script);
        Ok(())
    }

    /// Sign all unsigned inputs using the same template.
    ///
    /// A convenience method that reduces the per-input signing loop. For each
    /// input that has no `unlocking_script` yet, this resolves `source_satoshis`
    /// and `source_locking_script` from the input's `source_transaction` and
    /// signs with the given template and sighash scope.
    ///
    /// Inputs that already have an unlocking script are skipped.
    ///
    /// Each input must have its `source_transaction` set so that the source
    /// output's satoshis and locking script can be resolved. If you need
    /// different templates or scopes per input, use the single-input `sign()`.
    pub fn sign_all_inputs(
        &mut self,
        template: &dyn ScriptTemplateUnlock,
        scope: u32,
    ) -> Result<(), TransactionError> {
        let num_inputs = self.inputs.len();

        for i in 0..num_inputs {
            // Skip inputs that already have an unlocking script
            if self.inputs[i].unlocking_script.is_some() {
                continue;
            }

            // Resolve source satoshis and locking script from source_transaction
            let (source_satoshis, source_locking_script) = {
                let source_tx = self.inputs[i].source_transaction.as_ref().ok_or_else(|| {
                    TransactionError::SigningFailed(format!(
                        "input {}: source_transaction required for sign_all_inputs()",
                        i
                    ))
                })?;
                let out_idx = self.inputs[i].source_output_index as usize;
                let output = source_tx.outputs.get(out_idx).ok_or_else(|| {
                    TransactionError::SigningFailed(format!(
                        "input {}: source transaction has no output at index {}",
                        i, out_idx
                    ))
                })?;
                let satoshis = output.satoshis.ok_or_else(|| {
                    TransactionError::SigningFailed(format!(
                        "input {}: source output {} has no satoshis",
                        i, out_idx
                    ))
                })?;
                (satoshis, output.locking_script.clone())
            };

            let preimage =
                self.sighash_preimage(i, scope, source_satoshis, &source_locking_script)?;
            let unlocking_script = template
                .sign(&preimage)
                .map_err(|e| TransactionError::SigningFailed(format!("input {}: {}", i, e)))?;
            self.inputs[i].unlocking_script = Some(unlocking_script);
        }

        Ok(())
    }
}

impl Default for Transaction {
    fn default() -> Self {
        Self::new()
    }
}

/// Convert a byte slice to a lowercase hex string.
fn bytes_to_hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        s.push_str(&format!("{:02x}", b));
    }
    s
}

/// Strip OP_CODESEPARATOR (0xab) opcodes from a raw script byte array.
///
/// Properly parses the script to avoid removing 0xab bytes that appear
/// as data within push operations.
fn strip_codeseparator(script: &[u8]) -> Vec<u8> {
    const OP_CODESEPARATOR: u8 = 0xab;

    let mut result = Vec::with_capacity(script.len());
    let mut i = 0;
    while i < script.len() {
        let opcode = script[i];
        if opcode == OP_CODESEPARATOR {
            // Skip this opcode
            i += 1;
            continue;
        }

        if opcode > 0 && opcode < 76 {
            // Direct push: opcode is the number of bytes to push
            let push_len = opcode as usize;
            let end = std::cmp::min(i + 1 + push_len, script.len());
            result.extend_from_slice(&script[i..end]);
            i = end;
        } else if opcode == 76 {
            // OP_PUSHDATA1: next byte is length
            if i + 1 < script.len() {
                let push_len = script[i + 1] as usize;
                let end = std::cmp::min(i + 2 + push_len, script.len());
                result.extend_from_slice(&script[i..end]);
                i = end;
            } else {
                result.push(opcode);
                i += 1;
            }
        } else if opcode == 77 {
            // OP_PUSHDATA2: next 2 bytes are length (LE)
            if i + 2 < script.len() {
                let push_len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
                let end = std::cmp::min(i + 3 + push_len, script.len());
                result.extend_from_slice(&script[i..end]);
                i = end;
            } else {
                result.extend_from_slice(&script[i..]);
                break;
            }
        } else if opcode == 78 {
            // OP_PUSHDATA4: next 4 bytes are length (LE)
            if i + 4 < script.len() {
                let push_len = u32::from_le_bytes([
                    script[i + 1],
                    script[i + 2],
                    script[i + 3],
                    script[i + 4],
                ]) as usize;
                let end = std::cmp::min(i + 5 + push_len, script.len());
                result.extend_from_slice(&script[i..end]);
                i = end;
            } else {
                result.extend_from_slice(&script[i..]);
                break;
            }
        } else {
            // Regular opcode (0x00, or 0x4f..0xff except 0xab)
            result.push(opcode);
            i += 1;
        }
    }
    result
}

/// Write a Bitcoin-style varint directly to a Vec<u8> (no io::Write needed).
fn write_varint_to_vec(buf: &mut Vec<u8>, val: u64) {
    if val < 0xfd {
        buf.push(val as u8);
    } else if val <= 0xffff {
        buf.push(0xfd);
        buf.extend_from_slice(&(val as u16).to_le_bytes());
    } else if val <= 0xffff_ffff {
        buf.push(0xfe);
        buf.extend_from_slice(&(val as u32).to_le_bytes());
    } else {
        buf.push(0xff);
        buf.extend_from_slice(&val.to_le_bytes());
    }
}

/// Convert a hex string to bytes.
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
    if !hex.len().is_multiple_of(2) {
        return Err("odd length hex string".to_string());
    }
    let mut bytes = Vec::with_capacity(hex.len() / 2);
    for i in (0..hex.len()).step_by(2) {
        let byte = u8::from_str_radix(&hex[i..i + 2], 16)
            .map_err(|e| format!("invalid hex at position {}: {}", i, e))?;
        bytes.push(byte);
    }
    Ok(bytes)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::private_key::PrivateKey;
    use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
    use crate::script::templates::p2pkh::P2PKH;
    use crate::script::templates::ScriptTemplateLock;
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct TestVector {
        description: String,
        hex: String,
        txid: String,
        version: u32,
        inputs: usize,
        outputs: usize,
        locktime: u32,
    }

    fn load_test_vectors() -> Vec<TestVector> {
        let json = include_str!("../../test-vectors/transaction_valid.json");
        serde_json::from_str(json).expect("failed to parse transaction_valid.json")
    }

    #[test]
    fn test_from_binary_round_trip() {
        let vectors = load_test_vectors();
        for v in &vectors {
            let tx = Transaction::from_hex(&v.hex)
                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
            let result_hex = tx
                .to_hex()
                .unwrap_or_else(|e| panic!("failed to serialize '{}': {}", v.description, e));
            assert_eq!(
                result_hex, v.hex,
                "round-trip failed for '{}'",
                v.description
            );
        }
    }

    #[test]
    fn test_txid() {
        let vectors = load_test_vectors();
        for v in &vectors {
            let tx = Transaction::from_hex(&v.hex)
                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
            let txid = tx
                .id()
                .unwrap_or_else(|e| panic!("failed to compute id for '{}': {}", v.description, e));
            assert_eq!(txid, v.txid, "txid mismatch for '{}'", v.description);
        }
    }

    #[test]
    fn test_input_output_counts() {
        let vectors = load_test_vectors();
        for v in &vectors {
            let tx = Transaction::from_hex(&v.hex)
                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
            assert_eq!(
                tx.inputs.len(),
                v.inputs,
                "input count mismatch for '{}'",
                v.description
            );
            assert_eq!(
                tx.outputs.len(),
                v.outputs,
                "output count mismatch for '{}'",
                v.description
            );
            assert_eq!(
                tx.version, v.version,
                "version mismatch for '{}'",
                v.description
            );
            assert_eq!(
                tx.lock_time, v.locktime,
                "locktime mismatch for '{}'",
                v.description
            );
        }
    }

    #[test]
    fn test_empty_transaction() {
        let tx = Transaction::new();
        assert_eq!(tx.version, 1);
        assert!(tx.inputs.is_empty());
        assert!(tx.outputs.is_empty());
        assert_eq!(tx.lock_time, 0);
        assert!(tx.merkle_path.is_none());
    }

    #[test]
    fn test_add_input_output() {
        let mut tx = Transaction::new();
        assert_eq!(tx.inputs.len(), 0);
        assert_eq!(tx.outputs.len(), 0);

        tx.add_input(TransactionInput::default());
        assert_eq!(tx.inputs.len(), 1);

        tx.add_output(TransactionOutput::default());
        assert_eq!(tx.outputs.len(), 1);
    }

    #[test]
    fn test_ef_round_trip() {
        // EF format vector from TS SDK test
        let ef_hex = "010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000";

        let tx = Transaction::from_hex_ef(ef_hex).expect("failed to parse EF hex");
        assert_eq!(tx.inputs.len(), 1);
        assert_eq!(tx.outputs.len(), 1);

        // Verify source transaction info was captured
        let input = &tx.inputs[0];
        assert!(input.source_transaction.is_some());
        let source_tx = input.source_transaction.as_ref().unwrap();
        let source_output = &source_tx.outputs[input.source_output_index as usize];
        assert_eq!(source_output.satoshis, Some(0x663e)); // 26174 satoshis

        // Round-trip: serialize back to EF hex
        let result_hex = tx.to_hex_ef().expect("failed to serialize to EF");
        assert_eq!(result_hex, ef_hex);
    }

    #[test]
    fn test_hash_and_id_consistency() {
        // tx2 from the TS SDK test
        let tx2hex = "01000000029e8d016a7b0dc49a325922d05da1f916d1e4d4f0cb840c9727f3d22ce8d1363f000000008c493046022100e9318720bee5425378b4763b0427158b1051eec8b08442ce3fbfbf7b30202a44022100d4172239ebd701dae2fbaaccd9f038e7ca166707333427e3fb2a2865b19a7f27014104510c67f46d2cbb29476d1f0b794be4cb549ea59ab9cc1e731969a7bf5be95f7ad5e7f904e5ccf50a9dc1714df00fbeb794aa27aaff33260c1032d931a75c56f2ffffffffa3195e7a1ab665473ff717814f6881485dc8759bebe97e31c301ffe7933a656f020000008b48304502201c282f35f3e02a1f32d2089265ad4b561f07ea3c288169dedcf2f785e6065efa022100e8db18aadacb382eed13ee04708f00ba0a9c40e3b21cf91da8859d0f7d99e0c50141042b409e1ebbb43875be5edde9c452c82c01e3903d38fa4fd89f3887a52cb8aea9dc8aec7e2c9d5b3609c03eb16259a2537135a1bf0f9c5fbbcbdbaf83ba402442ffffffff02206b1000000000001976a91420bb5c3bfaef0231dc05190e7f1c8e22e098991e88acf0ca0100000000001976a9149e3e2d23973a04ec1b02be97c30ab9f2f27c3b2c88ac00000000";
        let tx2idhex = "8c9aa966d35bfeaf031409e0001b90ccdafd8d859799eb945a3c515b8260bcf2";

        let tx = Transaction::from_hex(tx2hex).unwrap();
        let id = tx.id().unwrap();
        assert_eq!(id, tx2idhex);

        // Verify hash is the reverse of id
        let hash = tx.hash().unwrap();
        let mut reversed_hash = hash;
        reversed_hash.reverse();
        let reversed_hex = bytes_to_hex(&reversed_hash);
        assert_eq!(reversed_hex, tx2idhex);
    }

    // -- Sighash preimage tests -----------------------------------------------

    /// OTDA sighash test vectors from TS SDK sighashTestData.ts
    /// Format: (raw_tx_hex, script_hex, input_index, hash_type_unsigned, expected_otda_hash_display)
    fn otda_test_vectors() -> Vec<(&'static str, &'static str, usize, u32, &'static str)> {
        vec![
            ("0122769903cfc6fedb9c63fe76930fed0c87b44be46f5032a534fa05861548616a5b99034701000000040063656ac864c70228b3f6ebaf97be065b2180ece52fae4f9039c7bf932e567625d7d89582abe1340100000008ac6363650063ab65ffffffff587125b913706705dc799454ab0343ee8aa59a2f2d538f75e12c0deeb40aa741030000000027b3a02003d21f34040000000000fa7f2c0300000000016387213b0300000000056aabacacab00000000", "", 1, 902085315, "49fdc84c5f88a590c5c65e17de58da7d1028133b74ad2d56a8b15ba317ac98f6"),
            ("7e8c3f7902634018b6e1db2ca591816dff64a6cff74643de7455323ebfc560500aad9eee8c0100000001519c2d146d06fdca2c2e5fa3a2559812df66b6b5e40a3c57b2f7071ae6fe3863c74ab0952d0100000001000bf6638c013c5f6503000000000351ac63cbbf66be", "acab", 0, 2433331782, "f6261bacaed3a70d504cd70d3c0623e3593f6d197cd47316e56cea79ceabe095"),
            ("459499bb032fdcc39d3c6cf819dcaa0a0165d97578446aa87ab745fb9fdcd3e6177b4cba3d0000000005006a6a5265ffffffff10e5929ebe065273c112cab15f6a1f6d9a8a517c288311b048b16663b3d406dc030000000700535263655151ffffffff981d73a7f3d477ab055398bcf9a7d349db1a8e6362055e20f4207ad1b775bac301000000066a6363ac6552ffffffff0403342603000000000165c4390004000000000965ac52006565006365373ce8010000000005520000516aba5a9404000000000351655300000000", "6a5352", 0, 3544391288, "738b7dcb86260e6fe3fad331ff342429c157730bbcb90c205b9e08568557cd94"),
            ("cb3b8d30043ccd81c3bda7f594cca60e2eef170c67ffe8a1eb1f1a994dc40a0a5cf89fa9690100000009ab536a52acab5300653bea9324983da711ccb6eaff060930e6f55cf6df75e5abdda91a8d5fc25c3b9b28d0e7370200000003ab51522f86cdbd8aa19b6b8536efb6ca8cc23ebccef585ad00a78b5956d803908482bb44b25c550000000007abab65530051ac8177a2acebc517db1d5b5be14f91ab40e811ec0316cf029ce657a4b06f04f30698f0a0e50000000007516aac636a6351ffffffff02ccbefa02000000000252ab3e297f0100000000060052525163521acc3e2b", "6aac636a63535153", 0, 3406487088, "3568dfad7e968afd3492bd146c8b0e3255f90e5b642a4ec10105693e8b029132"),
        ]
    }

    #[test]
    fn test_sighash_preimage_legacy_vectors() {
        let vectors = otda_test_vectors();
        let mut passed = 0;

        for (i, (raw_tx_hex, script_hex, input_index, hash_type, expected_hash)) in
            vectors.iter().enumerate()
        {
            let tx = Transaction::from_hex(raw_tx_hex)
                .unwrap_or_else(|e| panic!("vector {}: failed to parse tx: {}", i, e));

            let sub_script = if script_hex.is_empty() {
                vec![]
            } else {
                hex_to_bytes(script_hex).unwrap()
            };

            let preimage = tx
                .sighash_preimage_legacy(*input_index, *hash_type, &sub_script)
                .unwrap_or_else(|e| panic!("vector {}: sighash error: {}", i, e));

            // The expected hash is in display (BE/reversed) format
            let mut hash_bytes = hash256(&preimage);
            hash_bytes.reverse();
            let computed_hash = bytes_to_hex(&hash_bytes);

            if computed_hash == *expected_hash {
                passed += 1;
            } else {
                println!(
                    "MISMATCH vector {}: expected={}, got={}",
                    i, expected_hash, computed_hash
                );
            }
        }

        println!(
            "sighash legacy OTDA vectors: {}/{} passed",
            passed,
            vectors.len()
        );
        assert_eq!(
            passed,
            vectors.len(),
            "all sighash OTDA vectors should pass"
        );
    }

    #[test]
    fn test_sighash_preimage_bip143() {
        // Test vector from Go SDK: "1 Input 2 Outputs - SIGHASH_ALL (FORKID)"
        let unsigned_tx_hex = "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d25072326510000000000ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000";
        let source_script_hex = "76a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac";
        let source_satoshis: u64 = 100_000_000;
        let expected_preimage_hex = "010000007ced5b2e5cf3ea407b005d8b18c393b6256ea2429b6ff409983e10adc61d0ae83bb13029ce7b1f559ef5e747fcac439f1455a2ec7c5f09b72290795e7066504493a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d2507232651000000001976a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac00e1f50500000000ffffffff87841ab2b7a4133af2c58256edb7c3c9edca765a852ebe2d0dc962604a30f1030000000041000000";

        let tx = Transaction::from_hex(unsigned_tx_hex).unwrap();
        let source_script_bytes = hex_to_bytes(source_script_hex).unwrap();
        let source_locking_script = LockingScript::from_binary(&source_script_bytes);

        let scope = SIGHASH_ALL | SIGHASH_FORKID;
        let preimage = tx
            .sighash_preimage(0, scope, source_satoshis, &source_locking_script)
            .unwrap();
        let preimage_hex = bytes_to_hex(&preimage);

        assert_eq!(
            preimage_hex, expected_preimage_hex,
            "BIP143 preimage should match Go SDK test vector"
        );
    }

    // -- Transaction signing tests --------------------------------------------

    #[test]
    fn test_sign_p2pkh() {
        let key = PrivateKey::from_hex("1").unwrap();
        let p2pkh_lock = P2PKH::from_private_key(key.clone());
        let p2pkh_unlock = P2PKH::from_private_key(key.clone());

        let lock_script = p2pkh_lock.lock().unwrap();

        // Build a transaction with one input and one output
        let mut tx = Transaction::new();
        tx.add_input(TransactionInput {
            source_transaction: None,
            source_txid: Some("00".repeat(32)),
            source_output_index: 0,
            unlocking_script: None,
            sequence: 0xffffffff,
        });
        tx.add_output(TransactionOutput {
            satoshis: Some(50000),
            locking_script: lock_script.clone(),
            change: false,
        });

        // Sign the input
        let scope = SIGHASH_ALL | SIGHASH_FORKID;
        tx.sign(0, &p2pkh_unlock, scope, 100000, &lock_script)
            .expect("signing should succeed");

        // Verify unlocking script is set
        let unlock = tx.inputs[0].unlocking_script.as_ref().unwrap();
        let chunks = unlock.chunks();
        assert_eq!(
            chunks.len(),
            2,
            "P2PKH unlock should have 2 chunks (sig + pubkey)"
        );

        // First chunk: signature (DER + sighash byte)
        let sig_data = chunks[0].data.as_ref().unwrap();
        assert!(
            sig_data.len() >= 70 && sig_data.len() <= 74,
            "signature length {} should be 70-74",
            sig_data.len()
        );
        assert_eq!(
            *sig_data.last().unwrap(),
            (SIGHASH_ALL | SIGHASH_FORKID) as u8,
            "last byte should be sighash type"
        );

        // Second chunk: compressed public key (33 bytes)
        let pubkey_data = chunks[1].data.as_ref().unwrap();
        assert_eq!(pubkey_data.len(), 33);
    }

    #[test]
    fn test_sign_and_verify_round_trip() {
        use crate::script::spend::{Spend, SpendParams};

        let key = PrivateKey::from_hex("abcdef01").unwrap();
        let p2pkh = P2PKH::from_private_key(key.clone());
        let lock_script = p2pkh.lock().unwrap();

        // Build and sign a transaction
        let mut tx = Transaction::new();
        let source_satoshis = 100_000u64;

        tx.add_input(TransactionInput {
            source_transaction: None,
            source_txid: Some("aa".repeat(32)),
            source_output_index: 0,
            unlocking_script: None,
            sequence: 0xffffffff,
        });
        tx.add_output(TransactionOutput {
            satoshis: Some(90_000),
            locking_script: lock_script.clone(),
            change: false,
        });

        let scope = SIGHASH_ALL | SIGHASH_FORKID;
        tx.sign(0, &p2pkh, scope, source_satoshis, &lock_script)
            .expect("signing should succeed");

        // Now verify with Spend
        let unlock_script = tx.inputs[0].unlocking_script.clone().unwrap();

        let mut spend = Spend::new(SpendParams {
            locking_script: lock_script.clone(),
            unlocking_script: unlock_script,
            source_txid: "aa".repeat(32),
            source_output_index: 0,
            source_satoshis,
            transaction_version: tx.version,
            transaction_lock_time: tx.lock_time,
            transaction_sequence: tx.inputs[0].sequence,
            other_inputs: vec![],
            other_outputs: tx.outputs.clone(),
            input_index: 0,
        });

        let valid = spend.validate().expect("spend validation should not error");
        assert!(valid, "signed transaction should verify successfully");
    }

    #[test]
    fn test_to_beef_round_trip_all_vectors() {
        // Parse known valid BEEFs via from_beef, re-serialize with to_beef(),
        // and verify the round-trip produces valid BEEF with the same subject tx.
        let vectors_json = std::fs::read_to_string("test-vectors/beef_valid.json").unwrap();
        let vectors: Vec<serde_json::Value> = serde_json::from_str(&vectors_json).unwrap();

        for (i, v) in vectors.iter().enumerate() {
            let hex = v["hex"].as_str().unwrap();
            let version = v["version"].as_u64().unwrap();
            if version != 1 {
                continue; // to_beef() produces V1 only
            }

            // Parse BEEF at the Beef level to get subject tx with full source chain
            let beef = crate::transaction::beef::Beef::from_hex(hex)
                .unwrap_or_else(|e| panic!("vector {}: from_hex failed: {}", i, e));
            let tx = beef
                .into_transaction()
                .unwrap_or_else(|e| panic!("vector {}: into_transaction failed: {}", i, e));

            // Re-serialize to BEEF
            let beef_bytes = tx
                .to_beef()
                .unwrap_or_else(|e| panic!("vector {}: to_beef failed: {}", i, e));

            // Verify BEEF V1 header
            assert_eq!(
                &beef_bytes[0..4],
                &[0x01, 0x00, 0xBE, 0xEF],
                "vector {}: wrong BEEF header",
                i
            );

            // Parse back and verify same subject txid
            let re_hex: String = beef_bytes.iter().map(|b| format!("{:02x}", b)).collect();
            let re_parsed = Transaction::from_beef(&re_hex)
                .unwrap_or_else(|e| panic!("vector {}: round-trip from_beef failed: {}", i, e));
            assert_eq!(
                re_parsed.id().unwrap(),
                tx.id().unwrap(),
                "vector {}: txid mismatch after round-trip",
                i
            );
            assert_eq!(
                re_parsed.outputs.len(),
                tx.outputs.len(),
                "vector {}: output count mismatch",
                i
            );
        }
    }

    #[test]
    fn test_to_beef_byte_equality_simple() {
        // For the simple single-tx vector, check that to_beef produces
        // byte-identical output to the original.
        let vectors_json = std::fs::read_to_string("test-vectors/beef_valid.json").unwrap();
        let vectors: Vec<serde_json::Value> = serde_json::from_str(&vectors_json).unwrap();
        let original_hex = vectors[0]["hex"].as_str().unwrap();

        let tx = Transaction::from_beef(original_hex).expect("from_beef");
        let beef_bytes = tx.to_beef().expect("to_beef");
        let result_hex: String = beef_bytes.iter().map(|b| format!("{:02x}", b)).collect();

        assert_eq!(
            result_hex, original_hex,
            "round-trip should produce identical bytes"
        );
    }

    #[test]
    fn test_to_beef_multi_tx_with_source_chain() {
        // Build a transaction with a source transaction that has a merkle path.
        // This exercises the recursive collect into Beef.
        use crate::script::locking_script::LockingScript;
        use crate::transaction::merkle_path::{MerklePath, MerklePathLeaf};
        use crate::transaction::transaction_input::TransactionInput;
        use crate::transaction::transaction_output::TransactionOutput;

        // Create a proven parent transaction.
        let mut parent = Transaction::new();
        parent.add_output(TransactionOutput {
            satoshis: Some(50_000),
            locking_script: LockingScript::from_binary(&vec![
                0x76, 0xa9, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
                0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x88, 0xac,
            ]),
            change: false,
        });
        let parent_txid = parent.id().unwrap();

        // Give parent a merkle path (proven in block).
        parent.merkle_path = Some(MerklePath {
            block_height: 100,
            path: vec![vec![
                MerklePathLeaf {
                    offset: 0,
                    hash: Some(parent_txid.clone()),
                    txid: true,
                    duplicate: false,
                },
                MerklePathLeaf {
                    offset: 1,
                    hash: Some("bb".repeat(32)),
                    txid: false,
                    duplicate: false,
                },
            ]],
        });

        // Create child transaction spending parent.
        let mut child = Transaction::new();
        child.add_input(TransactionInput {
            source_transaction: Some(Box::new(parent.clone())),
            source_txid: Some(parent_txid.clone()),
            source_output_index: 0,
            unlocking_script: None,
            sequence: 0xffffffff,
        });
        child.add_output(TransactionOutput {
            satoshis: Some(40_000),
            locking_script: LockingScript::from_binary(&vec![0x6a, 0x04, 0xde, 0xad]),
            change: false,
        });

        // Serialize to BEEF.
        let beef_bytes = child.to_beef().expect("to_beef should succeed");

        // Verify header.
        assert_eq!(&beef_bytes[0..4], &[0x01, 0x00, 0xBE, 0xEF]);

        // Parse back via Beef and verify structure.
        let re_beef = crate::transaction::beef::Beef::from_hex(
            &beef_bytes
                .iter()
                .map(|b| format!("{:02x}", b))
                .collect::<String>(),
        )
        .expect("re-parse BEEF");

        assert_eq!(re_beef.bumps.len(), 1, "should have 1 bump");
        assert_eq!(re_beef.txs.len(), 2, "should have 2 txs (parent + child)");

        // Proven parent should come before unproven child (topological order).
        assert!(
            re_beef.txs[0].bump_index.is_some(),
            "first tx should be proven parent"
        );
        assert!(
            re_beef.txs[1].bump_index.is_none(),
            "second tx should be unproven child"
        );
    }

    #[test]
    fn test_to_beef_errors_no_merkle_proofs() {
        // A bare transaction with no source chain and no merkle path should error.
        let mut tx = Transaction::new();
        tx.add_output(TransactionOutput {
            satoshis: Some(1000),
            locking_script: crate::script::locking_script::LockingScript::from_binary(&vec![
                0x6a, 0x02, 0xab, 0xcd,
            ]),
            change: false,
        });

        let result = tx.to_beef();
        assert!(result.is_err(), "to_beef should fail with no proofs");
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("merkle proof") || err.contains("source transaction"),
            "error should mention missing proofs, got: {}",
            err
        );
    }

    #[test]
    fn test_to_beef_errors_missing_source_transaction() {
        // A transaction with an input that has no source_transaction and no
        // merkle_path should produce a clear error.
        use crate::transaction::transaction_input::TransactionInput;
        use crate::transaction::transaction_output::TransactionOutput;

        let mut tx = Transaction::new();
        tx.add_input(TransactionInput {
            source_transaction: None,
            source_txid: Some("aa".repeat(32)),
            source_output_index: 0,
            unlocking_script: None,
            sequence: 0xffffffff,
        });
        tx.add_output(TransactionOutput {
            satoshis: Some(1000),
            locking_script: crate::script::locking_script::LockingScript::from_binary(&vec![
                0x6a, 0x02, 0xab, 0xcd,
            ]),
            change: false,
        });

        let result = tx.to_beef();
        assert!(
            result.is_err(),
            "to_beef should fail with missing source tx"
        );
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("no source transaction"),
            "error should mention missing source transaction, got: {}",
            err
        );
    }
}
