use fiscal_core::constants::{NFE_NAMESPACE, NFE_VERSION};
use fiscal_core::state_codes::get_state_code;
use fiscal_core::types::SefazEnvironment;
use fiscal_core::xml_utils::extract_xml_tag_value;

/// Event type constants (`tpEvento`) matching the SEFAZ specification.
pub mod event_types {
    /// Carta de Correção Eletrônica (CC-e), code `110110`.
    pub const CCE: u32 = 110110;
    /// Cancellation event, code `110111`.
    pub const CANCELLATION: u32 = 110111;
    /// Cancelamento por substituição (NFC-e only), code `110112`.
    pub const CANCEL_SUBSTITUICAO: u32 = 110112;
    /// Comprovante de entrega da NF-e, code `110130`.
    pub const COMPROVANTE_ENTREGA: u32 = 110130;
    /// Cancelamento do comprovante de entrega da NF-e, code `110131`.
    pub const CANCEL_COMPROVANTE_ENTREGA: u32 = 110131;
    /// Ator interessado na NF-e, code `110150`.
    pub const ATOR_INTERESSADO: u32 = 110150;
    /// Insucesso na entrega da NF-e, code `110192`.
    pub const INSUCESSO_ENTREGA: u32 = 110192;
    /// Cancelamento do insucesso na entrega da NF-e, code `110193`.
    pub const CANCEL_INSUCESSO_ENTREGA: u32 = 110193;
    /// Recipient confirms operation, code `210200`.
    pub const CONFIRMATION: u32 = 210200;
    /// Recipient is aware of operation, code `210210`.
    pub const AWARENESS: u32 = 210210;
    /// Recipient does not recognize the operation, code `210220`.
    pub const UNKNOWN_OPERATION: u32 = 210220;
    /// Recipient confirms operation was not performed, code `210240`.
    pub const OPERATION_NOT_PERFORMED: u32 = 210240;
    /// EPEC — Evento Prévio de Emissão em Contingência, code `110140`.
    pub const EPEC: u32 = 110140;
    /// Pedido de prorrogação ICMS — 1.o prazo, code `111500`.
    pub const PRORROGACAO_1: u32 = 111500;
    /// Pedido de prorrogação ICMS — 2.o prazo, code `111501`.
    pub const PRORROGACAO_2: u32 = 111501;
    /// Cancelamento de pedido de prorrogação ICMS — 1.o prazo, code `111502`.
    pub const CANCEL_PRORROGACAO_1: u32 = 111502;
    /// Cancelamento de pedido de prorrogação ICMS — 2.o prazo, code `111503`.
    pub const CANCEL_PRORROGACAO_2: u32 = 111503;
    /// Conciliação financeira, code `110750`.
    pub const CONCILIACAO: u32 = 110750;
    /// Cancelamento de conciliação financeira, code `110751`.
    pub const CANCEL_CONCILIACAO: u32 = 110751;

    // ── RTC (Reforma Tributária Complementar) events ────────────────────

    /// Cancelamento de evento RTC, code `110001`.
    pub const RTC_CANCELA_EVENTO: u32 = 110001;
    /// Info pagamento integral (crédito presumido), code `112110`.
    pub const RTC_INFO_PAGTO_INTEGRAL: u32 = 112110;
    /// Importação ZFM não convertida em isenção, code `112120`.
    pub const RTC_IMPORTACAO_ZFM: u32 = 112120;
    /// Perecimento/roubo transporte fornecedor, code `112130`.
    pub const RTC_ROUBO_PERDA_FORNECEDOR: u32 = 112130;
    /// Fornecimento não realizado (pagamento antecipado), code `112140`.
    pub const RTC_FORNECIMENTO_NAO_REALIZADO: u32 = 112140;
    /// Atualização data previsão entrega, code `112150`.
    pub const RTC_ATUALIZACAO_DATA_ENTREGA: u32 = 112150;
    /// Solicitação apropriação crédito presumido, code `211110`.
    pub const RTC_SOL_APROP_CRED_PRESUMIDO: u32 = 211110;
    /// Destinação consumo pessoal, code `211120`.
    pub const RTC_DESTINO_CONSUMO_PESSOAL: u32 = 211120;
    /// Perecimento/roubo transporte adquirente, code `211124`.
    pub const RTC_ROUBO_PERDA_ADQUIRENTE: u32 = 211124;
    /// Aceite débito apuração, code `211128`.
    pub const RTC_ACEITE_DEBITO: u32 = 211128;
    /// Imobilização de item, code `211130`.
    pub const RTC_IMOBILIZACAO_ITEM: u32 = 211130;
    /// Apropriação crédito combustível, code `211140`.
    pub const RTC_APROPRIACAO_CREDITO_COMB: u32 = 211140;
    /// Apropriação crédito bens/serviços, code `211150`.
    pub const RTC_APROPRIACAO_CREDITO_BENS: u32 = 211150;
    /// Manifestação transferência crédito IBS, code `212110`.
    pub const RTC_MANIF_TRANSF_CRED_IBS: u32 = 212110;
    /// Manifestação transferência crédito CBS, code `212120`.
    pub const RTC_MANIF_TRANSF_CRED_CBS: u32 = 212120;
}

/// Event descriptions matching the SEFAZ specification.
fn event_description(event_type: u32) -> &'static str {
    match event_type {
        110110 => "Carta de Correcao",
        110111 => "Cancelamento",
        110112 => "Cancelamento por substituicao",
        110140 => "EPEC",
        110150 => "Ator interessado na NF-e",
        110130 => "Comprovante de Entrega da NF-e",
        110131 => "Cancelamento Comprovante de Entrega da NF-e",
        111500 | 111501 => "Pedido de Prorrogacao",
        111502 | 111503 => "Cancelamento de Pedido de Prorrogacao",
        210200 => "Confirmacao da Operacao",
        210210 => "Ciencia da Operacao",
        210220 => "Desconhecimento da Operacao",
        210240 => "Operacao nao Realizada",
        110192 => "Insucesso na Entrega da NF-e",
        110193 => "Cancelamento Insucesso na Entrega da NF-e",
        110750 => "ECONF",
        110751 => "Cancelamento Conciliação Financeira",
        // RTC events — descriptions must match PHP sped-nfe exactly
        110001 => "Cancelamento de Evento",
        112110 => {
            "Informação de efetivo pagamento integral para liberar crédito presumido do adquirente"
        }
        112120 => "Importação em ALC/ZFM não convertida em isenção",
        112130 => {
            "Perecimento, perda, roubo ou furto durante o transporte contratado pelo fornecedor"
        }
        112140 => "Fornecimento não realizado com pagamento antecipado",
        112150 => "Atualização da Data de Previsão de Entrega",
        211110 => "Solicitação de Apropriação de crédito presumido",
        211120 => "Destinação de item para consumo pessoal",
        211124 => {
            "Perecimento, perda, roubo ou furto durante o transporte contratado pelo adquirente"
        }
        211128 => "Aceite de débito na apuração por emissão de nota de crédito",
        211130 => "Imobilização de Item",
        211140 => "Solicitação de Apropriação de Crédito de Combustível",
        211150 => {
            "Solicitação de Apropriação de Crédito para bens e serviços que dependem de atividade do adquirente"
        }
        212110 => {
            "Manifestação sobre Pedido de Transferência de Crédito de IBS em Operação de Sucessão"
        }
        212120 => {
            "Manifestação sobre Pedido de Transferência de Crédito de CBS em Operação de Sucessão"
        }
        _ => "",
    }
}

/// Build the event ID string: `ID{tpEvento}{chNFe}{nSeqEvento:02}`.
fn build_event_id(event_type: u32, access_key: &str, seq: u32) -> String {
    format!("ID{event_type}{access_key}{seq:02}")
}

/// Generate a CNPJ XML tag from a tax ID string.
///
/// If the tax ID is 11 digits it is treated as CPF; otherwise CNPJ.
fn tax_id_xml_tag(tax_id: &str) -> String {
    let digits: String = tax_id.chars().filter(|c| c.is_ascii_digit()).collect();
    if digits.len() == 11 {
        format!("<CPF>{digits}</CPF>")
    } else {
        format!("<CNPJ>{digits}</CNPJ>")
    }
}

/// Build a SEFAZ authorization request XML (`<enviNFe>`).
///
/// Wraps one or more signed NF-e XML documents in an `<enviNFe>` envelope
/// for submission to the SEFAZ authorization web service.
///
/// # Arguments
///
/// * `xml` - The signed NF-e XML (XML declaration is stripped automatically).
/// * `lot_id` - Lot identifier for the submission batch.
/// * `sync` - Whether to use synchronous processing (`indSinc=1`).
/// * `compressed` - Whether the XML content is gzip-compressed (flag only,
///   actual compression is handled at the transport layer).
///
/// # Panics
///
/// Panics if `xml` is empty.
///
/// # Errors
///
/// This function does not return `Result` errors but panics on invalid input.
pub fn build_autorizacao_request(xml: &str, lot_id: &str, sync: bool, _compressed: bool) -> String {
    assert!(
        !xml.is_empty(),
        "XML content is required for authorization request"
    );

    // Strip XML declaration if present
    let content = xml.trim().trim_start_matches(|c: char| c != '<');
    let stripped = strip_xml_declaration(content);

    let ind_sinc = if sync { "1" } else { "0" };

    format!(
        "<enviNFe xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><idLote>{lot_id}</idLote><indSinc>{ind_sinc}</indSinc>{stripped}</enviNFe>"
    )
}

/// Build a SEFAZ batch authorization request XML (`<enviNFe>`) for multiple NF-e documents.
///
/// Wraps one or more signed NF-e XML documents in a single `<enviNFe>` envelope
/// for batch submission to the SEFAZ authorization web service.
///
/// When `ind_sinc` is `0` (asynchronous), SEFAZ returns a receipt number (`nRec`)
/// that must be polled via [`build_consulta_recibo_request`] to obtain the result.
/// When `ind_sinc` is `1` (synchronous), only a single NF-e is allowed per the
/// MOC specification.
///
/// This matches the PHP `sefazEnviaLote()` method from `Tools.php`.
///
/// # Arguments
///
/// * `xmls` - Slice of signed NF-e XML strings. Each one is stripped of its
///   XML declaration automatically.
/// * `id_lote` - Lot identifier for the submission batch.
/// * `ind_sinc` - Synchronous indicator: `0` for asynchronous (batch),
///   `1` for synchronous (single document).
///
/// # Errors
///
/// Returns [`FiscalError::InvalidTaxData`] if:
/// - `xmls` is empty
/// - `xmls` has more than 50 documents
/// - `ind_sinc` is `1` but `xmls` has more than 1 document
pub fn build_autorizacao_batch_request(
    xmls: &[&str],
    id_lote: &str,
    ind_sinc: u8,
) -> Result<String, fiscal_core::FiscalError> {
    if xmls.is_empty() {
        return Err(fiscal_core::FiscalError::InvalidTaxData(
            "At least one NF-e XML is required for batch authorization".into(),
        ));
    }
    if xmls.len() > 50 {
        return Err(fiscal_core::FiscalError::InvalidTaxData(format!(
            "Batch authorization accepts at most 50 NF-e documents, got {}",
            xmls.len()
        )));
    }
    if ind_sinc == 1 && xmls.len() > 1 {
        return Err(fiscal_core::FiscalError::InvalidTaxData(format!(
            "Synchronous mode (indSinc=1) accepts only 1 NF-e, got {}",
            xmls.len()
        )));
    }

    let mut nfe_concat = String::new();
    for xml in xmls {
        let content = xml.trim().trim_start_matches(|c: char| c != '<');
        let stripped = strip_xml_declaration(content);
        nfe_concat.push_str(stripped);
    }

    Ok(format!(
        "<enviNFe xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><idLote>{id_lote}</idLote><indSinc>{ind_sinc}</indSinc>{nfe_concat}</enviNFe>"
    ))
}

/// Build a SEFAZ service status request XML (`<consStatServ>`).
///
/// Queries the operational status of a SEFAZ web service for the given state.
///
/// # Panics
///
/// Panics if `uf` is not a valid Brazilian state code.
///
/// # Errors
///
/// This function panics on invalid state codes rather than returning `Result`.
pub fn build_status_request(uf: &str, environment: SefazEnvironment) -> String {
    let cuf = get_state_code(uf).expect("Invalid state code");
    let tp_amb = environment.as_str();

    format!(
        "<consStatServ xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><tpAmb>{tp_amb}</tpAmb><cUF>{cuf}</cUF><xServ>STATUS</xServ></consStatServ>"
    )
}

/// Build a SEFAZ consultation request XML (`<consSitNFe>`) for an access key.
///
/// Queries the current status of an NF-e by its 44-digit access key.
///
/// # Panics
///
/// Panics if `access_key` is empty, not exactly 44 characters, or non-numeric.
///
/// # Errors
///
/// This function panics on invalid input rather than returning `Result`.
pub fn build_consulta_request(access_key: &str, environment: SefazEnvironment) -> String {
    validate_access_key(access_key);
    let tp_amb = environment.as_str();

    format!(
        "<consSitNFe xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><tpAmb>{tp_amb}</tpAmb><xServ>CONSULTAR</xServ><chNFe>{access_key}</chNFe></consSitNFe>"
    )
}

/// Build a SEFAZ receipt consultation request XML (`<consReciNFe>`).
///
/// Queries the processing result of a previously submitted batch by receipt number.
///
/// # Panics
///
/// Panics if `receipt` is empty.
///
/// # Errors
///
/// This function panics on invalid input rather than returning `Result`.
pub fn build_consulta_recibo_request(receipt: &str, environment: SefazEnvironment) -> String {
    assert!(!receipt.is_empty(), "Receipt number (recibo) is required");
    let tp_amb = environment.as_str();

    format!(
        "<consReciNFe xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><tpAmb>{tp_amb}</tpAmb><nRec>{receipt}</nRec></consReciNFe>"
    )
}

/// Build a SEFAZ number voiding request XML (`<inutNFe>`).
///
/// Requests voiding (inutilizacao) of a range of NF-e/NFC-e numbers that
/// were skipped and will not be used.
///
/// # Arguments
///
/// * `year` - Two-digit year (e.g. `22` for 2022).
/// * `tax_id` - CNPJ of the issuer (14 digits, no formatting).
/// * `model` - Invoice model (`"55"` for NF-e, `"65"` for NFC-e).
/// * `series` - Series number.
/// * `start_number` - First invoice number in the range.
/// * `end_number` - Last invoice number in the range.
/// * `justification` - Justification text for the voiding.
/// * `environment` - SEFAZ environment (production or homologation).
/// * `uf` - State abbreviation (e.g. `"SP"`).
///
/// # Panics
///
/// Panics if `uf` is not a valid Brazilian state code.
///
/// # Errors
///
/// This function panics on invalid state codes rather than returning `Result`.
#[allow(clippy::too_many_arguments)]
pub fn build_inutilizacao_request(
    year: u16,
    tax_id: &str,
    model: &str,
    series: u32,
    start_number: u32,
    end_number: u32,
    justification: &str,
    environment: SefazEnvironment,
    uf: &str,
) -> String {
    let cuf = get_state_code(uf).expect("Invalid state code");
    let tp_amb = environment.as_str();
    let digits: String = tax_id.chars().filter(|c| c.is_ascii_digit()).collect();

    // PHP: str_pad($cnpj, 14, '0', STR_PAD_LEFT) — always pad to 14 for the ID
    let padded_id_tax = format!("{digits:0>14}");
    let id = format!(
        "ID{cuf}{year:02}{padded_id_tax}{model:0>2}{series:03}{start_number:09}{end_number:09}"
    );

    // PHP: if siglaUF == 'MT' && strlen($cnpj) == 11 => use <CPF>, else <CNPJ>
    let tax_tag = if digits.len() == 11 {
        format!("<CPF>{digits}</CPF>")
    } else {
        format!("<CNPJ>{digits}</CNPJ>")
    };

    format!(
        "<inutNFe xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><infInut Id=\"{id}\"><tpAmb>{tp_amb}</tpAmb><xServ>INUTILIZAR</xServ><cUF>{cuf}</cUF><ano>{year:02}</ano>{tax_tag}<mod>{model}</mod><serie>{series}</serie><nNFIni>{start_number}</nNFIni><nNFFin>{end_number}</nNFFin><xJust>{justification}</xJust></infInut></inutNFe>"
    )
}

/// Build a SEFAZ cancellation event request XML.
///
/// Builds the complete `<envEvento>` wrapper containing a cancellation event
/// (`tpEvento=110111`) for a previously authorized NF-e.
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e to cancel.
/// * `protocol` - The protocol number from the authorization response.
/// * `justification` - Justification text (minimum 15 characters).
/// * `seq` - Event sequence number (usually 1).
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the issuer.
///
/// # Panics
///
/// Panics if `justification` is empty.
///
/// # Errors
///
/// This function panics on invalid input rather than returning `Result`.
pub fn build_cancela_request(
    access_key: &str,
    protocol: &str,
    justification: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    assert!(
        !justification.is_empty(),
        "Cancellation justification (xJust) is required"
    );

    let additional = format!("<nProt>{protocol}</nProt><xJust>{justification}</xJust>");

    build_event_xml(
        access_key,
        event_types::CANCELLATION,
        seq,
        tax_id,
        environment,
        &additional,
    )
}

/// Build a SEFAZ CCe (correction letter) event request XML.
///
/// Builds the complete `<envEvento>` wrapper containing a Carta de Correcao
/// (`tpEvento=110110`) for a previously authorized NF-e.
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e to correct.
/// * `correction` - The correction text describing what is being changed.
/// * `seq` - Event sequence number (increments for each correction on the same NF-e).
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the issuer.
///
/// # Panics
///
/// Panics if `correction` is empty.
///
/// # Errors
///
/// This function panics on invalid input rather than returning `Result`.
pub fn build_cce_request(
    access_key: &str,
    correction: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    assert!(
        !correction.is_empty(),
        "Correction text (xCorrecao) is required for CCe"
    );

    let x_cond_uso = concat!(
        "A Carta de Correcao e disciplinada pelo paragrafo ",
        "1o-A do art. 7o do Convenio S/N, de 15 de dezembro de 1970 ",
        "e pode ser utilizada para regularizacao de erro ocorrido ",
        "na emissao de documento fiscal, desde que o erro nao esteja ",
        "relacionado com: I - as variaveis que determinam o valor ",
        "do imposto tais como: base de calculo, aliquota, ",
        "diferenca de preco, quantidade, valor da operacao ou da ",
        "prestacao; II - a correcao de dados cadastrais que implique ",
        "mudanca do remetente ou do destinatario; III - a data de ",
        "emissao ou de saida."
    );

    let additional =
        format!("<xCorrecao>{correction}</xCorrecao><xCondUso>{x_cond_uso}</xCondUso>");

    build_event_xml(
        access_key,
        event_types::CCE,
        seq,
        tax_id,
        environment,
        &additional,
    )
}

/// Build a SEFAZ manifest (manifestacao do destinatario) event request XML.
///
/// Builds the complete `<envEvento>` wrapper containing a manifestation event.
/// Valid event types are:
/// - `"210200"` (Confirmacao da Operacao)
/// - `"210210"` (Ciencia da Operacao)
/// - `"210220"` (Desconhecimento da Operacao)
/// - `"210240"` (Operacao nao Realizada) - requires justification
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `event_type` - Event type code as string (e.g. `"210210"`).
/// * `justification` - Required only for `"210240"` (operation not performed).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the recipient.
///
/// # Errors
///
/// This function does not return errors but may produce invalid XML if the
/// event type is not one of the valid manifest types.
pub fn build_manifesta_request(
    access_key: &str,
    event_type: &str,
    justification: Option<&str>,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let tp_evento: u32 = event_type.parse().unwrap_or(0);

    let additional = if tp_evento == event_types::OPERATION_NOT_PERFORMED {
        match justification {
            Some(just) if !just.is_empty() => format!("<xJust>{just}</xJust>"),
            _ => String::new(),
        }
    } else {
        String::new()
    };

    // Manifestacao do destinatario always uses cOrgao=91 (Ambiente Nacional)
    // PHP: $this->sefazEvento('AN', ...) where UFList::getCodeByUF('AN') = 91
    build_event_xml_with_org(
        access_key,
        tp_evento,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Build a SEFAZ DistDFe (distribution) request XML (`<distDFeInt>`).
///
/// Queries the distribution of fiscal documents (DF-e) from the national
/// environment. Can search by last NSU, specific NSU, or access key.
///
/// # Arguments
///
/// * `uf` - State abbreviation of the interested party.
/// * `tax_id` - CNPJ or CPF of the interested party.
/// * `nsu` - Specific NSU or last NSU to query. If this is a 44-digit
///   all-numeric string, it is treated as an access key (`consChNFe`).
///   If `Some` with a 15-digit NSU, it uses `consNSU`.
///   If `None`, defaults to `distNSU` with `ultNSU=000000000000000`.
/// * `access_key` - Optional 44-digit access key for direct lookup.
/// * `environment` - SEFAZ environment.
///
/// # Panics
///
/// Panics if `uf` is not a valid Brazilian state code.
///
/// # Errors
///
/// This function panics on invalid state codes rather than returning `Result`.
pub fn build_dist_dfe_request(
    uf: &str,
    tax_id: &str,
    nsu: Option<&str>,
    access_key: Option<&str>,
    environment: SefazEnvironment,
) -> String {
    let cuf = get_state_code(uf).expect("Invalid state code");
    let tp_amb = environment.as_str();
    let tax_id_tag = tax_id_xml_tag(tax_id);

    let query_tag = if let Some(ch_nfe) = access_key {
        if ch_nfe.len() == 44 && ch_nfe.chars().all(|c| c.is_ascii_digit()) {
            format!("<consChNFe><chNFe>{ch_nfe}</chNFe></consChNFe>")
        } else {
            // Treat as specific NSU
            format!("<consNSU><NSU>{ch_nfe}</NSU></consNSU>")
        }
    } else if let Some(nsu_val) = nsu {
        if nsu_val == "000000000000000" || nsu_val.starts_with('0') {
            // ultNSU (last NSU for incremental distribution)
            format!("<distNSU><ultNSU>{nsu_val}</ultNSU></distNSU>")
        } else {
            // Specific NSU
            format!("<consNSU><NSU>{nsu_val}</NSU></consNSU>")
        }
    } else {
        "<distNSU><ultNSU>000000000000000</ultNSU></distNSU>".to_string()
    };

    format!(
        "<distDFeInt xmlns=\"{NFE_NAMESPACE}\" versao=\"1.01\"><tpAmb>{tp_amb}</tpAmb><cUFAutor>{cuf}</cUFAutor>{tax_id_tag}{query_tag}</distDFeInt>"
    )
}

/// Build a SEFAZ cadastro (taxpayer registration) query XML (`<ConsCad>`).
///
/// Queries the SEFAZ taxpayer registry for a given state, searching by
/// CNPJ, IE (state tax ID), or CPF.
///
/// # Arguments
///
/// * `uf` - State abbreviation to query.
/// * `search_type` - One of `"CNPJ"`, `"IE"`, or `"CPF"`.
/// * `search_value` - The document number to search for.
///
/// # Errors
///
/// This function does not return `Result` errors.
pub fn build_cadastro_request(uf: &str, search_type: &str, search_value: &str) -> String {
    let filter = match search_type {
        "CNPJ" => format!("<CNPJ>{search_value}</CNPJ>"),
        "IE" => format!("<IE>{search_value}</IE>"),
        "CPF" => format!("<CPF>{search_value}</CPF>"),
        _ => String::new(),
    };

    format!(
        "<ConsCad xmlns=\"{NFE_NAMESPACE}\" versao=\"2.00\"><infCons><xServ>CONS-CAD</xServ><UF>{uf}</UF>{filter}</infCons></ConsCad>"
    )
}

/// Build a SEFAZ cancellation-by-substitution event request XML
/// (`tpEvento=110112`).
///
/// Used exclusively for NFC-e (model 65). Sends the event to
/// `RecepcaoEvento` of the issuing state (cOrgao from access key).
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NFC-e being cancelled.
/// * `ref_access_key` - The 44-digit access key of the replacement NFC-e.
/// * `protocol` - The authorization protocol of the original NFC-e.
/// * `justification` - Reason for cancellation (max 255 chars).
/// * `ver_aplic` - Version of the issuing application.
/// * `seq` - Event sequence number (usually 1).
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the issuer.
#[allow(clippy::too_many_arguments)]
pub fn build_cancel_substituicao_request(
    access_key: &str,
    ref_access_key: &str,
    protocol: &str,
    justification: &str,
    ver_aplic: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    assert!(
        !justification.is_empty(),
        "Cancellation justification (xJust) is required"
    );
    assert!(
        !ref_access_key.is_empty(),
        "Reference access key (chNFeRef) is required"
    );
    assert!(
        !ver_aplic.is_empty(),
        "Application version (verAplic) is required"
    );

    let c_orgao = &access_key[..2];
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <nProt>{protocol}</nProt>\
         <xJust>{justification}</xJust>\
         <chNFeRef>{ref_access_key}</chNFeRef>"
    );

    // Cancelamento por substituição goes to state endpoint (cOrgao from key)
    build_event_xml(
        access_key,
        event_types::CANCEL_SUBSTITUICAO,
        seq,
        tax_id,
        environment,
        &additional,
    )
}

/// Build a SEFAZ ator interessado event request XML (`tpEvento=110150`).
///
/// Authorizes a transporter (or subcontracted transporters) to access the
/// NF-e. Sent to the Ambiente Nacional (AN, cOrgao=91).
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `tp_autor` - Author type (1=emitente, 2=destinatário, 3=transportador).
/// * `ver_aplic` - Version of the issuing application.
/// * `authorized_cnpj` - Optional CNPJ to authorize (mutually exclusive with `authorized_cpf`).
/// * `authorized_cpf` - Optional CPF to authorize (mutually exclusive with `authorized_cnpj`).
/// * `tp_autorizacao` - Authorization type (0=not allowed to subcontract, 1=allowed).
/// * `issuer_uf` - UF of the issuer (for cOrgaoAutor).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the event sender.
#[allow(clippy::too_many_arguments)]
pub fn build_ator_interessado_request(
    access_key: &str,
    tp_autor: u8,
    ver_aplic: &str,
    authorized_cnpj: Option<&str>,
    authorized_cpf: Option<&str>,
    tp_autorizacao: u8,
    issuer_uf: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let c_uf = get_state_code(issuer_uf).expect("Invalid state code");

    let auth_tag = if let Some(cnpj) = authorized_cnpj {
        format!("<CNPJ>{cnpj}</CNPJ>")
    } else if let Some(cpf) = authorized_cpf {
        format!("<CPF>{cpf}</CPF>")
    } else {
        panic!("Either authorized_cnpj or authorized_cpf must be provided");
    };

    let mut additional = format!(
        "<cOrgaoAutor>{c_uf}</cOrgaoAutor>\
         <tpAutor>{tp_autor}</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <autXML>{auth_tag}</autXML>\
         <tpAutorizacao>{tp_autorizacao}</tpAutorizacao>"
    );

    if tp_autorizacao == 1 {
        let x_cond_uso = concat!(
            "O emitente ou destinatario da NF-e, declara que permite o ",
            "transportador declarado no campo CNPJ/CPF deste evento a ",
            "autorizar os transportadores subcontratados ou redespachados a ",
            "terem acesso ao download da NF-e"
        );
        additional.push_str(&format!("<xCondUso>{x_cond_uso}</xCondUso>"));
    }

    // Ator interessado always goes to AN (cOrgao=91)
    build_event_xml_with_org(
        access_key,
        event_types::ATOR_INTERESSADO,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Build a SEFAZ comprovante de entrega event request XML
/// (`tpEvento=110130`).
///
/// Records proof of delivery for an NF-e. Sent to Ambiente Nacional (AN).
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `ver_aplic` - Version of the issuing application.
/// * `delivery_date` - Date/time of delivery (ISO 8601 format).
/// * `doc_number` - Document number of the receiver.
/// * `name` - Name of the receiver.
/// * `lat` - Optional GPS latitude.
/// * `long` - Optional GPS longitude.
/// * `hash` - Base64-encoded SHA-1 hash of the delivery proof.
/// * `hash_date` - Date/time of the hash generation (ISO 8601 format).
/// * `issuer_uf` - UF of the issuer (for cOrgaoAutor).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the event sender.
#[allow(clippy::too_many_arguments)]
pub fn build_comprovante_entrega_request(
    access_key: &str,
    ver_aplic: &str,
    delivery_date: &str,
    doc_number: &str,
    name: &str,
    lat: Option<&str>,
    long: Option<&str>,
    hash: &str,
    hash_date: &str,
    issuer_uf: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let c_uf = get_state_code(issuer_uf).expect("Invalid state code");

    let mut additional = format!(
        "<cOrgaoAutor>{c_uf}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <dhEntrega>{delivery_date}</dhEntrega>\
         <nDoc>{doc_number}</nDoc>\
         <xNome>{name}</xNome>"
    );

    if let (Some(lat_val), Some(long_val)) = (lat, long) {
        if !lat_val.is_empty() && !long_val.is_empty() {
            additional.push_str(&format!(
                "<latGPS>{lat_val}</latGPS><longGPS>{long_val}</longGPS>"
            ));
        }
    }

    additional.push_str(&format!(
        "<hashComprovante>{hash}</hashComprovante>\
         <dhHashComprovante>{hash_date}</dhHashComprovante>"
    ));

    // Comprovante de entrega goes to AN (cOrgao=91)
    build_event_xml_with_org(
        access_key,
        event_types::COMPROVANTE_ENTREGA,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Build a SEFAZ cancelamento do comprovante de entrega event request XML
/// (`tpEvento=110131`).
///
/// Cancels a previously registered delivery receipt. Sent to AN.
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `ver_aplic` - Version of the issuing application.
/// * `event_protocol` - Protocol number of the delivery receipt event being cancelled.
/// * `issuer_uf` - UF of the issuer (for cOrgaoAutor).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the event sender.
#[allow(clippy::too_many_arguments)]
pub fn build_cancel_comprovante_entrega_request(
    access_key: &str,
    ver_aplic: &str,
    event_protocol: &str,
    issuer_uf: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let c_uf = get_state_code(issuer_uf).expect("Invalid state code");

    let additional = format!(
        "<cOrgaoAutor>{c_uf}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <nProtEvento>{event_protocol}</nProtEvento>"
    );

    build_event_xml_with_org(
        access_key,
        event_types::CANCEL_COMPROVANTE_ENTREGA,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Build a SEFAZ insucesso na entrega event request XML
/// (`tpEvento=110192`).
///
/// Records a failed delivery attempt. Sent to Ambiente Nacional (AN).
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `ver_aplic` - Version of the issuing application.
/// * `attempt_date` - Date/time of the delivery attempt (ISO 8601 format).
/// * `attempt_number` - Optional attempt number.
/// * `reason_type` - Reason code (1=recusado, 2=ausente, 3=não localizado, 4=outros).
/// * `reason_justification` - Required when reason_type=4.
/// * `lat` - Optional GPS latitude.
/// * `long` - Optional GPS longitude.
/// * `hash` - Base64-encoded SHA-1 hash of the attempt proof.
/// * `hash_date` - Date/time of the hash generation (ISO 8601 format).
/// * `issuer_uf` - UF of the issuer (for cOrgaoAutor).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the event sender.
#[allow(clippy::too_many_arguments)]
pub fn build_insucesso_entrega_request(
    access_key: &str,
    ver_aplic: &str,
    attempt_date: &str,
    attempt_number: Option<u32>,
    reason_type: u8,
    reason_justification: Option<&str>,
    lat: Option<&str>,
    long: Option<&str>,
    hash: &str,
    hash_date: &str,
    issuer_uf: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let c_uf = get_state_code(issuer_uf).expect("Invalid state code");

    let mut additional = format!(
        "<cOrgaoAutor>{c_uf}</cOrgaoAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <dhTentativaEntrega>{attempt_date}</dhTentativaEntrega>"
    );

    if let Some(n) = attempt_number {
        if n > 0 {
            additional.push_str(&format!("<nTentativa>{n}</nTentativa>"));
        }
    }

    additional.push_str(&format!("<tpMotivo>{reason_type}</tpMotivo>"));

    if reason_type == 4 {
        if let Some(just) = reason_justification {
            if !just.is_empty() {
                additional.push_str(&format!("<xJustMotivo>{just}</xJustMotivo>"));
            }
        }
    }

    if let (Some(lat_val), Some(long_val)) = (lat, long) {
        if !lat_val.is_empty() && !long_val.is_empty() {
            additional.push_str(&format!(
                "<latGPS>{lat_val}</latGPS><longGPS>{long_val}</longGPS>"
            ));
        }
    }

    additional.push_str(&format!(
        "<hashTentativaEntrega>{hash}</hashTentativaEntrega>\
         <dhHashTentativaEntrega>{hash_date}</dhHashTentativaEntrega>"
    ));

    // Insucesso na entrega goes to AN (cOrgao=91)
    build_event_xml_with_org(
        access_key,
        event_types::INSUCESSO_ENTREGA,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Build a SEFAZ cancelamento do insucesso de entrega event request XML
/// (`tpEvento=110193`).
///
/// Cancels a previously registered delivery failure event. Sent to AN.
///
/// # Arguments
///
/// * `access_key` - The 44-digit access key of the NF-e.
/// * `ver_aplic` - Version of the issuing application.
/// * `event_protocol` - Protocol number of the delivery failure event being cancelled.
/// * `issuer_uf` - UF of the issuer (for cOrgaoAutor).
/// * `seq` - Event sequence number.
/// * `environment` - SEFAZ environment.
/// * `tax_id` - CNPJ or CPF of the event sender.
#[allow(clippy::too_many_arguments)]
pub fn build_cancel_insucesso_entrega_request(
    access_key: &str,
    ver_aplic: &str,
    event_protocol: &str,
    issuer_uf: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let c_uf = get_state_code(issuer_uf).expect("Invalid state code");

    let additional = format!(
        "<cOrgaoAutor>{c_uf}</cOrgaoAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <nProtEvento>{event_protocol}</nProtEvento>"
    );

    build_event_xml_with_org(
        access_key,
        event_types::CANCEL_INSUCESSO_ENTREGA,
        seq,
        tax_id,
        environment,
        &additional,
        Some("91"),
    )
}

/// Data extracted from an NF-e XML for building an EPEC event request.
///
/// All fields are extracted from the signed NF-e XML. The struct is used
/// as input to [`build_epec_request`] to avoid a long parameter list.
#[derive(Debug, Clone)]
pub struct EpecData {
    /// 44-digit NF-e access key (from `infNFe@Id`, without "NFe" prefix).
    pub access_key: String,
    /// IBGE code of the issuer's state (first 2 digits of the access key).
    pub c_orgao_autor: String,
    /// Application version string (from `<verProc>` or caller override).
    pub ver_aplic: String,
    /// Emission date-time (from `<dhEmi>`).
    pub dh_emi: String,
    /// Fiscal operation type (from `<tpNF>`): 0=entrada, 1=saída.
    pub tp_nf: String,
    /// Issuer's state tax registration (from `<emit><IE>`).
    pub emit_ie: String,
    /// Recipient's state abbreviation (from `<dest><UF>`).
    pub dest_uf: String,
    /// Recipient's tax ID XML fragment: `<CNPJ>...</CNPJ>`, `<CPF>...</CPF>`,
    /// or `<idEstrangeiro>...</idEstrangeiro>`.
    pub dest_id_tag: String,
    /// Recipient's state tax registration (from `<dest><IE>`), if any.
    pub dest_ie: Option<String>,
    /// Total NF-e value (from `<total><ICMSTot><vNF>`).
    pub v_nf: String,
    /// Total ICMS value (from `<total><ICMSTot><vICMS>`).
    pub v_icms: String,
    /// Total ICMS-ST value (from `<total><ICMSTot><vST>`).
    pub v_st: String,
    /// CNPJ or CPF of the issuer (for the event envelope).
    pub tax_id: String,
}

/// Extract [`EpecData`] from a signed NF-e XML string.
///
/// Parses the XML to extract all fields needed by the EPEC event. The
/// `ver_aplic_override` parameter, when `Some`, overrides the `<verProc>`
/// value from the XML (matching the PHP behavior where `$this->verAplic`
/// can override).
///
/// # Errors
///
/// Returns [`fiscal_core::FiscalError::XmlParsing`] if required tags are missing.
pub fn extract_epec_data(
    nfe_xml: &str,
    ver_aplic_override: Option<&str>,
) -> Result<EpecData, fiscal_core::FiscalError> {
    use fiscal_core::FiscalError;

    // Extract access key from infNFe@Id
    let inf_nfe_start = nfe_xml
        .find("<infNFe")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <infNFe> in NF-e XML".into()))?;
    let inf_nfe_header_end = nfe_xml[inf_nfe_start..]
        .find('>')
        .ok_or_else(|| FiscalError::XmlParsing("Malformed <infNFe> tag".into()))?
        + inf_nfe_start;
    let inf_nfe_header = &nfe_xml[inf_nfe_start..inf_nfe_header_end];

    let id_pattern = "Id=\"";
    let id_start = inf_nfe_header
        .find(id_pattern)
        .ok_or_else(|| FiscalError::XmlParsing("Missing Id attribute in <infNFe>".into()))?
        + id_pattern.len();
    let id_end = inf_nfe_header[id_start..]
        .find('"')
        .ok_or_else(|| FiscalError::XmlParsing("Malformed Id attribute in <infNFe>".into()))?
        + id_start;
    let id_value = &inf_nfe_header[id_start..id_end];

    let access_key = id_value.strip_prefix("NFe").unwrap_or(id_value).to_string();
    if access_key.len() != 44 {
        return Err(FiscalError::XmlParsing(format!(
            "Invalid access key length: expected 44, got {}",
            access_key.len()
        )));
    }

    let c_orgao_autor = access_key[..2].to_string();

    // Extract emit section for IE
    let emit_section = extract_section(nfe_xml, "emit")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <emit> section in NF-e XML".into()))?;
    let emit_ie = extract_xml_tag_value(&emit_section, "IE")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <IE> in <emit>".into()))?;

    // Extract tax_id from emit (CNPJ or CPF)
    let tax_id = extract_xml_tag_value(&emit_section, "CNPJ")
        .or_else(|| extract_xml_tag_value(&emit_section, "CPF"))
        .ok_or_else(|| FiscalError::XmlParsing("Missing CNPJ/CPF in <emit>".into()))?;

    // Extract dest section
    let dest_section = extract_section(nfe_xml, "dest")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <dest> section in NF-e XML".into()))?;
    let dest_uf = extract_xml_tag_value(&dest_section, "UF")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <UF> in <dest>".into()))?;

    // Dest ID: try CNPJ, then CPF, then idEstrangeiro (same order as PHP)
    let dest_id_tag = if let Some(cnpj) = extract_xml_tag_value(&dest_section, "CNPJ") {
        format!("<CNPJ>{cnpj}</CNPJ>")
    } else if let Some(cpf) = extract_xml_tag_value(&dest_section, "CPF") {
        format!("<CPF>{cpf}</CPF>")
    } else if let Some(id_est) = extract_xml_tag_value(&dest_section, "idEstrangeiro") {
        format!("<idEstrangeiro>{id_est}</idEstrangeiro>")
    } else {
        return Err(FiscalError::XmlParsing(
            "Missing CNPJ/CPF/idEstrangeiro in <dest>".into(),
        ));
    };

    // Dest IE (optional)
    let dest_ie = extract_xml_tag_value(&dest_section, "IE").filter(|v| !v.is_empty());

    // Extract total section
    let total_section = extract_section(nfe_xml, "total")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <total> section in NF-e XML".into()))?;
    let v_nf = extract_xml_tag_value(&total_section, "vNF")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <vNF> in <total>".into()))?;
    let v_icms = extract_xml_tag_value(&total_section, "vICMS")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <vICMS> in <total>".into()))?;
    let v_st = extract_xml_tag_value(&total_section, "vST")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <vST> in <total>".into()))?;

    // Other fields
    let ver_proc = extract_xml_tag_value(nfe_xml, "verProc").unwrap_or_default();
    let ver_aplic = match ver_aplic_override {
        Some(v) if !v.is_empty() => v.to_string(),
        _ => ver_proc,
    };

    let dh_emi = extract_xml_tag_value(nfe_xml, "dhEmi")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <dhEmi> in NF-e XML".into()))?;
    let tp_nf = extract_xml_tag_value(nfe_xml, "tpNF")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <tpNF> in NF-e XML".into()))?;

    Ok(EpecData {
        access_key,
        c_orgao_autor,
        ver_aplic,
        dh_emi,
        tp_nf,
        emit_ie,
        dest_uf,
        dest_id_tag,
        dest_ie,
        v_nf,
        v_icms,
        v_st,
        tax_id,
    })
}

/// Build a SEFAZ EPEC (Evento Prévio de Emissão em Contingência) event
/// request XML (`tpEvento=110140`).
///
/// The EPEC event is sent to the Ambiente Nacional (AN) with `cOrgao`
/// set to the IBGE code of the issuer's state. This matches the PHP
/// `Tools::sefazEPEC()` behavior.
///
/// # Arguments
///
/// * `epec_data` - Pre-extracted NF-e data (see [`extract_epec_data`]).
/// * `environment` - SEFAZ environment (production or homologation).
///
/// # Example
///
/// ```no_run
/// use fiscal_sefaz::request_builders::{build_epec_request, extract_epec_data};
/// use fiscal_core::types::SefazEnvironment;
///
/// let nfe_xml = "...signed NF-e XML...";
/// let data = extract_epec_data(nfe_xml, None).unwrap();
/// let request = build_epec_request(&data, SefazEnvironment::Homologation);
/// ```
pub fn build_epec_request(epec_data: &EpecData, environment: SefazEnvironment) -> String {
    // Build the EPEC-specific additional tags (detEvento content after descEvento)
    let dest_ie_tag = match &epec_data.dest_ie {
        Some(ie) => format!("<IE>{ie}</IE>"),
        None => String::new(),
    };

    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <dhEmi>{dh_emi}</dhEmi>\
         <tpNF>{tp_nf}</tpNF>\
         <IE>{emit_ie}</IE>\
         <dest>\
         <UF>{dest_uf}</UF>\
         {dest_id}\
         {dest_ie}\
         <vNF>{v_nf}</vNF>\
         <vICMS>{v_icms}</vICMS>\
         <vST>{v_st}</vST>\
         </dest>",
        c_orgao = epec_data.c_orgao_autor,
        ver_aplic = epec_data.ver_aplic,
        dh_emi = epec_data.dh_emi,
        tp_nf = epec_data.tp_nf,
        emit_ie = epec_data.emit_ie,
        dest_uf = epec_data.dest_uf,
        dest_id = epec_data.dest_id_tag,
        dest_ie = dest_ie_tag,
        v_nf = epec_data.v_nf,
        v_icms = epec_data.v_icms,
        v_st = epec_data.v_st,
    );

    // EPEC always goes to AN (cOrgao in the evento envelope = IBGE code of issuer's UF)
    // PHP: $this->sefazEvento('AN', $chNFe, self::EVT_EPEC, 1, $tagAdic, null, null)
    // In sefazEvento, when uf='AN', cOrgao = UFList::getCodeByUF('AN') = 91
    // But $ignore = $tpEvento == self::EVT_EPEC skips the servico() call's UF validation
    // The actual cOrgao in the evento XML is derived from the access key's first 2 digits
    // when not in the special list, BUT for EPEC the PHP calls:
    //   $cOrgao = UFList::getCodeByUF($uf)  where $uf = 'AN'
    // So cOrgao = 91 for EPEC events.
    build_event_xml_with_org(
        &epec_data.access_key,
        event_types::EPEC,
        1, // nSeqEvento = 1 always for EPEC
        &epec_data.tax_id,
        environment,
        &additional,
        Some("91"), // AN = 91
    )
}

/// Build a SEFAZ EPEC NFC-e status request XML (`<consStatServ>`).
///
/// Queries the operational status of the EPEC NFC-e service. This service
/// exists only for SP (São Paulo) and model 65 (NFC-e), matching the PHP
/// `sefazStatusEpecNfce()` method from `TraitEPECNfce`.
///
/// # Arguments
///
/// * `uf` - State abbreviation (must be `"SP"`).
/// * `environment` - SEFAZ environment (production or homologation).
///
/// # Panics
///
/// Panics if `uf` is not a valid Brazilian state code.
pub fn build_epec_nfce_status_request(uf: &str, environment: SefazEnvironment) -> String {
    let cuf = get_state_code(uf).expect("Invalid state code");
    let tp_amb = environment.as_str();

    format!(
        "<consStatServ xmlns=\"{NFE_NAMESPACE}\" versao=\"{NFE_VERSION}\"><tpAmb>{tp_amb}</tpAmb><cUF>{cuf}</cUF><xServ>STATUS</xServ></consStatServ>"
    )
}

/// Data extracted from an NFC-e XML for building an EPEC NFC-e event request.
///
/// Similar to [`EpecData`] but adapted for NFC-e EPEC (SP only). Key
/// differences from the regular EPEC:
/// - No `vST` field (NFC-e EPEC does not include ICMS-ST value)
/// - Destination section is optional (NFC-e can have no recipient)
/// - Event is sent to the state's EPEC endpoint, not Ambiente Nacional
///
/// All fields are extracted from the signed NFC-e XML. Used as input to
/// [`build_epec_nfce_request`].
#[derive(Debug, Clone)]
pub struct EpecNfceData {
    /// 44-digit NFC-e access key (from `infNFe@Id`, without "NFe" prefix).
    pub access_key: String,
    /// IBGE code of the issuer's state (first 2 digits of the access key).
    pub c_orgao_autor: String,
    /// Application version string (from `<verProc>` or caller override).
    pub ver_aplic: String,
    /// Emission date-time (from `<dhEmi>`).
    pub dh_emi: String,
    /// Fiscal operation type (from `<tpNF>`): 0=entrada, 1=saída.
    pub tp_nf: String,
    /// Issuer's state tax registration (from `<emit><IE>`).
    pub emit_ie: String,
    /// Recipient's state abbreviation (from `<dest><UF>`), if present.
    pub dest_uf: Option<String>,
    /// Recipient's tax ID XML fragment: `<CNPJ>...</CNPJ>`, `<CPF>...</CPF>`,
    /// or `<idEstrangeiro>...</idEstrangeiro>`. `None` if no recipient.
    pub dest_id_tag: Option<String>,
    /// Recipient's state tax registration (from `<dest><IE>`), if any.
    pub dest_ie: Option<String>,
    /// Total NF-e value (from `<total><ICMSTot><vNF>`).
    pub v_nf: String,
    /// Total ICMS value (from `<total><ICMSTot><vICMS>`).
    pub v_icms: String,
    /// CNPJ or CPF of the issuer (for the event envelope).
    pub tax_id: String,
}

/// Extract [`EpecNfceData`] from a signed NFC-e XML string.
///
/// Parses the XML to extract all fields needed by the EPEC NFC-e event.
/// Unlike [`extract_epec_data`], the destination section is optional (NFC-e
/// can be issued without a recipient) and `vST` is not extracted.
///
/// # Errors
///
/// Returns [`fiscal_core::FiscalError::XmlParsing`] if required tags are missing.
pub fn extract_epec_nfce_data(
    nfce_xml: &str,
    ver_aplic_override: Option<&str>,
) -> Result<EpecNfceData, fiscal_core::FiscalError> {
    use fiscal_core::FiscalError;

    // Extract access key from infNFe@Id
    let inf_nfe_start = nfce_xml
        .find("<infNFe")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <infNFe> in NFC-e XML".into()))?;
    let inf_nfe_header_end = nfce_xml[inf_nfe_start..]
        .find('>')
        .ok_or_else(|| FiscalError::XmlParsing("Malformed <infNFe> tag".into()))?
        + inf_nfe_start;
    let inf_nfe_header = &nfce_xml[inf_nfe_start..inf_nfe_header_end];

    let id_pattern = "Id=\"";
    let id_start = inf_nfe_header
        .find(id_pattern)
        .ok_or_else(|| FiscalError::XmlParsing("Missing Id attribute in <infNFe>".into()))?
        + id_pattern.len();
    let id_end = inf_nfe_header[id_start..]
        .find('"')
        .ok_or_else(|| FiscalError::XmlParsing("Malformed Id attribute in <infNFe>".into()))?
        + id_start;
    let id_value = &inf_nfe_header[id_start..id_end];

    let access_key = id_value.strip_prefix("NFe").unwrap_or(id_value).to_string();
    if access_key.len() != 44 {
        return Err(FiscalError::XmlParsing(format!(
            "Invalid access key length: expected 44, got {}",
            access_key.len()
        )));
    }

    let c_orgao_autor = access_key[..2].to_string();

    // Extract emit section for IE and tax_id
    let emit_section = extract_section(nfce_xml, "emit")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <emit> section in NFC-e XML".into()))?;
    let emit_ie = extract_xml_tag_value(&emit_section, "IE")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <IE> in <emit>".into()))?;

    let tax_id = extract_xml_tag_value(&emit_section, "CNPJ")
        .or_else(|| extract_xml_tag_value(&emit_section, "CPF"))
        .ok_or_else(|| FiscalError::XmlParsing("Missing CNPJ/CPF in <emit>".into()))?;

    // Extract dest section (optional for NFC-e)
    let dest_section = extract_section(nfce_xml, "dest");
    let (dest_uf, dest_id_tag, dest_ie) = if let Some(ref dest) = dest_section {
        let uf = extract_xml_tag_value(dest, "UF");

        let id_tag = if let Some(cnpj) = extract_xml_tag_value(dest, "CNPJ") {
            Some(format!("<CNPJ>{cnpj}</CNPJ>"))
        } else if let Some(cpf) = extract_xml_tag_value(dest, "CPF") {
            Some(format!("<CPF>{cpf}</CPF>"))
        } else {
            extract_xml_tag_value(dest, "idEstrangeiro")
                .map(|id_est| format!("<idEstrangeiro>{id_est}</idEstrangeiro>"))
        };

        let ie = extract_xml_tag_value(dest, "IE").filter(|v| !v.is_empty());

        (uf, id_tag, ie)
    } else {
        (None, None, None)
    };

    // Extract total section
    let total_section = extract_section(nfce_xml, "total")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <total> section in NFC-e XML".into()))?;
    let v_nf = extract_xml_tag_value(&total_section, "vNF")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <vNF> in <total>".into()))?;
    let v_icms = extract_xml_tag_value(&total_section, "vICMS")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <vICMS> in <total>".into()))?;

    // Other fields
    let ver_proc = extract_xml_tag_value(nfce_xml, "verProc").unwrap_or_default();
    let ver_aplic = match ver_aplic_override {
        Some(v) if !v.is_empty() => v.to_string(),
        _ => ver_proc,
    };

    let dh_emi = extract_xml_tag_value(nfce_xml, "dhEmi")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <dhEmi> in NFC-e XML".into()))?;
    let tp_nf = extract_xml_tag_value(nfce_xml, "tpNF")
        .ok_or_else(|| FiscalError::XmlParsing("Missing <tpNF> in NFC-e XML".into()))?;

    Ok(EpecNfceData {
        access_key,
        c_orgao_autor,
        ver_aplic,
        dh_emi,
        tp_nf,
        emit_ie,
        dest_uf,
        dest_id_tag,
        dest_ie,
        v_nf,
        v_icms,
        tax_id,
    })
}

/// Build a SEFAZ EPEC NFC-e event request XML (`tpEvento=110140`).
///
/// Builds the complete `<envEvento>` wrapper for an EPEC event specific to
/// NFC-e (model 65). This is only available in SP and differs from the
/// standard EPEC in several ways:
///
/// - Sent to the state's `RecepcaoEPEC` endpoint (not Ambiente Nacional)
/// - `cOrgao` is the state's IBGE code (not 91)
/// - No `<vST>` field in the event detail
/// - The `<dest>` section is optional (NFC-e can have no recipient)
///
/// Matches the PHP `sefazEpecNfce()` method from `TraitEPECNfce`.
///
/// # Arguments
///
/// * `epec_data` - Pre-extracted NFC-e data (see [`extract_epec_nfce_data`]).
/// * `environment` - SEFAZ environment (production or homologation).
pub fn build_epec_nfce_request(epec_data: &EpecNfceData, environment: SefazEnvironment) -> String {
    // Build the optional dest section
    let dest_tag = if let Some(ref dest_id) = epec_data.dest_id_tag {
        let dest_uf = epec_data
            .dest_uf
            .as_deref()
            .unwrap_or(&epec_data.c_orgao_autor);
        let dest_ie_tag = match &epec_data.dest_ie {
            Some(ie) => format!("<IE>{ie}</IE>"),
            None => String::new(),
        };
        format!("<dest><UF>{dest_uf}</UF>{dest_id}{dest_ie_tag}</dest>")
    } else {
        String::new()
    };

    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <dhEmi>{dh_emi}</dhEmi>\
         <tpNF>{tp_nf}</tpNF>\
         <IE>{emit_ie}</IE>\
         {dest_tag}\
         <vNF>{v_nf}</vNF>\
         <vICMS>{v_icms}</vICMS>",
        c_orgao = epec_data.c_orgao_autor,
        ver_aplic = epec_data.ver_aplic,
        dh_emi = epec_data.dh_emi,
        tp_nf = epec_data.tp_nf,
        emit_ie = epec_data.emit_ie,
        v_nf = epec_data.v_nf,
        v_icms = epec_data.v_icms,
    );

    // EPEC NFC-e goes to the state endpoint, NOT AN.
    // cOrgao in the event envelope = IBGE code of the issuer's state.
    build_event_xml_with_org(
        &epec_data.access_key,
        event_types::EPEC,
        1, // nSeqEvento = 1 always for EPEC
        &epec_data.tax_id,
        environment,
        &additional,
        Some(&epec_data.c_orgao_autor),
    )
}

/// Item for a prorrogacao (ICMS extension) request.
///
/// Each item contains the item number and the requested quantity.
#[derive(Debug, Clone)]
pub struct ProrrogacaoItem {
    /// Item number in the original NF-e (starting from 1).
    pub num_item: u32,
    /// Quantity requested for prorrogacao.
    pub qtde: f64,
}

/// Build a SEFAZ prorrogacao (ICMS extension) event request XML.
///
/// Creates an `<envEvento>` wrapper containing a pedido de prorrogacao
/// (`tpEvento=111500` for first term, `111501` for second term).
///
/// # Arguments
///
/// * `access_key` — 44-digit access key of the NF-e.
/// * `protocol` — authorization protocol number of the original NF-e.
/// * `items` — list of items with quantities being extended.
/// * `second_term` — if `true`, uses the second-term event type (111501).
/// * `seq` — event sequence number.
/// * `environment` — SEFAZ environment.
/// * `tax_id` — CNPJ or CPF of the issuer.
pub fn build_prorrogacao_request(
    access_key: &str,
    protocol: &str,
    items: &[ProrrogacaoItem],
    second_term: bool,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let tp_evento = if second_term {
        event_types::PRORROGACAO_2
    } else {
        event_types::PRORROGACAO_1
    };

    let mut additional = format!("<nProt>{protocol}</nProt>");
    for item in items {
        additional.push_str(&format!(
            "<itemPedido numItem=\"{}\"><qtdeItem>{}</qtdeItem></itemPedido>",
            item.num_item, item.qtde
        ));
    }

    build_event_xml(access_key, tp_evento, seq, tax_id, environment, &additional)
}

/// Build a SEFAZ cancel-prorrogacao (cancel ICMS extension) event request XML.
///
/// Creates an `<envEvento>` wrapper containing a cancelamento de pedido de
/// prorrogacao (`tpEvento=111502` for first term, `111503` for second term).
///
/// # Arguments
///
/// * `access_key` — 44-digit access key of the NF-e.
/// * `protocol` — authorization protocol number of the prorrogacao event.
/// * `second_term` — if `true`, uses the second-term event type (111503).
/// * `seq` — event sequence number.
/// * `environment` — SEFAZ environment.
/// * `tax_id` — CNPJ or CPF of the issuer.
pub fn build_cancel_prorrogacao_request(
    access_key: &str,
    protocol: &str,
    second_term: bool,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
) -> String {
    let (tp_evento, orig_event) = if second_term {
        (
            event_types::CANCEL_PRORROGACAO_2,
            event_types::PRORROGACAO_2,
        )
    } else {
        (
            event_types::CANCEL_PRORROGACAO_1,
            event_types::PRORROGACAO_1,
        )
    };

    let id_pedido_cancelado = format!("ID{orig_event}{access_key}{seq:02}");
    let additional = format!(
        "<idPedidoCancelado>{id_pedido_cancelado}</idPedidoCancelado><nProt>{protocol}</nProt>"
    );

    build_event_xml(access_key, tp_evento, seq, tax_id, environment, &additional)
}

/// Build a SEFAZ CSC (Código de Segurança do Contribuinte) request XML
/// (`<admCscNFCe>`).
///
/// Manages the CSC for NFC-e (model 65). Used exclusively with the
/// `CscNFCe` web service.
///
/// # Arguments
///
/// * `environment` — SEFAZ environment (production or homologation).
/// * `ind_op` — Operation type: 1=query active CSCs, 2=request new CSC,
///   3=revoke active CSC.
/// * `cnpj` — Full CNPJ of the taxpayer (14 digits).
/// * `csc_id` — CSC identifier (required only for `ind_op=3`).
/// * `csc_code` — CSC code/value (required only for `ind_op=3`).
pub fn build_csc_request(
    environment: SefazEnvironment,
    ind_op: u8,
    cnpj: &str,
    csc_id: Option<&str>,
    csc_code: Option<&str>,
) -> String {
    let tp_amb = environment.as_str();
    let digits: String = cnpj.chars().filter(|c| c.is_ascii_digit()).collect();
    // raizCNPJ = first 8 digits of the CNPJ
    let raiz_cnpj = if digits.len() >= 8 {
        &digits[..8]
    } else {
        &digits
    };

    if ind_op == 3 {
        let id_csc = csc_id.unwrap_or("");
        let codigo_csc = csc_code.unwrap_or("");
        format!(
            "<admCscNFCe versao=\"1.00\" xmlns=\"{NFE_NAMESPACE}\">\
             <tpAmb>{tp_amb}</tpAmb>\
             <indOp>{ind_op}</indOp>\
             <raizCNPJ>{raiz_cnpj}</raizCNPJ>\
             <dadosCsc>\
             <idCsc>{id_csc}</idCsc>\
             <codigoCsc>{codigo_csc}</codigoCsc>\
             </dadosCsc>\
             </admCscNFCe>"
        )
    } else {
        format!(
            "<admCscNFCe versao=\"1.00\" xmlns=\"{NFE_NAMESPACE}\">\
             <tpAmb>{tp_amb}</tpAmb>\
             <indOp>{ind_op}</indOp>\
             <raizCNPJ>{raiz_cnpj}</raizCNPJ>\
             </admCscNFCe>"
        )
    }
}

/// An individual event for batch event submission.
///
/// Used with [`build_event_batch_request`] to send multiple events in a
/// single SOAP request, matching the PHP `sefazEventoLote()` pattern.
#[derive(Debug, Clone)]
pub struct EventItem {
    /// 44-digit access key of the NF-e.
    pub access_key: String,
    /// Event type code (e.g., `210200`, `210210`, `210220`, `210240`).
    pub event_type: u32,
    /// Event sequence number.
    pub seq: u32,
    /// CNPJ or CPF of the event sender.
    pub tax_id: String,
    /// Additional XML tags for `<detEvento>` (after `<descEvento>`).
    pub additional_tags: String,
}

/// Build a SEFAZ batch event request XML (`<envEvento>`) containing
/// multiple `<evento>` elements.
///
/// This matches the PHP `sefazEventoLote()` method. Each event is built
/// individually and concatenated inside a single `<envEvento>` wrapper.
///
/// Events with type EPEC (`110140`) are skipped, matching PHP behavior.
///
/// # Arguments
///
/// * `uf` — State abbreviation or `"AN"` for Ambiente Nacional.
/// * `events` — Slice of [`EventItem`] structs (max 20).
/// * `lot_id` — Lot identifier. If `None`, a timestamp-based ID is generated.
/// * `environment` — SEFAZ environment.
///
/// # Panics
///
/// Panics if `events` has more than 20 items or is empty.
pub fn build_event_batch_request(
    uf: &str,
    events: &[EventItem],
    lot_id: Option<&str>,
    environment: SefazEnvironment,
) -> String {
    assert!(
        !events.is_empty(),
        "Event batch must contain at least one event"
    );
    assert!(
        events.len() <= 20,
        "Event batch is limited to 20 events, got {}",
        events.len()
    );

    let c_orgao = get_state_code(uf).expect("Invalid state code");
    let tp_amb = environment.as_str();

    // Event datetime with BRT offset (-03:00)
    let dh_evento = chrono::Utc::now()
        .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).expect("valid offset"))
        .format("%Y-%m-%dT%H:%M:%S%:z")
        .to_string();

    let mut batch = String::new();
    for evt in events {
        // Skip EPEC events in batch — matches PHP behavior
        if evt.event_type == event_types::EPEC {
            continue;
        }

        let event_id = build_event_id(evt.event_type, &evt.access_key, evt.seq);
        let desc_evento = event_description(evt.event_type);
        let tax_tag = tax_id_xml_tag(&evt.tax_id);

        batch.push_str(&format!(
            "<evento xmlns=\"{NFE_NAMESPACE}\" versao=\"1.00\">\
             <infEvento Id=\"{event_id}\">\
             <cOrgao>{c_orgao}</cOrgao>\
             <tpAmb>{tp_amb}</tpAmb>\
             {tax_tag}\
             <chNFe>{access_key}</chNFe>\
             <dhEvento>{dh_evento}</dhEvento>\
             <tpEvento>{event_type}</tpEvento>\
             <nSeqEvento>{seq}</nSeqEvento>\
             <verEvento>1.00</verEvento>\
             <detEvento versao=\"1.00\">\
             <descEvento>{desc_evento}</descEvento>\
             {additional}\
             </detEvento>\
             </infEvento>\
             </evento>",
            access_key = evt.access_key,
            event_type = evt.event_type,
            seq = evt.seq,
            additional = evt.additional_tags,
        ));
    }

    let id_lote = match lot_id {
        Some(id) => id.to_string(),
        None => std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis().to_string())
            .unwrap_or_else(|_| "1".to_string()),
    };

    format!(
        "<envEvento xmlns=\"{NFE_NAMESPACE}\" versao=\"1.00\">\
         <idLote>{id_lote}</idLote>\
         {batch}\
         </envEvento>"
    )
}

/// Payment detail for a conciliação financeira event.
///
/// Used with [`build_conciliacao_request`] to describe each payment
/// method in the financial reconciliation.
#[derive(Debug, Clone)]
pub struct ConciliacaoDetPag {
    /// Payment indicator (optional, e.g., `"0"` = à vista, `"1"` = a prazo).
    pub ind_pag: Option<String>,
    /// Payment type code (e.g., `"01"` = dinheiro, `"03"` = cartão crédito).
    pub t_pag: String,
    /// Payment description (optional).
    pub x_pag: Option<String>,
    /// Payment value.
    pub v_pag: String,
    /// Payment date (YYYY-MM-DD).
    pub d_pag: String,
    /// CNPJ of the payment institution (optional).
    pub cnpj_pag: Option<String>,
    /// UF of the payment institution (optional, required with `cnpj_pag`).
    pub uf_pag: Option<String>,
    /// CNPJ of the payment intermediary (optional).
    pub cnpj_if: Option<String>,
    /// Card brand type (optional).
    pub t_band: Option<String>,
    /// Authorization code (optional).
    pub c_aut: Option<String>,
    /// CNPJ of the receiver (optional).
    pub cnpj_receb: Option<String>,
    /// UF of the receiver (optional, required with `cnpj_receb`).
    pub uf_receb: Option<String>,
}

/// Build a SEFAZ conciliação financeira event request XML
/// (`tpEvento=110750` or `110751` for cancellation).
///
/// Implements the PHP `sefazConciliacao()` method.
///
/// # Arguments
///
/// * `uf` — State abbreviation. For model 55 (NF-e), use `"SVRS"`;
///   for model 65 (NFC-e), use the actual state abbreviation.
/// * `access_key` — 44-digit access key.
/// * `ver_aplic` — Application version string.
/// * `det_pag` — Payment details (required for new conciliation, empty for cancel).
/// * `cancel` — If `true`, sends cancellation event (110751) instead.
/// * `cancel_protocol` — Protocol of the conciliation event being cancelled
///   (required when `cancel=true`).
/// * `seq` — Event sequence number.
/// * `environment` — SEFAZ environment.
/// * `tax_id` — CNPJ or CPF of the issuer.
#[allow(clippy::too_many_arguments)]
pub fn build_conciliacao_request(
    access_key: &str,
    ver_aplic: &str,
    det_pag: &[ConciliacaoDetPag],
    cancel: bool,
    cancel_protocol: Option<&str>,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    org_code_override: Option<&str>,
) -> String {
    if cancel {
        let protocol = cancel_protocol.unwrap_or("");
        let additional = format!(
            "<verAplic>{ver_aplic}</verAplic>\
             <nProtEvento>{protocol}</nProtEvento>"
        );
        build_event_xml_with_org(
            access_key,
            event_types::CANCEL_CONCILIACAO,
            seq,
            tax_id,
            environment,
            &additional,
            org_code_override,
        )
    } else {
        let mut additional = format!("<verAplic>{ver_aplic}</verAplic>");
        for pag in det_pag {
            additional.push_str("<detPag>");
            if let Some(ref ind) = pag.ind_pag {
                additional.push_str(&format!("<indPag>{ind}</indPag>"));
            }
            additional.push_str(&format!("<tPag>{}</tPag>", pag.t_pag));
            if let Some(ref x) = pag.x_pag {
                additional.push_str(&format!("<xPag>{x}</xPag>"));
            }
            additional.push_str(&format!("<vPag>{}</vPag>", pag.v_pag));
            additional.push_str(&format!("<dPag>{}</dPag>", pag.d_pag));
            if let (Some(cnpj), Some(uf)) = (&pag.cnpj_pag, &pag.uf_pag) {
                additional.push_str(&format!("<CNPJPag>{cnpj}</CNPJPag>"));
                additional.push_str(&format!("<UFPag>{uf}</UFPag>"));
                if let Some(ref cnpj_if) = pag.cnpj_if {
                    additional.push_str(&format!("<CNPJIF>{cnpj_if}</CNPJIF>"));
                }
            }
            if let Some(ref t_band) = pag.t_band {
                additional.push_str(&format!("<tBand>{t_band}</tBand>"));
            }
            if let Some(ref c_aut) = pag.c_aut {
                additional.push_str(&format!("<cAut>{c_aut}</cAut>"));
            }
            if let (Some(cnpj_receb), Some(uf_receb)) = (&pag.cnpj_receb, &pag.uf_receb) {
                additional.push_str(&format!("<CNPJReceb>{cnpj_receb}</CNPJReceb>"));
                additional.push_str(&format!("<UFReceb>{uf_receb}</UFReceb>"));
            }
            additional.push_str("</detPag>");
        }
        build_event_xml_with_org(
            access_key,
            event_types::CONCILIACAO,
            seq,
            tax_id,
            environment,
            &additional,
            org_code_override,
        )
    }
}

/// Extract a named section from XML (e.g., `<emit>...</emit>`).
///
/// Returns the full content between the opening and closing tags, inclusive.
fn extract_section(xml: &str, tag_name: &str) -> Option<String> {
    let open = format!("<{tag_name}");
    let close = format!("</{tag_name}>");

    let start = xml.find(&open)?;
    // Verify delimiter
    let after_open = start + open.len();
    if after_open < xml.len() {
        let next = xml.as_bytes()[after_open];
        if next != b' ' && next != b'>' && next != b'/' && next != b'\n' && next != b'\t' {
            return None;
        }
    }

    let end = xml[start..].find(&close)? + start + close.len();
    Some(xml[start..end].to_string())
}

// ── Internal helpers ────────────────────────────────────────────────────────

// ── RTC (Reforma Tributária Complementar) event builders ────────────────────

/// Item data for RTC events that list per-item IBS/CBS values.
#[derive(Debug, Clone)]
pub struct RtcItem {
    /// Item number (nItem attribute).
    pub item: u32,
    /// IBS value.
    pub v_ibs: f64,
    /// CBS value.
    pub v_cbs: f64,
    /// Quantity (used by stock-control events). Optional.
    pub quantidade: Option<f64>,
    /// Unit of measure (used by stock-control events). Optional.
    pub unidade: Option<String>,
    /// Referenced access key (DFeReferenciado). Optional.
    pub chave: Option<String>,
    /// Referenced item number. Optional.
    pub n_item: Option<u32>,
    /// IBS value inside gControleEstoque (fornecedor events). Optional.
    pub g_controle_estoque_v_ibs: Option<f64>,
    /// CBS value inside gControleEstoque (fornecedor events). Optional.
    pub g_controle_estoque_v_cbs: Option<f64>,
    /// IBS credit value (for bens/servicos events). Optional.
    pub v_cred_ibs: Option<f64>,
    /// CBS credit value (for bens/servicos events). Optional.
    pub v_cred_cbs: Option<f64>,
}

impl RtcItem {
    /// Create a minimal RTC item with IBS and CBS values.
    pub fn new(item: u32, v_ibs: f64, v_cbs: f64) -> Self {
        Self {
            item,
            v_ibs,
            v_cbs,
            quantidade: None,
            unidade: None,
            chave: None,
            n_item: None,
            g_controle_estoque_v_ibs: None,
            g_controle_estoque_v_cbs: None,
            v_cred_ibs: None,
            v_cred_cbs: None,
        }
    }
    /// Set quantity.
    pub fn quantidade(mut self, v: f64) -> Self {
        self.quantidade = Some(v);
        self
    }
    /// Set unit.
    pub fn unidade(mut self, v: impl Into<String>) -> Self {
        self.unidade = Some(v.into());
        self
    }
    /// Set referenced access key.
    pub fn chave(mut self, v: impl Into<String>) -> Self {
        self.chave = Some(v.into());
        self
    }
    /// Set referenced item number.
    pub fn n_item(mut self, v: u32) -> Self {
        self.n_item = Some(v);
        self
    }
}

/// Item for crédito presumido events.
#[derive(Debug, Clone)]
pub struct RtcCredPresItem {
    /// Item number.
    pub item: u32,
    /// Base de cálculo.
    pub v_bc: f64,
    /// IBS sub-group. Optional.
    pub g_ibs: Option<RtcCredPresSub>,
    /// CBS sub-group. Optional.
    pub g_cbs: Option<RtcCredPresSub>,
}

/// Sub-group for crédito presumido IBS or CBS.
#[derive(Debug, Clone)]
pub struct RtcCredPresSub {
    /// Código do crédito presumido.
    pub c_cred_pres: String,
    /// Percentual.
    pub p_cred_pres: f64,
    /// Valor.
    pub v_cred_pres: f64,
}

/// Build RTC event: Informação de pagamento integral (tpEvento=112110).
pub fn build_rtc_info_pagto_integral(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <indQuitacao>1</indQuitacao>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_INFO_PAGTO_INTEGRAL,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Solicitação de apropriação de crédito presumido (tpEvento=211110).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_sol_aprop_cred_presumido(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcCredPresItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let bc = format!("{:.2}", item.v_bc);
        additional.push_str(&format!(
            "<gCredPres nItem=\"{}\"><vBC>{bc}</vBC>",
            item.item
        ));
        if let Some(ref g) = item.g_ibs {
            let pc = format!("{:.4}", g.p_cred_pres);
            let vc = format!("{:.2}", g.v_cred_pres);
            additional.push_str(&format!(
                "<gIBS><cCredPres>{}</cCredPres><pCredPres>{pc}</pCredPres><vCredPres>{vc}</vCredPres></gIBS>",
                g.c_cred_pres
            ));
        }
        if let Some(ref g) = item.g_cbs {
            let pc = format!("{:.4}", g.p_cred_pres);
            let vc = format!("{:.2}", g.v_cred_pres);
            additional.push_str(&format!(
                "<gCBS><cCredPres>{}</cCredPres><pCredPres>{pc}</pCredPres><vCredPres>{vc}</vCredPres></gCBS>",
                g.c_cred_pres
            ));
        }
        additional.push_str("</gCredPres>");
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_SOL_APROP_CRED_PRESUMIDO,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Destinação de item para consumo pessoal (tpEvento=211120).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_destino_consumo_pessoal(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    tp_autor: u8,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>{tp_autor}</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qt = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        let ch = item.chave.as_deref().unwrap_or("");
        let ni = item.n_item.unwrap_or(0);
        additional.push_str(&format!(
            "<gConsumo nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qConsumo>{qt}</qConsumo><uConsumo>{u}</uConsumo></gControleEstoque>\
             <DFeReferenciado><chaveAcesso>{ch}</chaveAcesso><nItem>{ni}</nItem></DFeReferenciado>\
             </gConsumo>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_DESTINO_CONSUMO_PESSOAL,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Aceite de débito na apuração (tpEvento=211128).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_aceite_debito(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    ind_aceitacao: u8,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <indAceitacao>{ind_aceitacao}</indAceitacao>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_ACEITE_DEBITO,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Imobilização de item (tpEvento=211130).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_imobilizacao_item(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gImobilizacao nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qImobilizado>{qtd}</qImobilizado><uImobilizado>{u}</uImobilizado></gControleEstoque>\
             </gImobilizacao>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_IMOBILIZACAO_ITEM,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Apropriação de crédito combustível (tpEvento=211140).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_apropriacao_credito_comb(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gConsumoComb nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qComb>{qtd}</qComb><uComb>{u}</uComb></gControleEstoque>\
             </gConsumoComb>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_APROPRIACAO_CREDITO_COMB,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Apropriação de crédito bens/serviços (tpEvento=211150).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_apropriacao_credito_bens(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_cred_ibs.unwrap_or(item.v_ibs));
        let vc = format!("{:.2}", item.v_cred_cbs.unwrap_or(item.v_cbs));
        additional.push_str(&format!(
            "<gCredito nItem=\"{}\"><vCredIBS>{vi}</vCredIBS><vCredCBS>{vc}</vCredCBS></gCredito>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_APROPRIACAO_CREDITO_BENS,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Manifestação transferência crédito IBS (tpEvento=212110).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_manif_transf_cred_ibs(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    ind_aceitacao: u8,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>8</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <indAceitacao>{ind_aceitacao}</indAceitacao>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_MANIF_TRANSF_CRED_IBS,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Manifestação transferência crédito CBS (tpEvento=212120).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_manif_transf_cred_cbs(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    ind_aceitacao: u8,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>8</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <indAceitacao>{ind_aceitacao}</indAceitacao>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_MANIF_TRANSF_CRED_CBS,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Cancelamento de evento (tpEvento=110001).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_cancela_evento(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    tp_evento_aut: &str,
    n_prot_evento: &str,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <tpEventoAut>{tp_evento_aut}</tpEventoAut>\
         <nProtEvento>{n_prot_evento}</nProtEvento>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_CANCELA_EVENTO,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Importação ZFM não convertida em isenção (tpEvento=112120).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_importacao_zfm(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gConsumo nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qtde>{qtd}</qtde><unidade>{u}</unidade></gControleEstoque>\
             </gConsumo>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_IMPORTACAO_ZFM,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Perecimento/roubo transporte adquirente (tpEvento=211124).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_roubo_perda_adquirente(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>2</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gPerecimento nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qPerecimento>{qtd}</qPerecimento><uPerecimento>{u}</uPerecimento></gControleEstoque>\
             </gPerecimento>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_ROUBO_PERDA_ADQUIRENTE,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Perecimento/roubo transporte fornecedor (tpEvento=112130).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_roubo_perda_fornecedor(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let gvi = format!("{:.2}", item.g_controle_estoque_v_ibs.unwrap_or(0.0));
        let gvc = format!("{:.2}", item.g_controle_estoque_v_cbs.unwrap_or(0.0));
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gPerecimento nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qPerecimento>{qtd}</qPerecimento><uPerecimento>{u}</uPerecimento>\
             <vIBS>{gvi}</vIBS><vCBS>{gvc}</vCBS></gControleEstoque>\
             </gPerecimento>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_ROUBO_PERDA_FORNECEDOR,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Fornecimento não realizado (tpEvento=112140).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_fornecimento_nao_realizado(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    itens: &[RtcItem],
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let mut additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>"
    );
    for item in itens {
        let vi = format!("{:.2}", item.v_ibs);
        let vc = format!("{:.2}", item.v_cbs);
        let qtd = format!("{:.4}", item.quantidade.unwrap_or(0.0));
        let u = item.unidade.as_deref().unwrap_or("");
        additional.push_str(&format!(
            "<gItemNaoFornecido nItem=\"{}\"><vIBS>{vi}</vIBS><vCBS>{vc}</vCBS>\
             <gControleEstoque><qNaoFornecida>{qtd}</qNaoFornecida><uNaoFornecida>{u}</uNaoFornecida></gControleEstoque>\
             </gItemNaoFornecido>",
            item.item
        ));
    }
    build_event_xml_with_org(
        access_key,
        event_types::RTC_FORNECIMENTO_NAO_REALIZADO,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// Build RTC event: Atualização da data de previsão de entrega (tpEvento=112150).
#[allow(clippy::too_many_arguments)]
pub fn build_rtc_atualizacao_data_entrega(
    access_key: &str,
    seq: u32,
    environment: SefazEnvironment,
    tax_id: &str,
    c_uf: &str,
    ver_aplic: &str,
    data_prevista: &str,
    org_code_override: Option<&str>,
) -> String {
    let c_orgao = get_state_code(c_uf)
        .map(|c| c.to_string())
        .unwrap_or_default();
    let additional = format!(
        "<cOrgaoAutor>{c_orgao}</cOrgaoAutor>\
         <tpAutor>1</tpAutor>\
         <verAplic>{ver_aplic}</verAplic>\
         <dPrevEntrega>{data_prevista}</dPrevEntrega>"
    );
    build_event_xml_with_org(
        access_key,
        event_types::RTC_ATUALIZACAO_DATA_ENTREGA,
        seq,
        tax_id,
        environment,
        &additional,
        org_code_override,
    )
}

/// This is the core event builder used by cancellation, CCe, manifestation,
/// and other event-type request builders.
///
/// When `org_code_override` is `Some`, the provided value is used as `cOrgao`
/// instead of deriving it from the access key. This is needed for manifestation
/// events which must use code 91 (Ambiente Nacional).
fn build_event_xml(
    access_key: &str,
    event_type: u32,
    seq: u32,
    tax_id: &str,
    environment: SefazEnvironment,
    additional_tags: &str,
) -> String {
    build_event_xml_with_org(
        access_key,
        event_type,
        seq,
        tax_id,
        environment,
        additional_tags,
        None,
    )
}

/// Build a generic SEFAZ event XML with an optional `cOrgao` override.
fn build_event_xml_with_org(
    access_key: &str,
    event_type: u32,
    seq: u32,
    tax_id: &str,
    environment: SefazEnvironment,
    additional_tags: &str,
    org_code_override: Option<&str>,
) -> String {
    let event_id = build_event_id(event_type, access_key, seq);
    let desc_evento = event_description(event_type);
    let tax_id_tag = tax_id_xml_tag(tax_id);
    let tp_amb = environment.as_str();
    // cOrgao: use override when provided (e.g. "91" for manifestacao),
    // otherwise derive from the first 2 digits of the access key.
    let org_code_owned;
    let org_code = match org_code_override {
        Some(code) => code,
        None => {
            org_code_owned = access_key[..2].to_string();
            &org_code_owned
        }
    };
    // Use current timestamp as lot ID (milliseconds)
    let lot_id = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis().to_string())
        .unwrap_or_else(|_| "1".to_string());
    // Event datetime with BRT offset (-03:00)
    let dh_evento = chrono::Utc::now()
        .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).unwrap())
        .format("%Y-%m-%dT%H:%M:%S%:z")
        .to_string();

    format!(
        "<envEvento xmlns=\"{NFE_NAMESPACE}\" versao=\"1.00\"><idLote>{lot_id}</idLote><evento xmlns=\"{NFE_NAMESPACE}\" versao=\"1.00\"><infEvento Id=\"{event_id}\"><cOrgao>{org_code}</cOrgao><tpAmb>{tp_amb}</tpAmb>{tax_id_tag}<chNFe>{access_key}</chNFe><dhEvento>{dh_evento}</dhEvento><tpEvento>{event_type}</tpEvento><nSeqEvento>{seq}</nSeqEvento><verEvento>1.00</verEvento><detEvento versao=\"1.00\"><descEvento>{desc_evento}</descEvento>{additional_tags}</detEvento></infEvento></evento></envEvento>"
    )
}

/// Validate that an access key is exactly 44 numeric digits.
fn validate_access_key(access_key: &str) {
    assert!(!access_key.is_empty(), "Access key is required");
    assert!(
        access_key.len() == 44,
        "Invalid access key: must be exactly 44 digits, got {}",
        access_key.len()
    );
    assert!(
        access_key.chars().all(|c| c.is_ascii_digit()),
        "Invalid access key: must contain only digits"
    );
}

/// Strip XML declaration (`<?xml ... ?>`) from a string.
fn strip_xml_declaration(xml: &str) -> &str {
    if let Some(start) = xml.find("<?xml") {
        if let Some(end) = xml[start..].find("?>") {
            let after = &xml[start + end + 2..];
            return after.trim_start();
        }
    }
    xml
}

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

    // Synthetic 44-digit access key for tests (all zeros is fine for XML structure tests).
    const TEST_KEY: &str = "35240112345678000195550010000000011000000019";
    const TEST_CNPJ: &str = "12345678000195";
    const TEST_CPF: &str = "12345678901";

    // ── Fix #1: cOrgao = 91 for manifestacao ────────────────────────

    #[test]
    fn manifesta_request_uses_c_orgao_91() {
        let xml = build_manifesta_request(
            TEST_KEY,
            "210210",
            None,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(
            xml.contains("<cOrgao>91</cOrgao>"),
            "Manifestacao must use cOrgao=91 (Ambiente Nacional), got: {xml}"
        );
    }

    #[test]
    fn manifesta_request_confirmation_has_correct_desc() {
        let xml = build_manifesta_request(
            TEST_KEY,
            "210200",
            None,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(xml.contains("<descEvento>Confirmacao da Operacao</descEvento>"));
        assert!(xml.contains("<tpEvento>210200</tpEvento>"));
    }

    #[test]
    fn manifesta_request_operation_not_performed_includes_justification() {
        let xml = build_manifesta_request(
            TEST_KEY,
            "210240",
            Some("Motivo teste da operacao nao realizada"),
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(xml.contains("<xJust>Motivo teste da operacao nao realizada</xJust>"));
        assert!(xml.contains("<tpEvento>210240</tpEvento>"));
    }

    #[test]
    fn cancela_request_uses_c_orgao_from_access_key() {
        let xml = build_cancela_request(
            TEST_KEY,
            "135220000009921",
            "Erro na emissao da NF-e",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        // First 2 digits of TEST_KEY = "35" (SP)
        assert!(
            xml.contains("<cOrgao>35</cOrgao>"),
            "Cancellation must use cOrgao from access key (35), got: {xml}"
        );
    }

    // ── Fix #3: CPF in inutilizacao ─────────────────────────────────

    #[test]
    fn inutilizacao_with_cnpj_uses_cnpj_tag() {
        let xml = build_inutilizacao_request(
            24,
            TEST_CNPJ,
            "55",
            1,
            1,
            10,
            "Pulo de numeracao",
            SefazEnvironment::Homologation,
            "SP",
        );
        assert!(
            xml.contains(&format!("<CNPJ>{TEST_CNPJ}</CNPJ>")),
            "Should use <CNPJ> tag for 14-digit tax ID"
        );
        assert!(!xml.contains("<CPF>"), "Should not contain <CPF> tag");
    }

    #[test]
    fn inutilizacao_with_cpf_uses_cpf_tag() {
        let xml = build_inutilizacao_request(
            24,
            TEST_CPF,
            "55",
            1,
            1,
            10,
            "Pulo de numeracao",
            SefazEnvironment::Homologation,
            "MT",
        );
        assert!(
            xml.contains(&format!("<CPF>{TEST_CPF}</CPF>")),
            "Should use <CPF> tag for 11-digit tax ID"
        );
        assert!(!xml.contains("<CNPJ>"), "Should not contain <CNPJ> tag");
    }

    // ── Fix #4: CNPJ/CPF padding in inutilizacao ID ────────────────

    #[test]
    fn inutilizacao_id_pads_cnpj_to_14_digits() {
        let xml = build_inutilizacao_request(
            24,
            TEST_CNPJ,
            "55",
            1,
            1,
            10,
            "Pulo de numeracao",
            SefazEnvironment::Homologation,
            "SP",
        );
        // Expected ID: ID + cUF(35) + year(24) + padded_cnpj(14 digits) + model(55) + serie(001) + ini(000000001) + fin(000000010)
        // CNPJ "12345678000195" is already 14 digits, no padding needed
        let expected_id = format!("ID3524{TEST_CNPJ}55001000000001000000010");
        assert!(
            xml.contains(&format!("Id=\"{expected_id}\"")),
            "ID should contain padded CNPJ (14 digits), expected {expected_id}, got:\n{xml}"
        );
    }

    #[test]
    fn inutilizacao_id_pads_cpf_to_14_digits() {
        let xml = build_inutilizacao_request(
            24,
            TEST_CPF,
            "55",
            1,
            1,
            10,
            "Pulo de numeracao",
            SefazEnvironment::Homologation,
            "MT",
        );
        // CPF "12345678901" padded to 14 = "00012345678901"
        let padded = format!("{:0>14}", TEST_CPF);
        let expected_id = format!("ID5124{padded}55001000000001000000010");
        assert!(
            xml.contains(&format!("Id=\"{expected_id}\"")),
            "ID should pad CPF to 14 digits, expected {expected_id}, got:\n{xml}"
        );
    }

    // ── DistDFe request ─────────────────────────────────────────────

    #[test]
    fn dist_dfe_request_with_ult_nsu() {
        let xml =
            build_dist_dfe_request("SP", TEST_CNPJ, None, None, SefazEnvironment::Homologation);
        assert!(xml.contains("<distNSU><ultNSU>000000000000000</ultNSU></distNSU>"));
        assert!(xml.contains(&format!("<CNPJ>{TEST_CNPJ}</CNPJ>")));
        assert!(xml.contains("<cUFAutor>35</cUFAutor>"));
    }

    #[test]
    fn dist_dfe_request_with_access_key() {
        let xml = build_dist_dfe_request(
            "SP",
            TEST_CNPJ,
            None,
            Some(TEST_KEY),
            SefazEnvironment::Homologation,
        );
        assert!(xml.contains(&format!("<consChNFe><chNFe>{TEST_KEY}</chNFe></consChNFe>")));
    }

    #[test]
    fn dist_dfe_request_with_cpf() {
        let xml =
            build_dist_dfe_request("SP", TEST_CPF, None, None, SefazEnvironment::Homologation);
        assert!(xml.contains(&format!("<CPF>{TEST_CPF}</CPF>")));
    }

    // ── Cadastro request ────────────────────────────────────────────

    #[test]
    fn cadastro_request_with_cnpj() {
        let xml = build_cadastro_request("SP", "CNPJ", TEST_CNPJ);
        assert!(xml.contains(&format!("<CNPJ>{TEST_CNPJ}</CNPJ>")));
        assert!(xml.contains("<UF>SP</UF>"));
        assert!(xml.contains("<xServ>CONS-CAD</xServ>"));
    }

    #[test]
    fn cadastro_request_with_cpf() {
        let xml = build_cadastro_request("MT", "CPF", TEST_CPF);
        assert!(xml.contains(&format!("<CPF>{TEST_CPF}</CPF>")));
        assert!(xml.contains("<UF>MT</UF>"));
    }

    #[test]
    fn cadastro_request_with_ie() {
        let xml = build_cadastro_request("SP", "IE", "123456789");
        assert!(xml.contains("<IE>123456789</IE>"));
    }

    // ── tax_id_xml_tag helper ───────────────────────────────────────

    #[test]
    fn tax_id_xml_tag_detects_cpf_and_cnpj() {
        assert_eq!(tax_id_xml_tag("12345678901"), "<CPF>12345678901</CPF>");
        assert_eq!(
            tax_id_xml_tag("12345678000195"),
            "<CNPJ>12345678000195</CNPJ>"
        );
    }

    // ── Event ID format ─────────────────────────────────────────────

    #[test]
    fn event_id_format() {
        let id = build_event_id(210210, TEST_KEY, 1);
        assert_eq!(id, format!("ID210210{TEST_KEY}01"));
    }

    #[test]
    fn event_id_seq_padding() {
        let id = build_event_id(110111, TEST_KEY, 3);
        assert_eq!(id, format!("ID110111{TEST_KEY}03"));
    }

    // ── EPEC request ─────────────────────────────────────────────────

    /// Sample NF-e XML for EPEC extraction tests.
    fn sample_nfe_xml() -> String {
        format!(
            concat!(
                r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
                r#"<infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><CNPJ>98765432000188</CNPJ><UF>RJ</UF><IE>987654321</IE></dest>"#,
                r#"<total><ICMSTot><vNF>1500.00</vNF><vICMS>270.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infAdic/>"#,
                r#"<infRespTec><verProc>fiscal-rs 0.1.0</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        )
    }

    #[test]
    fn extract_epec_data_from_nfe_xml() {
        let xml = sample_nfe_xml();
        let data = extract_epec_data(&xml, None).unwrap();

        assert_eq!(data.access_key, TEST_KEY);
        assert_eq!(data.c_orgao_autor, "35");
        assert_eq!(data.ver_aplic, "fiscal-rs 0.1.0");
        assert_eq!(data.dh_emi, "2026-03-12T10:00:00-03:00");
        assert_eq!(data.tp_nf, "1");
        assert_eq!(data.emit_ie, "123456789");
        assert_eq!(data.dest_uf, "RJ");
        assert_eq!(data.dest_id_tag, "<CNPJ>98765432000188</CNPJ>");
        assert_eq!(data.dest_ie.as_deref(), Some("987654321"));
        assert_eq!(data.v_nf, "1500.00");
        assert_eq!(data.v_icms, "270.00");
        assert_eq!(data.v_st, "0.00");
        assert_eq!(data.tax_id, "12345678000199");
    }

    #[test]
    fn extract_epec_data_with_ver_aplic_override() {
        let xml = sample_nfe_xml();
        let data = extract_epec_data(&xml, Some("MyApp 2.0")).unwrap();
        assert_eq!(data.ver_aplic, "MyApp 2.0");
    }

    #[test]
    fn extract_epec_data_with_cpf_dest() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><CPF>12345678909</CPF><UF>SP</UF></dest>"#,
                r#"<total><ICMSTot><vNF>100.00</vNF><vICMS>18.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        );
        let data = extract_epec_data(&xml, None).unwrap();
        assert_eq!(data.dest_id_tag, "<CPF>12345678909</CPF>");
        assert_eq!(data.dest_ie, None);
    }

    #[test]
    fn extract_epec_data_with_id_estrangeiro() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><idEstrangeiro>ABC123</idEstrangeiro><UF>EX</UF></dest>"#,
                r#"<total><ICMSTot><vNF>500.00</vNF><vICMS>0.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        );
        let data = extract_epec_data(&xml, None).unwrap();
        assert_eq!(data.dest_id_tag, "<idEstrangeiro>ABC123</idEstrangeiro>");
    }

    #[test]
    fn extract_epec_data_rejects_missing_inf_nfe() {
        let xml = "<NFe><ide/></NFe>";
        let err = extract_epec_data(xml, None).unwrap_err();
        assert!(matches!(err, fiscal_core::FiscalError::XmlParsing(_)));
    }

    #[test]
    fn build_epec_request_structure() {
        let xml = sample_nfe_xml();
        let data = extract_epec_data(&xml, None).unwrap();
        let request = build_epec_request(&data, SefazEnvironment::Homologation);

        // Event type
        assert!(
            request.contains("<tpEvento>110140</tpEvento>"),
            "EPEC must have tpEvento=110140"
        );
        // Description
        assert!(
            request.contains("<descEvento>EPEC</descEvento>"),
            "EPEC must have descEvento=EPEC"
        );
        // cOrgao in envelope should be 91 (AN)
        assert!(
            request.contains("<cOrgao>91</cOrgao>"),
            "EPEC envelope must use cOrgao=91 (AN)"
        );
        // cOrgaoAutor in detEvento should be the issuer's UF code
        assert!(
            request.contains("<cOrgaoAutor>35</cOrgaoAutor>"),
            "EPEC detEvento must contain cOrgaoAutor from issuer UF"
        );
        // tpAutor=1
        assert!(request.contains("<tpAutor>1</tpAutor>"));
        // verAplic
        assert!(request.contains("<verAplic>fiscal-rs 0.1.0</verAplic>"));
        // dhEmi
        assert!(request.contains("<dhEmi>2026-03-12T10:00:00-03:00</dhEmi>"));
        // tpNF
        assert!(request.contains("<tpNF>1</tpNF>"));
        // Issuer IE
        assert!(request.contains("<IE>123456789</IE>"));
        // Dest section
        assert!(request.contains("<dest>"));
        assert!(request.contains("<UF>RJ</UF>"));
        assert!(request.contains("<CNPJ>98765432000188</CNPJ>"));
        assert!(
            request.contains("<IE>987654321</IE>"),
            "Dest IE should be present"
        );
        assert!(request.contains("<vNF>1500.00</vNF>"));
        assert!(request.contains("<vICMS>270.00</vICMS>"));
        assert!(request.contains("<vST>0.00</vST>"));
        assert!(request.contains("</dest>"));
        // nSeqEvento=1 always
        assert!(request.contains("<nSeqEvento>1</nSeqEvento>"));
        // envEvento wrapper
        assert!(request.contains("<envEvento"));
        assert!(request.contains("</envEvento>"));
        // Access key
        assert!(request.contains(&format!("<chNFe>{TEST_KEY}</chNFe>")));
        // Event ID
        assert!(request.contains(&format!("Id=\"ID110140{TEST_KEY}01\"")));
        // tpAmb=2 (homologation)
        assert!(request.contains("<tpAmb>2</tpAmb>"));
        // Issuer tax ID in envelope
        assert!(request.contains("<CNPJ>12345678000199</CNPJ>"));
    }

    #[test]
    fn build_epec_request_without_dest_ie() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>0</tpNF><dhEmi>2026-01-01T08:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>111222333</IE></emit>"#,
                r#"<dest><CPF>12345678909</CPF><UF>MG</UF></dest>"#,
                r#"<total><ICMSTot><vNF>200.00</vNF><vICMS>36.00</vICMS><vST>5.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        );
        let data = extract_epec_data(&xml, None).unwrap();
        let request = build_epec_request(&data, SefazEnvironment::Production);

        // Should NOT contain dest IE (CPF dest, no IE)
        // The dest section should have CPF but no IE
        assert!(request.contains("<CPF>12345678909</CPF>"));
        assert!(request.contains("<tpAmb>1</tpAmb>")); // Production
        assert!(request.contains("<tpNF>0</tpNF>")); // Entrada
        assert!(request.contains("<vST>5.00</vST>"));
    }

    // ── Cancelamento por substituição (110112) ────────────────────

    #[test]
    fn cancel_substituicao_request_structure() {
        let ref_key = "35240112345678000195650010000000021000000028";
        let xml = build_cancel_substituicao_request(
            TEST_KEY,
            ref_key,
            "135220000009921",
            "Erro na emissao da NFCe",
            "FISCAL-RS-1.0",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpEvento>110112</tpEvento>"));
        assert!(xml.contains("<descEvento>Cancelamento por substituicao</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<nProt>135220000009921</nProt>"));
        assert!(xml.contains("<xJust>Erro na emissao da NFCe</xJust>"));
        assert!(xml.contains(&format!("<chNFeRef>{ref_key}</chNFeRef>")));
        assert!(xml.contains("<cOrgao>35</cOrgao>"));
    }

    #[test]
    fn cancel_substituicao_event_id_format() {
        let xml = build_cancel_substituicao_request(
            TEST_KEY,
            "35240112345678000195650010000000021000000028",
            "135220000009921",
            "Justificativa teste",
            "APP-1.0",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        let expected_id = format!("ID110112{TEST_KEY}01");
        assert!(xml.contains(&format!("Id=\"{expected_id}\"")));
    }

    // ── Ator interessado (110150) ─────────────────────────────────

    #[test]
    fn ator_interessado_request_with_cnpj() {
        let xml = build_ator_interessado_request(
            TEST_KEY,
            1,
            "FISCAL-RS-1.0",
            Some("12345678000199"),
            None,
            0,
            "SP",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(xml.contains("<tpEvento>110150</tpEvento>"));
        assert!(xml.contains("<descEvento>Ator interessado na NF-e</descEvento>"));
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<autXML><CNPJ>12345678000199</CNPJ></autXML>"));
        assert!(xml.contains("<tpAutorizacao>0</tpAutorizacao>"));
        assert!(!xml.contains("<xCondUso>"));
    }

    #[test]
    fn ator_interessado_request_with_cpf_and_cond_uso() {
        let xml = build_ator_interessado_request(
            TEST_KEY,
            2,
            "APP-2.0",
            None,
            Some("12345678901"),
            1,
            "SP",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(xml.contains("<autXML><CPF>12345678901</CPF></autXML>"));
        assert!(xml.contains("<tpAutorizacao>1</tpAutorizacao>"));
        assert!(xml.contains("<xCondUso>"));
        assert!(xml.contains("transportador declarado no campo CNPJ/CPF"));
    }

    // ── Comprovante de entrega (110130) ───────────────────────────

    #[test]
    fn comprovante_entrega_request_structure() {
        let xml = build_comprovante_entrega_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            "2024-01-15T10:30:00-03:00",
            "12345678901",
            "Joao da Silva",
            Some("-23.5505"),
            Some("-46.6333"),
            "abc123hashbase64==",
            "2024-01-15T10:31:00-03:00",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpEvento>110130</tpEvento>"));
        assert!(xml.contains("<descEvento>Comprovante de Entrega da NF-e</descEvento>"));
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<dhEntrega>2024-01-15T10:30:00-03:00</dhEntrega>"));
        assert!(xml.contains("<nDoc>12345678901</nDoc>"));
        assert!(xml.contains("<xNome>Joao da Silva</xNome>"));
        assert!(xml.contains("<latGPS>-23.5505</latGPS>"));
        assert!(xml.contains("<longGPS>-46.6333</longGPS>"));
        assert!(xml.contains("<hashComprovante>abc123hashbase64==</hashComprovante>"));
        assert!(xml.contains("<dhHashComprovante>2024-01-15T10:31:00-03:00</dhHashComprovante>"));
    }

    #[test]
    fn comprovante_entrega_without_gps() {
        let xml = build_comprovante_entrega_request(
            TEST_KEY,
            "APP-1.0",
            "2024-01-15T10:30:00-03:00",
            "12345678901",
            "Maria Santos",
            None,
            None,
            "hashvalue==",
            "2024-01-15T10:31:00-03:00",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(!xml.contains("<latGPS>"));
        assert!(!xml.contains("<longGPS>"));
        assert!(xml.contains("<hashComprovante>hashvalue==</hashComprovante>"));
    }

    // ── Cancelamento comprovante de entrega (110131) ──────────────

    #[test]
    fn cancel_comprovante_entrega_request_structure() {
        let xml = build_cancel_comprovante_entrega_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            "135220000009999",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpEvento>110131</tpEvento>"));
        assert!(
            xml.contains("<descEvento>Cancelamento Comprovante de Entrega da NF-e</descEvento>")
        );
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<nProtEvento>135220000009999</nProtEvento>"));
    }

    // ── Insucesso na entrega (110192) ─────────────────────────────

    #[test]
    fn insucesso_entrega_request_structure() {
        let xml = build_insucesso_entrega_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            "2024-01-15T14:00:00-03:00",
            Some(3),
            1,
            None,
            Some("-23.5505"),
            Some("-46.6333"),
            "hashinsucesso==",
            "2024-01-15T14:01:00-03:00",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpEvento>110192</tpEvento>"));
        assert!(xml.contains("<descEvento>Insucesso na Entrega da NF-e</descEvento>"));
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<dhTentativaEntrega>2024-01-15T14:00:00-03:00</dhTentativaEntrega>"));
        assert!(xml.contains("<nTentativa>3</nTentativa>"));
        assert!(xml.contains("<tpMotivo>1</tpMotivo>"));
        assert!(!xml.contains("<xJustMotivo>"));
        assert!(xml.contains("<latGPS>-23.5505</latGPS>"));
        assert!(xml.contains("<longGPS>-46.6333</longGPS>"));
        assert!(xml.contains("<hashTentativaEntrega>hashinsucesso==</hashTentativaEntrega>"));
        assert!(xml.contains(
            "<dhHashTentativaEntrega>2024-01-15T14:01:00-03:00</dhHashTentativaEntrega>"
        ));
    }

    #[test]
    fn insucesso_entrega_with_reason_type_4_includes_justification() {
        let xml = build_insucesso_entrega_request(
            TEST_KEY,
            "APP-1.0",
            "2024-01-15T14:00:00-03:00",
            None,
            4,
            Some("Destinatario mudou de endereco"),
            None,
            None,
            "hashval==",
            "2024-01-15T14:01:00-03:00",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpMotivo>4</tpMotivo>"));
        assert!(xml.contains("<xJustMotivo>Destinatario mudou de endereco</xJustMotivo>"));
        assert!(!xml.contains("<nTentativa>"));
        assert!(!xml.contains("<latGPS>"));
    }

    // ── Cancelamento insucesso entrega (110193) ───────────────────

    #[test]
    fn cancel_insucesso_entrega_request_structure() {
        let xml = build_cancel_insucesso_entrega_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            "135220000009888",
            "SP",
            1,
            SefazEnvironment::Homologation,
            "12345678000199",
        );
        assert!(xml.contains("<tpEvento>110193</tpEvento>"));
        assert!(xml.contains("<descEvento>Cancelamento Insucesso na Entrega da NF-e</descEvento>"));
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<nProtEvento>135220000009888</nProtEvento>"));
    }

    // ── Prorrogação ICMS (111500/111501) ────────────────────────────

    #[test]
    fn prorrogacao_first_term_request_structure() {
        let items = vec![
            ProrrogacaoItem {
                num_item: 1,
                qtde: 10.0,
            },
            ProrrogacaoItem {
                num_item: 2,
                qtde: 5.5,
            },
        ];
        let xml = build_prorrogacao_request(
            TEST_KEY,
            "135220000009921",
            &items,
            false,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(
            xml.contains("<tpEvento>111500</tpEvento>"),
            "First-term prorrogacao must use tpEvento=111500"
        );
        assert!(xml.contains("<descEvento>Pedido de Prorrogacao</descEvento>"));
        assert!(xml.contains("<nProt>135220000009921</nProt>"));
        assert!(xml.contains("<itemPedido numItem=\"1\"><qtdeItem>10</qtdeItem></itemPedido>"));
        assert!(xml.contains("<itemPedido numItem=\"2\"><qtdeItem>5.5</qtdeItem></itemPedido>"));
        assert!(xml.contains("<cOrgao>35</cOrgao>"));
        assert!(xml.contains(&format!("<chNFe>{TEST_KEY}</chNFe>")));
        let expected_id = format!("ID111500{TEST_KEY}01");
        assert!(xml.contains(&format!("Id=\"{expected_id}\"")));
    }

    #[test]
    fn prorrogacao_second_term_request_structure() {
        let items = vec![ProrrogacaoItem {
            num_item: 1,
            qtde: 3.0,
        }];
        let xml = build_prorrogacao_request(
            TEST_KEY,
            "135220000009921",
            &items,
            true,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(
            xml.contains("<tpEvento>111501</tpEvento>"),
            "Second-term prorrogacao must use tpEvento=111501"
        );
        assert!(xml.contains("<descEvento>Pedido de Prorrogacao</descEvento>"));
    }

    // ── Cancelamento de prorrogação ICMS (111502/111503) ────────────

    #[test]
    fn cancel_prorrogacao_first_term_request_structure() {
        let xml = build_cancel_prorrogacao_request(
            TEST_KEY,
            "135220000009921",
            false,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(
            xml.contains("<tpEvento>111502</tpEvento>"),
            "First-term cancel prorrogacao must use tpEvento=111502"
        );
        assert!(xml.contains("<descEvento>Cancelamento de Pedido de Prorrogacao</descEvento>"));
        let expected_id_cancelado = format!("ID111500{TEST_KEY}01");
        assert!(
            xml.contains(&format!(
                "<idPedidoCancelado>{expected_id_cancelado}</idPedidoCancelado>"
            )),
            "Must contain idPedidoCancelado referencing the original prorrogacao event"
        );
        assert!(xml.contains("<nProt>135220000009921</nProt>"));
        assert!(xml.contains("<cOrgao>35</cOrgao>"));
    }

    #[test]
    fn cancel_prorrogacao_second_term_request_structure() {
        let xml = build_cancel_prorrogacao_request(
            TEST_KEY,
            "135220000009921",
            true,
            2,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        assert!(
            xml.contains("<tpEvento>111503</tpEvento>"),
            "Second-term cancel prorrogacao must use tpEvento=111503"
        );
        assert!(xml.contains("<descEvento>Cancelamento de Pedido de Prorrogacao</descEvento>"));
        // second term: orig_event = 111501
        let expected_id_cancelado = format!("ID111501{TEST_KEY}02");
        assert!(
            xml.contains(&format!(
                "<idPedidoCancelado>{expected_id_cancelado}</idPedidoCancelado>"
            )),
            "Must contain idPedidoCancelado referencing the 2nd-term prorrogacao event"
        );
    }

    // ── CSC request (admCscNFCe) ───────────────────────────────────

    #[test]
    fn csc_request_query_structure() {
        let xml = build_csc_request(
            SefazEnvironment::Homologation,
            1,
            "12345678000195",
            None,
            None,
        );
        assert!(xml.contains("<admCscNFCe versao=\"1.00\""));
        assert!(xml.contains(&format!("xmlns=\"{NFE_NAMESPACE}\"")));
        assert!(xml.contains("<tpAmb>2</tpAmb>"));
        assert!(xml.contains("<indOp>1</indOp>"));
        assert!(xml.contains("<raizCNPJ>12345678</raizCNPJ>"));
        assert!(!xml.contains("<dadosCsc>"));
    }

    #[test]
    fn csc_request_new_csc_structure() {
        let xml = build_csc_request(
            SefazEnvironment::Production,
            2,
            "12345678000195",
            None,
            None,
        );
        assert!(xml.contains("<tpAmb>1</tpAmb>"));
        assert!(xml.contains("<indOp>2</indOp>"));
        assert!(xml.contains("<raizCNPJ>12345678</raizCNPJ>"));
        assert!(!xml.contains("<dadosCsc>"));
    }

    #[test]
    fn csc_request_revoke_includes_dados_csc() {
        let xml = build_csc_request(
            SefazEnvironment::Homologation,
            3,
            "12345678000195",
            Some("000001"),
            Some("ABC123DEF456"),
        );
        assert!(xml.contains("<indOp>3</indOp>"));
        assert!(xml.contains("<dadosCsc>"));
        assert!(xml.contains("<idCsc>000001</idCsc>"));
        assert!(xml.contains("<codigoCsc>ABC123DEF456</codigoCsc>"));
        assert!(xml.contains("</dadosCsc>"));
    }

    #[test]
    fn csc_request_raiz_cnpj_is_first_8_digits() {
        let xml = build_csc_request(
            SefazEnvironment::Homologation,
            1,
            "98765432000188",
            None,
            None,
        );
        assert!(xml.contains("<raizCNPJ>98765432</raizCNPJ>"));
    }

    // ── Event batch request ─────────────────────────────────────────

    #[test]
    fn event_batch_request_structure() {
        let events = vec![
            EventItem {
                access_key: TEST_KEY.to_string(),
                event_type: event_types::CONFIRMATION,
                seq: 1,
                tax_id: TEST_CNPJ.to_string(),
                additional_tags: String::new(),
            },
            EventItem {
                access_key: TEST_KEY.to_string(),
                event_type: event_types::AWARENESS,
                seq: 1,
                tax_id: TEST_CNPJ.to_string(),
                additional_tags: String::new(),
            },
        ];
        let xml = build_event_batch_request(
            "AN",
            &events,
            Some("202401151030001"),
            SefazEnvironment::Homologation,
        );

        assert!(xml.contains("<envEvento"));
        assert!(xml.contains("<idLote>202401151030001</idLote>"));
        // Two <evento> elements
        let evento_count = xml.matches("<evento ").count();
        assert_eq!(
            evento_count, 2,
            "Should have 2 <evento> elements, got {evento_count}"
        );
        // Both event types present
        assert!(xml.contains("<tpEvento>210200</tpEvento>"));
        assert!(xml.contains("<tpEvento>210210</tpEvento>"));
        // cOrgao=91 for AN
        assert!(xml.contains("<cOrgao>91</cOrgao>"));
        // Descriptions
        assert!(xml.contains("<descEvento>Confirmacao da Operacao</descEvento>"));
        assert!(xml.contains("<descEvento>Ciencia da Operacao</descEvento>"));
    }

    #[test]
    fn event_batch_skips_epec() {
        let events = vec![
            EventItem {
                access_key: TEST_KEY.to_string(),
                event_type: event_types::EPEC,
                seq: 1,
                tax_id: TEST_CNPJ.to_string(),
                additional_tags: String::new(),
            },
            EventItem {
                access_key: TEST_KEY.to_string(),
                event_type: event_types::AWARENESS,
                seq: 1,
                tax_id: TEST_CNPJ.to_string(),
                additional_tags: String::new(),
            },
        ];
        let xml =
            build_event_batch_request("AN", &events, Some("123"), SefazEnvironment::Homologation);

        // EPEC should be skipped
        assert!(!xml.contains("<tpEvento>110140</tpEvento>"));
        // Ciencia should be present
        assert!(xml.contains("<tpEvento>210210</tpEvento>"));
        // Only one <evento> element
        let evento_count = xml.matches("<evento ").count();
        assert_eq!(
            evento_count, 1,
            "EPEC should be skipped, got {evento_count} events"
        );
    }

    #[test]
    fn event_batch_with_additional_tags() {
        let events = vec![EventItem {
            access_key: TEST_KEY.to_string(),
            event_type: event_types::OPERATION_NOT_PERFORMED,
            seq: 1,
            tax_id: TEST_CNPJ.to_string(),
            additional_tags: "<xJust>Motivo teste</xJust>".to_string(),
        }];
        let xml =
            build_event_batch_request("AN", &events, Some("456"), SefazEnvironment::Homologation);
        assert!(xml.contains("<xJust>Motivo teste</xJust>"));
        assert!(xml.contains("<tpEvento>210240</tpEvento>"));
        assert!(xml.contains("<descEvento>Operacao nao Realizada</descEvento>"));
    }

    #[test]
    #[should_panic(expected = "limited to 20")]
    fn event_batch_rejects_more_than_20() {
        let events: Vec<EventItem> = (0..21)
            .map(|i| EventItem {
                access_key: TEST_KEY.to_string(),
                event_type: event_types::AWARENESS,
                seq: i + 1,
                tax_id: TEST_CNPJ.to_string(),
                additional_tags: String::new(),
            })
            .collect();
        let _ = build_event_batch_request("AN", &events, None, SefazEnvironment::Homologation);
    }

    #[test]
    fn event_batch_with_cpf_tax_id() {
        let events = vec![EventItem {
            access_key: TEST_KEY.to_string(),
            event_type: event_types::AWARENESS,
            seq: 1,
            tax_id: TEST_CPF.to_string(),
            additional_tags: String::new(),
        }];
        let xml =
            build_event_batch_request("AN", &events, Some("789"), SefazEnvironment::Homologation);
        assert!(xml.contains(&format!("<CPF>{TEST_CPF}</CPF>")));
    }

    #[test]
    fn event_batch_event_id_format() {
        let events = vec![EventItem {
            access_key: TEST_KEY.to_string(),
            event_type: event_types::CONFIRMATION,
            seq: 3,
            tax_id: TEST_CNPJ.to_string(),
            additional_tags: String::new(),
        }];
        let xml =
            build_event_batch_request("SP", &events, Some("111"), SefazEnvironment::Homologation);
        let expected_id = format!("ID210200{TEST_KEY}03");
        assert!(
            xml.contains(&format!("Id=\"{expected_id}\"")),
            "Event ID must follow ID{{tpEvento}}{{chNFe}}{{nSeqEvento:02}} format"
        );
        // cOrgao=35 for SP
        assert!(xml.contains("<cOrgao>35</cOrgao>"));
    }

    // ── Conciliação financeira (110750/110751) ────────────────────────

    #[test]
    fn conciliacao_request_structure() {
        let det_pag = vec![ConciliacaoDetPag {
            ind_pag: Some("0".to_string()),
            t_pag: "01".to_string(),
            x_pag: Some("Dinheiro".to_string()),
            v_pag: "150.00".to_string(),
            d_pag: "2024-06-15".to_string(),
            cnpj_pag: None,
            uf_pag: None,
            cnpj_if: None,
            t_band: None,
            c_aut: None,
            cnpj_receb: None,
            uf_receb: None,
        }];
        let xml = build_conciliacao_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            &det_pag,
            false,
            None,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            None,
        );

        assert!(xml.contains("<tpEvento>110750</tpEvento>"));
        assert!(xml.contains("<descEvento>ECONF</descEvento>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<detPag>"));
        assert!(xml.contains("<indPag>0</indPag>"));
        assert!(xml.contains("<tPag>01</tPag>"));
        assert!(xml.contains("<xPag>Dinheiro</xPag>"));
        assert!(xml.contains("<vPag>150.00</vPag>"));
        assert!(xml.contains("<dPag>2024-06-15</dPag>"));
        assert!(xml.contains("</detPag>"));
        assert!(xml.contains("<cOrgao>35</cOrgao>"));
    }

    #[test]
    fn conciliacao_request_with_card_payment() {
        let det_pag = vec![ConciliacaoDetPag {
            ind_pag: Some("0".to_string()),
            t_pag: "03".to_string(),
            x_pag: None,
            v_pag: "250.50".to_string(),
            d_pag: "2024-06-15".to_string(),
            cnpj_pag: Some("11222333000144".to_string()),
            uf_pag: Some("SP".to_string()),
            cnpj_if: Some("99888777000166".to_string()),
            t_band: Some("01".to_string()),
            c_aut: Some("AUTH123".to_string()),
            cnpj_receb: Some("55444333000122".to_string()),
            uf_receb: Some("RJ".to_string()),
        }];
        let xml = build_conciliacao_request(
            TEST_KEY,
            "APP-2.0",
            &det_pag,
            false,
            None,
            1,
            SefazEnvironment::Production,
            TEST_CNPJ,
            None,
        );

        assert!(xml.contains("<tPag>03</tPag>"));
        assert!(xml.contains("<CNPJPag>11222333000144</CNPJPag>"));
        assert!(xml.contains("<UFPag>SP</UFPag>"));
        assert!(xml.contains("<CNPJIF>99888777000166</CNPJIF>"));
        assert!(xml.contains("<tBand>01</tBand>"));
        assert!(xml.contains("<cAut>AUTH123</cAut>"));
        assert!(xml.contains("<CNPJReceb>55444333000122</CNPJReceb>"));
        assert!(xml.contains("<UFReceb>RJ</UFReceb>"));
        assert!(xml.contains("<tpAmb>1</tpAmb>"));
    }

    #[test]
    fn conciliacao_cancel_request_structure() {
        let xml = build_conciliacao_request(
            TEST_KEY,
            "FISCAL-RS-1.0",
            &[],
            true,
            Some("135220000009999"),
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            None,
        );

        assert!(xml.contains("<tpEvento>110751</tpEvento>"));
        assert!(xml.contains("<descEvento>Cancelamento Conciliação Financeira</descEvento>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<nProtEvento>135220000009999</nProtEvento>"));
        assert!(!xml.contains("<detPag>"));
    }

    #[test]
    fn conciliacao_multiple_payments() {
        let det_pag = vec![
            ConciliacaoDetPag {
                ind_pag: Some("0".to_string()),
                t_pag: "01".to_string(),
                x_pag: None,
                v_pag: "100.00".to_string(),
                d_pag: "2024-06-15".to_string(),
                cnpj_pag: None,
                uf_pag: None,
                cnpj_if: None,
                t_band: None,
                c_aut: None,
                cnpj_receb: None,
                uf_receb: None,
            },
            ConciliacaoDetPag {
                ind_pag: Some("1".to_string()),
                t_pag: "03".to_string(),
                x_pag: None,
                v_pag: "200.00".to_string(),
                d_pag: "2024-07-15".to_string(),
                cnpj_pag: None,
                uf_pag: None,
                cnpj_if: None,
                t_band: None,
                c_aut: None,
                cnpj_receb: None,
                uf_receb: None,
            },
        ];
        let xml = build_conciliacao_request(
            TEST_KEY,
            "APP-1.0",
            &det_pag,
            false,
            None,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            None,
        );

        let det_pag_count = xml.matches("<detPag>").count();
        assert_eq!(det_pag_count, 2, "Should have 2 <detPag> elements");
        assert!(xml.contains("<vPag>100.00</vPag>"));
        assert!(xml.contains("<vPag>200.00</vPag>"));
        assert!(xml.contains("<dPag>2024-06-15</dPag>"));
        assert!(xml.contains("<dPag>2024-07-15</dPag>"));
    }

    // ── Event type constants ────────────────────────────────────────

    #[test]
    fn conciliacao_event_type_values() {
        assert_eq!(event_types::CONCILIACAO, 110750);
        assert_eq!(event_types::CANCEL_CONCILIACAO, 110751);
    }

    // ── Download (dist_dfe by access key) ───────────────────────────

    #[test]
    fn download_uses_cons_ch_nfe() {
        // sefazDownload builds a distDFeInt with consChNFe
        let xml = build_dist_dfe_request(
            "SP",
            TEST_CNPJ,
            None,
            Some(TEST_KEY),
            SefazEnvironment::Homologation,
        );
        assert!(xml.contains(&format!("<consChNFe><chNFe>{TEST_KEY}</chNFe></consChNFe>")));
        assert!(xml.contains("<tpAmb>2</tpAmb>"));
        assert!(xml.contains("<cUFAutor>35</cUFAutor>"));
        assert!(xml.contains(&format!("<CNPJ>{TEST_CNPJ}</CNPJ>")));
    }

    // ── EPEC NFC-e status (SP only) ─────────────────────────────────

    #[test]
    fn epec_nfce_status_request_structure() {
        let xml = build_epec_nfce_status_request("SP", SefazEnvironment::Homologation);
        assert!(xml.contains("<consStatServ"));
        assert!(xml.contains(&format!("xmlns=\"{NFE_NAMESPACE}\"")));
        assert!(xml.contains(&format!("versao=\"{NFE_VERSION}\"")));
        assert!(xml.contains("<tpAmb>2</tpAmb>"));
        assert!(xml.contains("<cUF>35</cUF>"));
        assert!(xml.contains("<xServ>STATUS</xServ>"));
        assert!(xml.contains("</consStatServ>"));
    }

    #[test]
    fn epec_nfce_status_request_production() {
        let xml = build_epec_nfce_status_request("SP", SefazEnvironment::Production);
        assert!(xml.contains("<tpAmb>1</tpAmb>"));
    }

    // ── EPEC NFC-e data extraction ──────────────────────────────────

    /// Sample NFC-e XML for EPEC NFC-e extraction tests (with dest).
    fn sample_nfce_xml_with_dest() -> String {
        format!(
            concat!(
                r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
                r#"<infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><CPF>12345678909</CPF><UF>SP</UF></dest>"#,
                r#"<total><ICMSTot><vNF>150.00</vNF><vICMS>27.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>fiscal-rs 0.1.0</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        )
    }

    /// Sample NFC-e XML without dest (consumer sale).
    fn sample_nfce_xml_without_dest() -> String {
        format!(
            concat!(
                r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
                r#"<infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T14:30:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<total><ICMSTot><vNF>50.00</vNF><vICMS>9.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>fiscal-rs 0.1.0</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY
        )
    }

    #[test]
    fn extract_epec_nfce_data_with_dest() {
        let xml = sample_nfce_xml_with_dest();
        let data = extract_epec_nfce_data(&xml, None).unwrap();

        assert_eq!(data.access_key, TEST_KEY);
        assert_eq!(data.c_orgao_autor, "35");
        assert_eq!(data.ver_aplic, "fiscal-rs 0.1.0");
        assert_eq!(data.dh_emi, "2026-03-12T10:00:00-03:00");
        assert_eq!(data.tp_nf, "1");
        assert_eq!(data.emit_ie, "123456789");
        assert_eq!(data.dest_uf.as_deref(), Some("SP"));
        assert_eq!(data.dest_id_tag.as_deref(), Some("<CPF>12345678909</CPF>"));
        assert_eq!(data.dest_ie, None);
        assert_eq!(data.v_nf, "150.00");
        assert_eq!(data.v_icms, "27.00");
        assert_eq!(data.tax_id, "12345678000199");
    }

    #[test]
    fn extract_epec_nfce_data_without_dest() {
        let xml = sample_nfce_xml_without_dest();
        let data = extract_epec_nfce_data(&xml, None).unwrap();

        assert_eq!(data.access_key, TEST_KEY);
        assert_eq!(data.dest_uf, None);
        assert_eq!(data.dest_id_tag, None);
        assert_eq!(data.dest_ie, None);
        assert_eq!(data.v_nf, "50.00");
        assert_eq!(data.v_icms, "9.00");
    }

    #[test]
    fn extract_epec_nfce_data_with_ver_aplic_override() {
        let xml = sample_nfce_xml_with_dest();
        let data = extract_epec_nfce_data(&xml, Some("MyPOS 3.0")).unwrap();
        assert_eq!(data.ver_aplic, "MyPOS 3.0");
    }

    #[test]
    fn extract_epec_nfce_data_rejects_missing_inf_nfe() {
        let xml = "<NFe><ide/></NFe>";
        let err = extract_epec_nfce_data(xml, None).unwrap_err();
        assert!(matches!(err, fiscal_core::FiscalError::XmlParsing(_)));
    }

    // ── EPEC NFC-e request building ─────────────────────────────────

    #[test]
    fn build_epec_nfce_request_with_dest() {
        let xml = sample_nfce_xml_with_dest();
        let data = extract_epec_nfce_data(&xml, None).unwrap();
        let request = build_epec_nfce_request(&data, SefazEnvironment::Homologation);

        // Event type
        assert!(
            request.contains("<tpEvento>110140</tpEvento>"),
            "EPEC NFC-e must have tpEvento=110140"
        );
        // Description
        assert!(
            request.contains("<descEvento>EPEC</descEvento>"),
            "EPEC NFC-e must have descEvento=EPEC"
        );
        // cOrgao in envelope should be the state code (35 for SP), NOT 91
        assert!(
            request.contains("<cOrgao>35</cOrgao>"),
            "EPEC NFC-e envelope must use cOrgao=35 (state), not 91 (AN)"
        );
        // cOrgaoAutor in detEvento
        assert!(request.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        // tpAutor=1
        assert!(request.contains("<tpAutor>1</tpAutor>"));
        // verAplic
        assert!(request.contains("<verAplic>fiscal-rs 0.1.0</verAplic>"));
        // dhEmi
        assert!(request.contains("<dhEmi>2026-03-12T10:00:00-03:00</dhEmi>"));
        // tpNF
        assert!(request.contains("<tpNF>1</tpNF>"));
        // Issuer IE
        assert!(request.contains("<IE>123456789</IE>"));
        // Dest section with CPF
        assert!(request.contains("<dest>"));
        assert!(request.contains("<UF>SP</UF>"));
        assert!(request.contains("<CPF>12345678909</CPF>"));
        assert!(request.contains("</dest>"));
        // vNF and vICMS (no vST for NFC-e EPEC)
        assert!(request.contains("<vNF>150.00</vNF>"));
        assert!(request.contains("<vICMS>27.00</vICMS>"));
        assert!(
            !request.contains("<vST>"),
            "EPEC NFC-e must NOT contain <vST>"
        );
        // nSeqEvento=1
        assert!(request.contains("<nSeqEvento>1</nSeqEvento>"));
        // envEvento wrapper
        assert!(request.contains("<envEvento"));
        assert!(request.contains("</envEvento>"));
        // Access key
        assert!(request.contains(&format!("<chNFe>{TEST_KEY}</chNFe>")));
        // Event ID
        assert!(request.contains(&format!("Id=\"ID110140{TEST_KEY}01\"")));
        // tpAmb=2 (homologation)
        assert!(request.contains("<tpAmb>2</tpAmb>"));
        // Issuer tax ID in envelope
        assert!(request.contains("<CNPJ>12345678000199</CNPJ>"));
    }

    #[test]
    fn build_epec_nfce_request_without_dest() {
        let xml = sample_nfce_xml_without_dest();
        let data = extract_epec_nfce_data(&xml, None).unwrap();
        let request = build_epec_nfce_request(&data, SefazEnvironment::Production);

        // Event type
        assert!(request.contains("<tpEvento>110140</tpEvento>"));
        // Production
        assert!(request.contains("<tpAmb>1</tpAmb>"));
        // Should NOT contain <dest> section
        assert!(
            !request.contains("<dest>"),
            "EPEC NFC-e without recipient must NOT contain <dest>"
        );
        // Should still have vNF and vICMS
        assert!(request.contains("<vNF>50.00</vNF>"));
        assert!(request.contains("<vICMS>9.00</vICMS>"));
        // cOrgao = state code, not 91
        assert!(request.contains("<cOrgao>35</cOrgao>"));
    }

    #[test]
    fn build_epec_nfce_request_uses_state_c_orgao_not_an() {
        let xml = sample_nfce_xml_with_dest();
        let data = extract_epec_nfce_data(&xml, None).unwrap();
        let request = build_epec_nfce_request(&data, SefazEnvironment::Homologation);

        // The key difference from regular EPEC: cOrgao = state code (35), not 91
        assert!(
            !request.contains("<cOrgao>91</cOrgao>"),
            "EPEC NFC-e must NOT use cOrgao=91 (that is for standard EPEC to AN)"
        );
        assert!(
            request.contains("<cOrgao>35</cOrgao>"),
            "EPEC NFC-e must use cOrgao from the issuer's state"
        );
    }

    // ── RTC event_description coverage (lines 102-117) ──────────────

    #[test]
    fn event_description_rtc_events() {
        assert_eq!(
            event_description(event_types::RTC_CANCELA_EVENTO),
            "Cancelamento de Evento"
        );
        assert_eq!(
            event_description(event_types::RTC_INFO_PAGTO_INTEGRAL),
            "Informação de efetivo pagamento integral para liberar crédito presumido do adquirente"
        );
        assert_eq!(
            event_description(event_types::RTC_IMPORTACAO_ZFM),
            "Importação em ALC/ZFM não convertida em isenção"
        );
        assert_eq!(
            event_description(event_types::RTC_ROUBO_PERDA_FORNECEDOR),
            "Perecimento, perda, roubo ou furto durante o transporte contratado pelo fornecedor"
        );
        assert_eq!(
            event_description(event_types::RTC_FORNECIMENTO_NAO_REALIZADO),
            "Fornecimento não realizado com pagamento antecipado"
        );
        assert_eq!(
            event_description(event_types::RTC_ATUALIZACAO_DATA_ENTREGA),
            "Atualização da Data de Previsão de Entrega"
        );
        assert_eq!(
            event_description(event_types::RTC_SOL_APROP_CRED_PRESUMIDO),
            "Solicitação de Apropriação de crédito presumido"
        );
        assert_eq!(
            event_description(event_types::RTC_DESTINO_CONSUMO_PESSOAL),
            "Destinação de item para consumo pessoal"
        );
        assert_eq!(
            event_description(event_types::RTC_ROUBO_PERDA_ADQUIRENTE),
            "Perecimento, perda, roubo ou furto durante o transporte contratado pelo adquirente"
        );
        assert_eq!(
            event_description(event_types::RTC_ACEITE_DEBITO),
            "Aceite de débito na apuração por emissão de nota de crédito"
        );
        assert_eq!(
            event_description(event_types::RTC_IMOBILIZACAO_ITEM),
            "Imobilização de Item"
        );
        assert_eq!(
            event_description(event_types::RTC_APROPRIACAO_CREDITO_COMB),
            "Solicitação de Apropriação de Crédito de Combustível"
        );
        assert_eq!(
            event_description(event_types::RTC_APROPRIACAO_CREDITO_BENS),
            "Solicitação de Apropriação de Crédito para bens e serviços que dependem de atividade do adquirente"
        );
        assert_eq!(
            event_description(event_types::RTC_MANIF_TRANSF_CRED_IBS),
            "Manifestação sobre Pedido de Transferência de Crédito de IBS em Operação de Sucessão"
        );
        assert_eq!(
            event_description(event_types::RTC_MANIF_TRANSF_CRED_CBS),
            "Manifestação sobre Pedido de Transferência de Crédito de CBS em Operação de Sucessão"
        );
    }

    #[test]
    fn event_description_unknown_returns_empty() {
        assert_eq!(event_description(999999), "");
    }

    // ── Manifestacao with OPERATION_NOT_PERFORMED but no justification (line 432) ──

    #[test]
    fn manifesta_operation_not_performed_empty_justification() {
        let xml = build_manifesta_request(
            TEST_KEY,
            "210240",
            None,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        // With None justification, should NOT contain <xJust>
        assert!(!xml.contains("<xJust>"));
        assert!(xml.contains("<tpEvento>210240</tpEvento>"));
    }

    #[test]
    fn manifesta_operation_not_performed_empty_string_justification() {
        let xml = build_manifesta_request(
            TEST_KEY,
            "210240",
            Some(""),
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
        // Empty string justification should NOT produce <xJust>
        assert!(!xml.contains("<xJust>"));
    }

    // ── DistDFe with specific NSU (line 498) ────────────────────────

    #[test]
    fn dist_dfe_request_with_specific_nsu() {
        let xml = build_dist_dfe_request(
            "SP",
            TEST_CNPJ,
            Some("123456789012345"),
            None,
            SefazEnvironment::Homologation,
        );
        // NSU that does NOT start with '0' should use consNSU
        assert!(xml.contains("<consNSU><NSU>123456789012345</NSU></consNSU>"));
    }

    #[test]
    fn dist_dfe_request_access_key_non_numeric_uses_cons_nsu() {
        // Non-44-digit or non-numeric "access key" treated as NSU
        let xml = build_dist_dfe_request(
            "SP",
            TEST_CNPJ,
            None,
            Some("ABC123"),
            SefazEnvironment::Homologation,
        );
        assert!(xml.contains("<consNSU><NSU>ABC123</NSU></consNSU>"));
    }

    // ── Cadastro with unknown search type (line 528) ────────────────

    #[test]
    fn cadastro_request_with_unknown_search_type() {
        let xml = build_cadastro_request("SP", "UNKNOWN", "12345");
        // Unknown type should produce empty filter
        assert!(xml.contains("<xServ>CONS-CAD</xServ>"));
        assert!(xml.contains("<UF>SP</UF>"));
        // Should not contain any filter tag
        assert!(!xml.contains("<CNPJ>"));
        assert!(!xml.contains("<CPF>"));
        assert!(!xml.contains("<IE>"));
    }

    // ── cancel_substituicao asserts (lines 563, 571) ────────────────

    #[test]
    #[should_panic(expected = "Cancellation justification (xJust) is required")]
    fn cancel_substituicao_rejects_empty_justification() {
        build_cancel_substituicao_request(
            TEST_KEY,
            "35240112345678000195650010000000021000000028",
            "135220000009921",
            "",
            "APP-1.0",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
    }

    #[test]
    #[should_panic(expected = "Reference access key (chNFeRef) is required")]
    fn cancel_substituicao_rejects_empty_ref_key() {
        build_cancel_substituicao_request(
            TEST_KEY,
            "",
            "135220000009921",
            "Motivo",
            "APP-1.0",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
    }

    #[test]
    #[should_panic(expected = "Application version (verAplic) is required")]
    fn cancel_substituicao_rejects_empty_ver_aplic() {
        build_cancel_substituicao_request(
            TEST_KEY,
            "35240112345678000195650010000000021000000028",
            "135220000009921",
            "Motivo",
            "",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
    }

    // ── Ator interessado panic (line 634) ───────────────────────────

    #[test]
    #[should_panic(expected = "Either authorized_cnpj or authorized_cpf must be provided")]
    fn ator_interessado_panics_without_cnpj_or_cpf() {
        build_ator_interessado_request(
            TEST_KEY,
            1,
            "APP-1.0",
            None,
            None,
            0,
            "SP",
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
        );
    }

    // ── extract_epec_data invalid access key length (lines 987, 989) ─

    #[test]
    fn extract_epec_data_rejects_short_access_key() {
        let xml = concat!(
            r#"<NFe><infNFe versao="4.00" Id="NFe12345">"#,
            r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
            r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
            r#"<dest><CNPJ>98765432000188</CNPJ><UF>RJ</UF></dest>"#,
            r#"<total><ICMSTot><vNF>100.00</vNF><vICMS>18.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
            r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
            r#"</infNFe></NFe>"#,
        );
        let err = extract_epec_data(xml, None).unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("Invalid access key length"), "got: {msg}");
    }

    // ── extract_epec_data missing dest ID (lines 1020-1021) ─────────

    #[test]
    fn extract_epec_data_rejects_dest_without_id() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><UF>SP</UF></dest>"#,
                r#"<total><ICMSTot><vNF>100.00</vNF><vICMS>18.00</vICMS><vST>0.00</vST></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY,
        );
        let err = extract_epec_data(&xml, None).unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("Missing CNPJ/CPF/idEstrangeiro"), "got: {msg}");
    }

    // ── extract_epec_nfce_data invalid access key (lines 1243, 1245) ─

    #[test]
    fn extract_epec_nfce_data_rejects_short_access_key() {
        let xml = concat!(
            r#"<NFe><infNFe versao="4.00" Id="NFe999">"#,
            r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
            r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
            r#"<total><ICMSTot><vNF>50.00</vNF><vICMS>9.00</vICMS></ICMSTot></total>"#,
            r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
            r#"</infNFe></NFe>"#,
        );
        let err = extract_epec_nfce_data(xml, None).unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("Invalid access key length"), "got: {msg}");
    }

    // ── extract_epec_nfce_data dest with CNPJ (line 1267) ──────────

    #[test]
    fn extract_epec_nfce_data_dest_with_cnpj() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><CNPJ>98765432000188</CNPJ><UF>RJ</UF><IE>987654321</IE></dest>"#,
                r#"<total><ICMSTot><vNF>200.00</vNF><vICMS>36.00</vICMS></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY,
        );
        let data = extract_epec_nfce_data(&xml, None).unwrap();
        assert_eq!(
            data.dest_id_tag.as_deref(),
            Some("<CNPJ>98765432000188</CNPJ>")
        );
        assert_eq!(data.dest_ie.as_deref(), Some("987654321"));
    }

    // ── extract_epec_nfce_data dest with idEstrangeiro (lines 1271-1272) ─

    #[test]
    fn extract_epec_nfce_data_dest_with_id_estrangeiro() {
        let xml = format!(
            concat!(
                r#"<NFe><infNFe versao="4.00" Id="NFe{key}">"#,
                r#"<ide><tpNF>1</tpNF><dhEmi>2026-03-12T10:00:00-03:00</dhEmi></ide>"#,
                r#"<emit><CNPJ>12345678000199</CNPJ><IE>123456789</IE></emit>"#,
                r#"<dest><idEstrangeiro>PASS123</idEstrangeiro><UF>EX</UF></dest>"#,
                r#"<total><ICMSTot><vNF>300.00</vNF><vICMS>0.00</vICMS></ICMSTot></total>"#,
                r#"<infRespTec><verProc>test</verProc></infRespTec>"#,
                r#"</infNFe></NFe>"#,
            ),
            key = TEST_KEY,
        );
        let data = extract_epec_nfce_data(&xml, None).unwrap();
        assert_eq!(
            data.dest_id_tag.as_deref(),
            Some("<idEstrangeiro>PASS123</idEstrangeiro>")
        );
        assert_eq!(data.dest_uf.as_deref(), Some("EX"));
    }

    // ── build_epec_nfce_request with dest_ie present (line 1343) ────

    #[test]
    fn build_epec_nfce_request_with_dest_ie() {
        let data = EpecNfceData {
            access_key: TEST_KEY.to_string(),
            c_orgao_autor: "35".to_string(),
            ver_aplic: "test".to_string(),
            dh_emi: "2026-03-12T10:00:00-03:00".to_string(),
            tp_nf: "1".to_string(),
            emit_ie: "123456789".to_string(),
            dest_uf: Some("RJ".to_string()),
            dest_id_tag: Some("<CNPJ>98765432000188</CNPJ>".to_string()),
            dest_ie: Some("987654321".to_string()),
            v_nf: "1500.00".to_string(),
            v_icms: "270.00".to_string(),
            tax_id: "12345678000199".to_string(),
        };
        let request = build_epec_nfce_request(&data, SefazEnvironment::Homologation);
        assert!(request.contains("<IE>987654321</IE>"));
        assert!(
            request
                .contains("<dest><UF>RJ</UF><CNPJ>98765432000188</CNPJ><IE>987654321</IE></dest>")
        );
    }

    // ── CSC request with short CNPJ (line 1502) ─────────────────────

    #[test]
    fn csc_request_with_short_cnpj() {
        let xml = build_csc_request(SefazEnvironment::Homologation, 1, "12345", None, None);
        // When CNPJ < 8 digits, raizCNPJ = entire string
        assert!(xml.contains("<raizCNPJ>12345</raizCNPJ>"));
    }

    // ── Event batch with lot_id=None (lines 1628-1631) ──────────────

    #[test]
    fn event_batch_request_auto_lot_id() {
        let events = vec![EventItem {
            access_key: TEST_KEY.to_string(),
            event_type: event_types::AWARENESS,
            seq: 1,
            tax_id: TEST_CNPJ.to_string(),
            additional_tags: String::new(),
        }];
        let xml = build_event_batch_request("AN", &events, None, SefazEnvironment::Homologation);
        // Should have a numeric auto-generated lot ID (timestamp in millis)
        assert!(xml.contains("<idLote>"));
        assert!(xml.contains("</idLote>"));
        // The lot ID should be numeric (system time millis)
        let lot_start = xml.find("<idLote>").unwrap() + 8;
        let lot_end = xml[lot_start..].find("</idLote>").unwrap() + lot_start;
        let lot_value = &xml[lot_start..lot_end];
        assert!(
            lot_value.chars().all(|c| c.is_ascii_digit()),
            "Auto-generated lot ID should be numeric, got: {lot_value}"
        );
    }

    // ── extract_section partial tag match (line 1773) ───────────────

    #[test]
    fn extract_section_rejects_partial_tag_match() {
        let xml = "<emitter>data</emitter>";
        // Looking for <emit> should NOT match <emitter>
        let result = extract_section(xml, "emit");
        assert!(
            result.is_none(),
            "extract_section should not match <emitter> when looking for <emit>"
        );
    }

    #[test]
    fn extract_section_matches_exact_tag() {
        let xml = "<emit>data</emit>";
        let result = extract_section(xml, "emit");
        assert_eq!(result.as_deref(), Some("<emit>data</emit>"));
    }

    #[test]
    fn extract_section_matches_tag_with_attributes() {
        let xml = r#"<emit versao="4.00">data</emit>"#;
        let result = extract_section(xml, "emit");
        assert_eq!(
            result.as_deref(),
            Some(r#"<emit versao="4.00">data</emit>"#)
        );
    }

    // ── strip_xml_declaration (lines 2508-2510) ─────────────────────

    #[test]
    fn strip_xml_declaration_removes_declaration() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root>data</root>"#;
        let result = strip_xml_declaration(xml);
        assert_eq!(result, "<root>data</root>");
    }

    #[test]
    fn strip_xml_declaration_no_declaration() {
        let xml = "<root>data</root>";
        let result = strip_xml_declaration(xml);
        assert_eq!(result, "<root>data</root>");
    }

    #[test]
    fn strip_xml_declaration_with_leading_whitespace() {
        let xml = r#"<?xml version="1.0"?>  <root>data</root>"#;
        let result = strip_xml_declaration(xml);
        assert_eq!(result, "<root>data</root>");
    }

    // ── RtcItem::new and builder methods (lines 1814, 1830-1847) ────

    #[test]
    fn rtc_item_new_defaults() {
        let item = RtcItem::new(1, 10.50, 5.25);
        assert_eq!(item.item, 1);
        assert!((item.v_ibs - 10.50).abs() < f64::EPSILON);
        assert!((item.v_cbs - 5.25).abs() < f64::EPSILON);
        assert!(item.quantidade.is_none());
        assert!(item.unidade.is_none());
        assert!(item.chave.is_none());
        assert!(item.n_item.is_none());
        assert!(item.g_controle_estoque_v_ibs.is_none());
        assert!(item.g_controle_estoque_v_cbs.is_none());
        assert!(item.v_cred_ibs.is_none());
        assert!(item.v_cred_cbs.is_none());
    }

    #[test]
    fn rtc_item_builder_methods() {
        let item = RtcItem::new(1, 10.0, 5.0)
            .quantidade(3.5)
            .unidade("UN")
            .chave("35240112345678000195550010000000011000000019")
            .n_item(2);
        assert_eq!(item.quantidade, Some(3.5));
        assert_eq!(item.unidade.as_deref(), Some("UN"));
        assert_eq!(
            item.chave.as_deref(),
            Some("35240112345678000195550010000000011000000019")
        );
        assert_eq!(item.n_item, Some(2));
    }

    // ── RTC: build_rtc_info_pagto_integral (lines 1876-1899) ────────

    #[test]
    fn rtc_info_pagto_integral_structure() {
        let xml = build_rtc_info_pagto_integral(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "FISCAL-RS-1.0",
            None,
        );
        assert!(xml.contains("<tpEvento>112110</tpEvento>"));
        assert!(xml.contains("<descEvento>Informação de efetivo pagamento integral para liberar crédito presumido do adquirente</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<verAplic>FISCAL-RS-1.0</verAplic>"));
        assert!(xml.contains("<indQuitacao>1</indQuitacao>"));
    }

    // ── RTC: build_rtc_sol_aprop_cred_presumido (lines 1904-1951) ───

    #[test]
    fn rtc_sol_aprop_cred_presumido_structure() {
        let itens = vec![RtcCredPresItem {
            item: 1,
            v_bc: 1000.00,
            g_ibs: Some(RtcCredPresSub {
                c_cred_pres: "001".to_string(),
                p_cred_pres: 0.0525,
                v_cred_pres: 52.50,
            }),
            g_cbs: Some(RtcCredPresSub {
                c_cred_pres: "002".to_string(),
                p_cred_pres: 0.0312,
                v_cred_pres: 31.20,
            }),
        }];
        let xml = build_rtc_sol_aprop_cred_presumido(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211110</tpEvento>"));
        assert!(
            xml.contains(
                "<descEvento>Solicitação de Apropriação de crédito presumido</descEvento>"
            )
        );
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<gCredPres nItem=\"1\">"));
        assert!(xml.contains("<vBC>1000.00</vBC>"));
        assert!(xml.contains("<gIBS><cCredPres>001</cCredPres><pCredPres>0.0525</pCredPres><vCredPres>52.50</vCredPres></gIBS>"));
        assert!(xml.contains("<gCBS><cCredPres>002</cCredPres><pCredPres>0.0312</pCredPres><vCredPres>31.20</vCredPres></gCBS>"));
        assert!(xml.contains("</gCredPres>"));
    }

    // ── RTC: build_rtc_destino_consumo_pessoal (lines 1957-1996) ────

    #[test]
    fn rtc_destino_consumo_pessoal_structure() {
        let itens = vec![
            RtcItem::new(1, 15.00, 7.50)
                .quantidade(2.0)
                .unidade("UN")
                .chave(TEST_KEY)
                .n_item(1),
        ];
        let xml = build_rtc_destino_consumo_pessoal(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            2,
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211120</tpEvento>"));
        assert!(xml.contains("<descEvento>Destinação de item para consumo pessoal</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<gConsumo nItem=\"1\">"));
        assert!(xml.contains("<vIBS>15.00</vIBS>"));
        assert!(xml.contains("<vCBS>7.50</vCBS>"));
        assert!(xml.contains("<qConsumo>2.0000</qConsumo>"));
        assert!(xml.contains("<uConsumo>UN</uConsumo>"));
        assert!(xml.contains(&format!("<chaveAcesso>{TEST_KEY}</chaveAcesso>")));
        assert!(xml.contains("<nItem>1</nItem>"));
        assert!(xml.contains("</gConsumo>"));
    }

    // ── RTC: build_rtc_aceite_debito (lines 2001-2025) ──────────────

    #[test]
    fn rtc_aceite_debito_structure() {
        let xml = build_rtc_aceite_debito(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            1,
            None,
        );
        assert!(xml.contains("<tpEvento>211128</tpEvento>"));
        assert!(xml.contains(
            "<descEvento>Aceite de débito na apuração por emissão de nota de crédito</descEvento>"
        ));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<indAceitacao>1</indAceitacao>"));
    }

    // ── RTC: build_rtc_imobilizacao_item (lines 2030-2065) ──────────

    #[test]
    fn rtc_imobilizacao_item_structure() {
        let itens = vec![RtcItem::new(1, 20.00, 10.00).quantidade(5.0).unidade("PC")];
        let xml = build_rtc_imobilizacao_item(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211130</tpEvento>"));
        assert!(xml.contains("<descEvento>Imobilização de Item</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<gImobilizacao nItem=\"1\">"));
        assert!(xml.contains("<vIBS>20.00</vIBS>"));
        assert!(xml.contains("<vCBS>10.00</vCBS>"));
        assert!(xml.contains("<qImobilizado>5.0000</qImobilizado>"));
        assert!(xml.contains("<uImobilizado>PC</uImobilizado>"));
        assert!(xml.contains("</gImobilizacao>"));
    }

    // ── RTC: build_rtc_apropriacao_credito_comb (lines 2070-2105) ───

    #[test]
    fn rtc_apropriacao_credito_comb_structure() {
        let itens = vec![
            RtcItem::new(1, 30.00, 15.00)
                .quantidade(100.0)
                .unidade("LT"),
        ];
        let xml = build_rtc_apropriacao_credito_comb(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211140</tpEvento>"));
        assert!(xml.contains(
            "<descEvento>Solicitação de Apropriação de Crédito de Combustível</descEvento>"
        ));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<gConsumoComb nItem=\"1\">"));
        assert!(xml.contains("<vIBS>30.00</vIBS>"));
        assert!(xml.contains("<vCBS>15.00</vCBS>"));
        assert!(xml.contains("<qComb>100.0000</qComb>"));
        assert!(xml.contains("<uComb>LT</uComb>"));
        assert!(xml.contains("</gConsumoComb>"));
    }

    // ── RTC: build_rtc_apropriacao_credito_bens (lines 2110-2141) ───

    #[test]
    fn rtc_apropriacao_credito_bens_structure() {
        let mut item = RtcItem::new(1, 50.00, 25.00);
        item.v_cred_ibs = Some(45.00);
        item.v_cred_cbs = Some(22.50);
        let itens = vec![item];
        let xml = build_rtc_apropriacao_credito_bens(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211150</tpEvento>"));
        assert!(xml.contains("<descEvento>Solicitação de Apropriação de Crédito para bens e serviços que dependem de atividade do adquirente</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<gCredito nItem=\"1\">"));
        assert!(xml.contains("<vCredIBS>45.00</vCredIBS>"));
        assert!(xml.contains("<vCredCBS>22.50</vCredCBS>"));
        assert!(xml.contains("</gCredito>"));
    }

    #[test]
    fn rtc_apropriacao_credito_bens_uses_ibs_cbs_fallback() {
        // Without explicit v_cred_ibs/v_cred_cbs, should use v_ibs/v_cbs
        let itens = vec![RtcItem::new(1, 50.00, 25.00)];
        let xml = build_rtc_apropriacao_credito_bens(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<vCredIBS>50.00</vCredIBS>"));
        assert!(xml.contains("<vCredCBS>25.00</vCredCBS>"));
    }

    // ── RTC: build_rtc_manif_transf_cred_ibs (lines 2146-2170) ─────

    #[test]
    fn rtc_manif_transf_cred_ibs_structure() {
        let xml = build_rtc_manif_transf_cred_ibs(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            1,
            None,
        );
        assert!(xml.contains("<tpEvento>212110</tpEvento>"));
        assert!(xml.contains("<descEvento>Manifestação sobre Pedido de Transferência de Crédito de IBS em Operação de Sucessão</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>8</tpAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<indAceitacao>1</indAceitacao>"));
    }

    // ── RTC: build_rtc_manif_transf_cred_cbs (lines 2175-2199) ─────

    #[test]
    fn rtc_manif_transf_cred_cbs_structure() {
        let xml = build_rtc_manif_transf_cred_cbs(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            0,
            None,
        );
        assert!(xml.contains("<tpEvento>212120</tpEvento>"));
        assert!(xml.contains("<descEvento>Manifestação sobre Pedido de Transferência de Crédito de CBS em Operação de Sucessão</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>8</tpAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<indAceitacao>0</indAceitacao>"));
    }

    // ── RTC: build_rtc_cancela_evento (lines 2205-2230) ─────────────

    #[test]
    fn rtc_cancela_evento_structure() {
        let xml = build_rtc_cancela_evento(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            "211120",
            "135220000009999",
            None,
        );
        assert!(xml.contains("<tpEvento>110001</tpEvento>"));
        assert!(xml.contains("<descEvento>Cancelamento de Evento</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<tpEventoAut>211120</tpEventoAut>"));
        assert!(xml.contains("<nProtEvento>135220000009999</nProtEvento>"));
    }

    // ── RTC: build_rtc_importacao_zfm (lines 2235-2270) ─────────────

    #[test]
    fn rtc_importacao_zfm_structure() {
        let itens = vec![RtcItem::new(1, 12.00, 6.00).quantidade(10.0).unidade("KG")];
        let xml = build_rtc_importacao_zfm(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>112120</tpEvento>"));
        assert!(
            xml.contains(
                "<descEvento>Importação em ALC/ZFM não convertida em isenção</descEvento>"
            )
        );
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<gConsumo nItem=\"1\">"));
        assert!(xml.contains("<vIBS>12.00</vIBS>"));
        assert!(xml.contains("<vCBS>6.00</vCBS>"));
        assert!(xml.contains("<qtde>10.0000</qtde>"));
        assert!(xml.contains("<unidade>KG</unidade>"));
        assert!(xml.contains("</gConsumo>"));
    }

    // ── RTC: build_rtc_roubo_perda_adquirente (lines 2275-2310) ─────

    #[test]
    fn rtc_roubo_perda_adquirente_structure() {
        let itens = vec![RtcItem::new(1, 8.00, 4.00).quantidade(3.0).unidade("CX")];
        let xml = build_rtc_roubo_perda_adquirente(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>211124</tpEvento>"));
        assert!(xml.contains("<descEvento>Perecimento, perda, roubo ou furto durante o transporte contratado pelo adquirente</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>2</tpAutor>"));
        assert!(xml.contains("<gPerecimento nItem=\"1\">"));
        assert!(xml.contains("<vIBS>8.00</vIBS>"));
        assert!(xml.contains("<vCBS>4.00</vCBS>"));
        assert!(xml.contains("<qPerecimento>3.0000</qPerecimento>"));
        assert!(xml.contains("<uPerecimento>CX</uPerecimento>"));
        assert!(xml.contains("</gPerecimento>"));
    }

    // ── RTC: build_rtc_roubo_perda_fornecedor (lines 2315-2353) ─────

    #[test]
    fn rtc_roubo_perda_fornecedor_structure() {
        let mut item = RtcItem::new(1, 16.00, 8.00).quantidade(7.0).unidade("UN");
        item.g_controle_estoque_v_ibs = Some(14.00);
        item.g_controle_estoque_v_cbs = Some(7.00);
        let itens = vec![item];
        let xml = build_rtc_roubo_perda_fornecedor(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>112130</tpEvento>"));
        assert!(xml.contains("<descEvento>Perecimento, perda, roubo ou furto durante o transporte contratado pelo fornecedor</descEvento>"));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<gPerecimento nItem=\"1\">"));
        assert!(xml.contains("<vIBS>16.00</vIBS>"));
        assert!(xml.contains("<vCBS>8.00</vCBS>"));
        assert!(xml.contains("<qPerecimento>7.0000</qPerecimento>"));
        assert!(xml.contains("<uPerecimento>UN</uPerecimento>"));
        // gControleEstoque also contains vIBS/vCBS
        assert!(xml.contains("<vIBS>14.00</vIBS>"));
        assert!(xml.contains("<vCBS>7.00</vCBS>"));
        assert!(xml.contains("</gPerecimento>"));
    }

    // ── RTC: build_rtc_fornecimento_nao_realizado (lines 2358-2393) ──

    #[test]
    fn rtc_fornecimento_nao_realizado_structure() {
        let itens = vec![RtcItem::new(1, 22.00, 11.00).quantidade(4.0).unidade("MT")];
        let xml = build_rtc_fornecimento_nao_realizado(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            &itens,
            None,
        );
        assert!(xml.contains("<tpEvento>112140</tpEvento>"));
        assert!(xml.contains(
            "<descEvento>Fornecimento não realizado com pagamento antecipado</descEvento>"
        ));
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<gItemNaoFornecido nItem=\"1\">"));
        assert!(xml.contains("<vIBS>22.00</vIBS>"));
        assert!(xml.contains("<vCBS>11.00</vCBS>"));
        assert!(xml.contains("<qNaoFornecida>4.0000</qNaoFornecida>"));
        assert!(xml.contains("<uNaoFornecida>MT</uNaoFornecida>"));
        assert!(xml.contains("</gItemNaoFornecido>"));
    }

    // ── RTC: build_rtc_atualizacao_data_entrega (lines 2398-2422) ───

    #[test]
    fn rtc_atualizacao_data_entrega_structure() {
        let xml = build_rtc_atualizacao_data_entrega(
            TEST_KEY,
            1,
            SefazEnvironment::Homologation,
            TEST_CNPJ,
            "SP",
            "APP-1.0",
            "2026-06-15",
            None,
        );
        assert!(xml.contains("<tpEvento>112150</tpEvento>"));
        assert!(
            xml.contains("<descEvento>Atualização da Data de Previsão de Entrega</descEvento>")
        );
        assert!(xml.contains("<cOrgaoAutor>35</cOrgaoAutor>"));
        assert!(xml.contains("<tpAutor>1</tpAutor>"));
        assert!(xml.contains("<verAplic>APP-1.0</verAplic>"));
        assert!(xml.contains("<dPrevEntrega>2026-06-15</dPrevEntrega>"));
    }

    // ── build_autorizacao_batch_request ──────────────────────────────

    #[test]
    fn batch_request_single_xml() {
        let nfe = "<NFe><infNFe>data1</infNFe></NFe>";
        let result = build_autorizacao_batch_request(&[nfe], "123", 0).unwrap();
        assert!(result.contains("<idLote>123</idLote>"));
        assert!(result.contains("<indSinc>0</indSinc>"));
        assert!(result.contains("<NFe><infNFe>data1</infNFe></NFe>"));
        assert!(
            result.starts_with(
                "<enviNFe xmlns=\"http://www.portalfiscal.inf.br/nfe\" versao=\"4.00\">"
            )
        );
        assert!(result.ends_with("</enviNFe>"));
    }

    #[test]
    fn batch_request_multiple_xmls() {
        let nfe1 = "<NFe><infNFe>data1</infNFe></NFe>";
        let nfe2 = "<NFe><infNFe>data2</infNFe></NFe>";
        let nfe3 = "<NFe><infNFe>data3</infNFe></NFe>";
        let result = build_autorizacao_batch_request(&[nfe1, nfe2, nfe3], "456", 0).unwrap();
        assert!(result.contains("<indSinc>0</indSinc>"));
        assert!(result.contains("<NFe><infNFe>data1</infNFe></NFe><NFe><infNFe>data2</infNFe></NFe><NFe><infNFe>data3</infNFe></NFe>"));
    }

    #[test]
    fn batch_request_strips_xml_declarations() {
        let nfe1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><NFe><infNFe>d1</infNFe></NFe>";
        let nfe2 = "<?xml version=\"1.0\"?><NFe><infNFe>d2</infNFe></NFe>";
        let result = build_autorizacao_batch_request(&[nfe1, nfe2], "789", 0).unwrap();
        assert!(!result.contains("<?xml"));
        assert!(result.contains("<NFe><infNFe>d1</infNFe></NFe>"));
        assert!(result.contains("<NFe><infNFe>d2</infNFe></NFe>"));
    }

    #[test]
    fn batch_request_sync_single_ok() {
        let nfe = "<NFe><infNFe>data</infNFe></NFe>";
        let result = build_autorizacao_batch_request(&[nfe], "1", 1).unwrap();
        assert!(result.contains("<indSinc>1</indSinc>"));
    }

    #[test]
    fn batch_request_sync_multiple_rejected() {
        let nfe1 = "<NFe>1</NFe>";
        let nfe2 = "<NFe>2</NFe>";
        let err = build_autorizacao_batch_request(&[nfe1, nfe2], "1", 1).unwrap_err();
        assert!(
            matches!(err, fiscal_core::FiscalError::InvalidTaxData(_)),
            "expected InvalidTaxData, got: {err}"
        );
    }

    #[test]
    fn batch_request_rejects_empty() {
        let err = build_autorizacao_batch_request(&[], "1", 0).unwrap_err();
        assert!(matches!(err, fiscal_core::FiscalError::InvalidTaxData(_)));
    }

    #[test]
    fn batch_request_rejects_more_than_50() {
        let xmls: Vec<&str> = (0..51).map(|_| "<NFe>x</NFe>").collect();
        let err = build_autorizacao_batch_request(&xmls, "1", 0).unwrap_err();
        assert!(matches!(err, fiscal_core::FiscalError::InvalidTaxData(_)));
        let msg = err.to_string();
        assert!(
            msg.contains("50"),
            "error should mention the 50-doc limit: {msg}"
        );
    }

    #[test]
    fn batch_request_accepts_exactly_50() {
        let xmls: Vec<&str> = (0..50).map(|_| "<NFe>x</NFe>").collect();
        let result = build_autorizacao_batch_request(&xmls, "1", 0);
        assert!(result.is_ok());
    }
}
