use std::collections::HashMap;

use chrono::NaiveDate;

use crate::mappings::{CustomConceptMapping, TargetField};
use crate::terminal::sec_edgar::earnings::presentation::{
    BASIC_EPS_CONCEPTS, BASIC_SHARES_CONCEPTS, COMMON_SHAREHOLDER_NET_INCOME_CONCEPTS,
    CONSOLIDATED_NET_INCOME_CONCEPTS, COST_OF_SALES_CONCEPTS, DILUTED_EPS_CONCEPTS,
    DILUTED_SHARES_CONCEPTS, EBITDA_CONCEPTS, EFFECTIVE_TAX_RATE_CONCEPTS, GROSS_PROFIT_CONCEPTS,
    INCOME_BEFORE_TAX_CONCEPTS, NON_OPERATING_INCOME_CONCEPTS, OPERATING_PROFIT_CONCEPTS,
    PROVISION_FOR_INCOME_TAXES_CONCEPTS, RESEARCH_AND_DEVELOPMENT_CONCEPTS, REVENUE_CONCEPTS,
    R_AND_D_CONCEPTS, SELLING_GENERAL_AND_ADMINISTRATIVE_CONCEPTS,
    TOTAL_NON_OPERATING_INCOME_CONCEPTS, TOTAL_SHARES_OUTSTANDING_CONCEPTS,
};
use crate::terminal::sec_edgar::types::ConceptCandidate;
use crate::terminal::{EarningsPeriod, EarningsPeriodOption, FilingRef, Frequency};

use super::super::mapping_support::{
    collect_available_concepts, collect_formula_mappings, evaluate_formulas, formula_check_targets,
    FormulaCheckTargets,
};
use super::dei::PeriodIdentity;
use super::facts::{
    classify_fact_unit_family, filing_fact_currency_code, FilingContext, FilingXbrlDocument,
};

#[derive(Debug, Clone)]
pub(crate) struct StatementWindow {
    pub start: String,
    pub end: String,
}

#[derive(Debug, Clone)]
pub(crate) struct FilingEarningsRecord {
    pub period: EarningsPeriod,
    pub option: EarningsPeriodOption,
    pub filing: FilingRef,
    pub formula_check_targets: FormulaCheckTargets,
}

pub(crate) fn map_filing_to_record(
    document: &FilingXbrlDocument,
    filing: &FilingRef,
    identity: &PeriodIdentity,
    frequency: Frequency,
    ticker: &str,
    custom_mappings: &[CustomConceptMapping],
    selected_currency: Option<&str>,
) -> Option<(FilingEarningsRecord, Vec<String>)> {
    let statement_window = select_statement_window(
        document,
        identity,
        frequency,
        ticker,
        custom_mappings,
        selected_currency,
    )?;

    // Step 1: Collect all raw XBRL values and concept-mapped values
    let mut field_values = collect_raw_field_values(
        document,
        &statement_window,
        identity,
        custom_mappings,
        ticker,
        selected_currency,
    );

    // Step 2: Collect and evaluate formula mappings
    let formula_mappings = collect_formula_mappings(custom_mappings, ticker, &identity.period_end)
        .into_iter()
        .filter(|(mapping, _)| mapping.target_field != TargetField::TotalNonOperatingIncome)
        .collect::<Vec<_>>();
    let formula_check_targets = formula_check_targets(&formula_mappings);

    // Create concept resolver for formula evaluation
    let concept_resolver =
        crate::mappings::XbrlConceptResolver::new(collect_available_concepts(document));

    // Evaluate formulas (which will add calculated values to field_values)
    let (formula_values, formula_warnings) =
        evaluate_formulas(formula_mappings, &field_values, &concept_resolver);
    for (field, value) in formula_values {
        field_values.insert(field, value);
    }

    // Step 3: Extract values from the field_values HashMap
    let total_revenues = get_field_value_with_formulas(&field_values, TargetField::TotalRevenues);
    let consolidated_net_income =
        get_field_value_with_formulas(&field_values, TargetField::NetIncome);

    if total_revenues.is_none() && consolidated_net_income.is_none() {
        return None;
    }

    let cost_of_sales = get_field_value_with_formulas(&field_values, TargetField::CostOfSales);
    let gross_profit = get_field_value_with_formulas(&field_values, TargetField::GrossProfit);
    let selling_general_and_administrative_expenses =
        get_field_value_with_formulas(&field_values, TargetField::SellingGeneralAndAdministrative);
    let research_and_development_expenses =
        get_field_value_with_formulas(&field_values, TargetField::ResearchAndDevelopment).or_else(
            || {
                // Try alternative R&D concepts
                select_duration_value_with_custom(
                    document,
                    &statement_window,
                    R_AND_D_CONCEPTS,
                    TargetField::ResearchAndDevelopment,
                    custom_mappings,
                    ticker,
                    &identity.period_end,
                    selected_currency,
                )
            },
        );
    let operating_profit =
        get_field_value_with_formulas(&field_values, TargetField::OperatingProfit);
    let non_operating_income =
        get_field_value_with_formulas(&field_values, TargetField::NonOperatingIncome);
    let total_non_operating_income =
        get_field_value_with_formulas(&field_values, TargetField::TotalNonOperatingIncome);
    let income_before_provision_for_income_taxes = get_field_value_with_formulas(
        &field_values,
        TargetField::IncomeBeforeProvisionForIncomeTaxes,
    );
    let provision_for_income_taxes =
        get_field_value_with_formulas(&field_values, TargetField::ProvisionForIncomeTaxes);
    let net_income_attributable_to_common_shareholders = get_field_value_with_formulas(
        &field_values,
        TargetField::NetIncomeAttributableToCommonShareholders,
    );
    let total_shares_outstanding =
        get_field_value_with_formulas(&field_values, TargetField::TotalSharesOutstanding);
    let basic_weighted_average_shares_outstanding = get_field_value_with_formulas(
        &field_values,
        TargetField::BasicWeightedAverageSharesOutstanding,
    );
    let diluted_weighted_average_shares_outstanding = get_field_value_with_formulas(
        &field_values,
        TargetField::DilutedWeightedAverageSharesOutstanding,
    );
    let basic_eps = get_field_value_with_formulas(&field_values, TargetField::BasicEps);
    let diluted_eps = get_field_value_with_formulas(&field_values, TargetField::DilutedEps);
    let ebitda = get_field_value_with_formulas(&field_values, TargetField::Ebitda);
    let effective_tax_rate =
        get_field_value_with_formulas(&field_values, TargetField::EffectiveTaxRate)
            .map(normalize_percent);
    let other_operating_expenses =
        get_field_value_with_formulas(&field_values, TargetField::OtherOperatingExpenses).or_else(
            || {
                calculate_other_operating_expenses(
                    operating_profit,
                    gross_profit,
                    selling_general_and_administrative_expenses,
                    research_and_development_expenses,
                )
            },
        );

    let period = EarningsPeriod {
        label: identity.display_label.clone(),
        display_label: identity.display_label.clone(),
        fiscal_year: Some(identity.fiscal_year.clone()),
        fiscal_period: identity.fiscal_period.clone(),
        period_start: Some(statement_window.start.clone()),
        period_end: identity.period_end.clone(),
        filed_date: filing.filing_date.clone(),
        form: filing.form.clone(),
        total_revenues,
        total_revenues_yoy_change_percent: None,
        cost_of_sales,
        gross_profit,
        gross_profit_margin: calculate_gross_profit_margin(gross_profit, total_revenues),
        selling_general_and_administrative_expenses,
        research_and_development_expenses,
        other_operating_expenses,
        operating_profit,
        operating_margin: calculate_operating_margin(operating_profit, total_revenues),
        non_operating_income,
        total_non_operating_income,
        income_before_provision_for_income_taxes,
        provision_for_income_taxes,
        consolidated_net_income,
        net_income_attributable_to_common_shareholders,
        total_shares_outstanding,
        basic_weighted_average_shares_outstanding,
        diluted_weighted_average_shares_outstanding,
        ebitda,
        effective_tax_rate,
        revenue: total_revenues,
        net_income: consolidated_net_income,
        basic_eps,
        diluted_eps,
        diluted_weighted_average_shares: diluted_weighted_average_shares_outstanding,
        revenue_yoy_change_percent: None,
        diluted_eps_yoy_change_percent: None,
    };

    let option = EarningsPeriodOption {
        id: identity.key.token(),
        label: identity.display_label.clone(),
        frequency,
        fiscal_year: Some(identity.fiscal_year.clone()),
        fiscal_period: identity.fiscal_period.clone(),
        period_end: identity.period_end.clone(),
        sort_key: identity.key.sort_key(),
    };

    Some((
        FilingEarningsRecord {
            period,
            option,
            filing: filing.clone(),
            formula_check_targets,
        },
        formula_warnings,
    ))
}

pub(crate) fn is_clearly_quarter_length(period: &EarningsPeriod) -> bool {
    let Some(start) = period.period_start.as_deref().and_then(parse_date) else {
        return false;
    };
    let Some(end) = parse_date(&period.period_end) else {
        return false;
    };
    let days = (end - start).num_days().abs() + 1;
    (70..=110).contains(&days)
}

pub(crate) fn statement_currency_codes(
    document: &FilingXbrlDocument,
    identity: &PeriodIdentity,
    frequency: Frequency,
    ticker: &str,
    custom_mappings: &[CustomConceptMapping],
) -> Vec<String> {
    let Some(statement_window) =
        select_statement_window(document, identity, frequency, ticker, custom_mappings, None)
    else {
        return Vec::new();
    };

    let mut currencies = collect_window_currency_counts(document, &statement_window)
        .into_keys()
        .collect::<Vec<_>>();
    currencies.sort();
    currencies
}

pub(crate) fn dominant_statement_currency(
    document: &FilingXbrlDocument,
    identity: &PeriodIdentity,
    frequency: Frequency,
    ticker: &str,
    custom_mappings: &[CustomConceptMapping],
) -> Option<String> {
    let statement_window =
        select_statement_window(document, identity, frequency, ticker, custom_mappings, None)?;

    collect_window_currency_counts(document, &statement_window)
        .into_iter()
        .max_by(|left, right| left.1.cmp(&right.1).then_with(|| right.0.cmp(&left.0)))
        .map(|(currency, _)| currency)
}

fn select_statement_window(
    document: &FilingXbrlDocument,
    identity: &PeriodIdentity,
    frequency: Frequency,
    ticker: &str,
    custom_mappings: &[CustomConceptMapping],
    selected_currency: Option<&str>,
) -> Option<StatementWindow> {
    let mut candidates = document
        .contexts
        .values()
        .filter_map(|context| {
            score_context(
                document,
                context,
                identity,
                frequency,
                ticker,
                custom_mappings,
                selected_currency,
            )
        })
        .collect::<Vec<_>>();

    candidates.sort_by_key(|candidate| candidate.0);
    candidates.into_iter().next().map(|(_, window)| window)
}

fn collect_window_currency_counts(
    document: &FilingXbrlDocument,
    window: &StatementWindow,
) -> HashMap<String, usize> {
    let mut counts = HashMap::<String, usize>::new();

    for fact in &document.facts {
        let Some(context_ref) = fact.context_ref.as_deref() else {
            continue;
        };
        let Some(context) = document.contexts.get(context_ref) else {
            continue;
        };
        if context.has_dimensions
            || context.period_start.as_deref() != Some(window.start.as_str())
            || context.period_end.as_deref() != Some(window.end.as_str())
        {
            continue;
        }

        let Some(unit_family) = classify_fact_unit_family(document, fact) else {
            continue;
        };
        if !unit_family.uses_currency() {
            continue;
        }
        let Some(currency_code) = filing_fact_currency_code(document, fact) else {
            continue;
        };

        *counts.entry(currency_code).or_insert(0) += 1;
    }

    counts
}

fn score_context(
    document: &FilingXbrlDocument,
    context: &FilingContext,
    identity: &PeriodIdentity,
    frequency: Frequency,
    ticker: &str,
    custom_mappings: &[CustomConceptMapping],
    selected_currency: Option<&str>,
) -> Option<(i64, StatementWindow)> {
    if context.has_dimensions || !context.is_duration() {
        return None;
    }

    let start = context.period_start.as_ref()?;
    let end = context.period_end.as_ref()?;
    if end != &identity.period_end {
        return None;
    }

    let days = duration_days(start, end)?;
    let target_days = match frequency {
        Frequency::Annual => 365,
        Frequency::Quarterly => 91,
    };
    let accepted = match frequency {
        Frequency::Annual => (300..=390).contains(&days),
        Frequency::Quarterly => (70..=110).contains(&days),
    };
    if !accepted {
        return None;
    }

    let has_revenue = context_has_fact_for_field(
        document,
        context,
        REVENUE_CONCEPTS,
        TargetField::TotalRevenues,
        custom_mappings,
        ticker,
        &identity.period_end,
        selected_currency,
    );
    let has_net_income = context_has_fact_for_field(
        document,
        context,
        CONSOLIDATED_NET_INCOME_CONCEPTS,
        TargetField::NetIncome,
        custom_mappings,
        ticker,
        &identity.period_end,
        selected_currency,
    );
    let completeness_penalty = match (has_revenue, has_net_income) {
        (true, true) => 0,
        (true, false) | (false, true) => 500,
        (false, false) => 1_000,
    };

    Some((
        (days - target_days).abs() + completeness_penalty,
        StatementWindow {
            start: start.clone(),
            end: end.clone(),
        },
    ))
}

fn context_has_fact_for_field(
    document: &FilingXbrlDocument,
    context: &FilingContext,
    standard_candidates: &[ConceptCandidate],
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
) -> bool {
    select_numeric_value_with_custom(
        document,
        standard_candidates,
        target_field,
        custom_mappings,
        ticker,
        period_date,
        selected_currency,
        |candidate_context| {
            candidate_context.period_start == context.period_start
                && candidate_context.period_end == context.period_end
                && !candidate_context.has_dimensions
        },
    )
    .is_some()
}

fn select_duration_value_with_custom(
    document: &FilingXbrlDocument,
    window: &StatementWindow,
    standard_candidates: &[ConceptCandidate],
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
) -> Option<f64> {
    select_numeric_value_with_custom(
        document,
        standard_candidates,
        target_field,
        custom_mappings,
        ticker,
        period_date,
        selected_currency,
        |context| {
            !context.has_dimensions
                && context.period_start.as_deref() == Some(window.start.as_str())
                && context.period_end.as_deref() == Some(window.end.as_str())
        },
    )
}

fn select_instant_value_with_custom(
    document: &FilingXbrlDocument,
    period_end: &str,
    standard_candidates: &[ConceptCandidate],
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
) -> Option<f64> {
    select_numeric_value_with_custom(
        document,
        standard_candidates,
        target_field,
        custom_mappings,
        ticker,
        period_date,
        selected_currency,
        |context| !context.has_dimensions && context.instant.as_deref() == Some(period_end),
    )
}

fn select_numeric_value_with_custom(
    document: &FilingXbrlDocument,
    standard_candidates: &[ConceptCandidate],
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
    matches_context: impl Fn(&FilingContext) -> bool + Copy,
) -> Option<f64> {
    super::super::mapping_support::select_numeric_value_with_custom(
        document,
        standard_candidates,
        target_field,
        custom_mappings,
        ticker,
        period_date,
        selected_currency,
        matches_context,
    )
}

fn normalize_percent(value: f64) -> f64 {
    if value >= -1.0 && value <= 1.0 {
        value * 100.0
    } else {
        value
    }
}

/// Calculates gross profit margin as a percentage.
/// Returns None if numerator or denominator is unavailable or if revenue is effectively zero.
fn calculate_gross_profit_margin(gross_profit: Option<f64>, revenue: Option<f64>) -> Option<f64> {
    let gross_profit = gross_profit?;
    let revenue = revenue?;
    (revenue.abs() > f64::EPSILON).then_some((gross_profit / revenue.abs()) * 100.0)
}

/// Calculates operating margin as a percentage.
/// Returns None if numerator or denominator is unavailable or if revenue is effectively zero.
fn calculate_operating_margin(operating_profit: Option<f64>, revenue: Option<f64>) -> Option<f64> {
    let operating_profit = operating_profit?;
    let revenue = revenue?;
    (revenue.abs() > f64::EPSILON).then_some((operating_profit / revenue.abs()) * 100.0)
}

/// Calculates other operating expenses as a derived value.
/// Formula: gross_profit - operating_profit - sga_expenses - rd_expenses
/// Returns None if not all required components are available.
/// Returns Some(0.0) if calculation yields a value within EPSILON of zero.
fn calculate_other_operating_expenses(
    operating_profit: Option<f64>,
    gross_profit: Option<f64>,
    sga_expenses: Option<f64>,
    rd_expenses: Option<f64>,
) -> Option<f64> {
    let operating_profit = operating_profit?;
    let gross_profit = gross_profit?;
    let sga_expenses = sga_expenses.unwrap_or(0.0);
    let rd_expenses = rd_expenses.unwrap_or(0.0);

    let result = gross_profit - operating_profit - sga_expenses - rd_expenses;

    if result.abs() < f64::EPSILON {
        Some(0.0)
    } else {
        Some(result)
    }
}

/// Get the value for a target field, checking formulas first, then concept mappings, then standard concepts.
pub fn get_field_value_with_formulas(
    field_values: &HashMap<TargetField, f64>,
    target_field: TargetField,
) -> Option<f64> {
    field_values.get(&target_field).copied()
}

/// Collect all raw XBRL values and concept-mapped values into a HashMap.
/// This is the first step before formula evaluation.
fn collect_raw_field_values(
    document: &FilingXbrlDocument,
    window: &StatementWindow,
    identity: &PeriodIdentity,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    selected_currency: Option<&str>,
) -> HashMap<TargetField, f64> {
    let mut field_values = HashMap::new();

    // Helper function to collect a field value
    let mut collect_field =
        |target_field: TargetField, standard_candidates: &[ConceptCandidate], is_instant: bool| {
            // First check custom concept mappings
            let value = if is_instant {
                select_instant_value_with_custom(
                    document,
                    &identity.period_end,
                    standard_candidates,
                    target_field,
                    custom_mappings,
                    ticker,
                    &identity.period_end,
                    selected_currency,
                )
            } else {
                select_duration_value_with_custom(
                    document,
                    window,
                    standard_candidates,
                    target_field,
                    custom_mappings,
                    ticker,
                    &identity.period_end,
                    selected_currency,
                )
            };

            if let Some(v) = value {
                field_values.insert(target_field, v);
            }
        };

    // Collect all field values
    collect_field(TargetField::TotalRevenues, REVENUE_CONCEPTS, false);
    collect_field(
        TargetField::NetIncome,
        CONSOLIDATED_NET_INCOME_CONCEPTS,
        false,
    );
    collect_field(TargetField::CostOfSales, COST_OF_SALES_CONCEPTS, false);
    collect_field(TargetField::GrossProfit, GROSS_PROFIT_CONCEPTS, false);
    collect_field(
        TargetField::SellingGeneralAndAdministrative,
        SELLING_GENERAL_AND_ADMINISTRATIVE_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::ResearchAndDevelopment,
        RESEARCH_AND_DEVELOPMENT_CONCEPTS,
        false,
    );
    collect_field(TargetField::OtherOperatingExpenses, &[], false);
    collect_field(
        TargetField::OperatingProfit,
        OPERATING_PROFIT_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::NonOperatingIncome,
        NON_OPERATING_INCOME_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::TotalNonOperatingIncome,
        TOTAL_NON_OPERATING_INCOME_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::IncomeBeforeProvisionForIncomeTaxes,
        INCOME_BEFORE_TAX_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::ProvisionForIncomeTaxes,
        PROVISION_FOR_INCOME_TAXES_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::NetIncomeAttributableToCommonShareholders,
        COMMON_SHAREHOLDER_NET_INCOME_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::TotalSharesOutstanding,
        TOTAL_SHARES_OUTSTANDING_CONCEPTS,
        true,
    );
    collect_field(
        TargetField::BasicWeightedAverageSharesOutstanding,
        BASIC_SHARES_CONCEPTS,
        false,
    );
    collect_field(
        TargetField::DilutedWeightedAverageSharesOutstanding,
        DILUTED_SHARES_CONCEPTS,
        false,
    );
    collect_field(TargetField::BasicEps, BASIC_EPS_CONCEPTS, false);
    collect_field(TargetField::DilutedEps, DILUTED_EPS_CONCEPTS, false);
    collect_field(TargetField::Ebitda, EBITDA_CONCEPTS, false);
    collect_field(
        TargetField::EffectiveTaxRate,
        EFFECTIVE_TAX_RATE_CONCEPTS,
        false,
    );

    field_values
}

fn duration_days(start: &str, end: &str) -> Option<i64> {
    let start = parse_date(start)?;
    let end = parse_date(end)?;
    Some((end - start).num_days().abs() + 1)
}

fn parse_date(value: &str) -> Option<NaiveDate> {
    NaiveDate::parse_from_str(value, "%Y-%m-%d").ok()
}

#[cfg(test)]
mod tests {
    use crate::mappings::{
        CustomConcept, CustomConceptMapping, FormulaMapping, FormulaParser, MappingKind,
        MappingScope, TargetField,
    };
    use crate::terminal::{FilingRef, Frequency};

    use super::super::dei::PeriodIdentity;
    use super::super::facts::{parse_filing_xbrl_instance, FilingXbrlDocument};
    use super::*;
    use crate::terminal::sec_edgar::{types::EarningsPeriodKey, UnitFamily};

    #[test]
    fn map_filing_to_record_should_select_the_quarter_length_context() {
        let document = sample_document();
        let filing = FilingRef {
            accession_number: "0001".to_string(),
            filing_date: "2025-05-30".to_string(),
            report_date: Some("2025-04-27".to_string()),
            form: "10-Q".to_string(),
            primary_document: None,
        };
        let identity = PeriodIdentity {
            key: EarningsPeriodKey::Quarterly {
                fiscal_year: 2026,
                quarter: 1,
            },
            fiscal_year: "2026".to_string(),
            fiscal_period: Some("Q1".to_string()),
            period_end: "2025-04-27".to_string(),
            display_label: "FY2026 Q1 (Apr)".to_string(),
        };

        let (record, _warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &[],
            None,
        )
        .expect("record should map");

        assert_eq!(record.period.total_revenues, Some(100.0));
        assert_eq!(record.period.period_start.as_deref(), Some("2025-01-27"));
    }

    #[test]
    fn custom_mapping_overrides_standard_candidate() {
        let document = custom_only_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )];

        let (record, _warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        )
        .expect("record should map using the custom concept");

        assert_eq!(record.period.total_revenues, Some(123.0));
    }

    #[test]
    fn custom_mapping_supports_cost_of_sales() {
        let document = custom_cost_of_sales_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::CostOfSales,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeCostOfSales".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )];

        let (record, _warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        )
        .expect("record should map using the custom cost of sales concept");

        assert_eq!(record.period.total_revenues, Some(100.0));
        assert_eq!(record.period.cost_of_sales, Some(55.0));
    }

    #[test]
    fn custom_mapping_supports_other_operating_expenses() {
        let document = custom_other_operating_expenses_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::OtherOperatingExpenses,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeOtherOperatingExpenses".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )];

        let (record, _warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        )
        .expect("record should map using the custom other operating expenses concept");

        assert_eq!(record.period.total_revenues, Some(100.0));
        assert_eq!(record.period.other_operating_expenses, Some(12.0));
    }

    #[test]
    fn custom_mapping_supports_total_shares_outstanding() {
        let document = custom_total_shares_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::TotalSharesOutstanding,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeSharesOutstanding".to_string(),
                unit_family: UnitFamily::Shares,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )];

        let (record, _warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        )
        .expect("record should map using the custom total shares concept");

        assert_eq!(record.period.total_revenues, Some(100.0));
        assert_eq!(record.period.total_shares_outstanding, Some(101.0));
    }

    #[test]
    fn total_non_operating_income_formula_mapping_is_bypassed() {
        let document = sample_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping {
            id: uuid::Uuid::new_v4().to_string(),
            target_field: TargetField::TotalNonOperatingIncome,
            mapping_kind: MappingKind::Formula(FormulaMapping {
                expression: "$non_operating_income".to_string(),
                cached_ast: Some(
                    FormulaParser::parse("$non_operating_income").expect("formula should parse"),
                ),
            }),
            scope: MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            enabled: true,
            notes: None,
        }];

        let (record, warnings) = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        )
        .expect("record should map");

        assert_eq!(record.period.total_revenues, Some(100.0));
        assert_eq!(record.period.total_non_operating_income, None);
        assert!(warnings.is_empty());
    }

    #[test]
    fn custom_mapping_is_ignored_when_ticker_does_not_match() {
        let document = custom_only_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("MSFT".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )];

        let result = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        );

        assert!(result.is_none());
    }

    #[test]
    fn custom_mapping_is_ignored_when_period_does_not_match() {
        let document = custom_only_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let custom_mappings = vec![CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: Some("2024-01-01".to_string()),
                period_end: Some("2024-12-31".to_string()),
            },
            None,
        )];

        let result = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &custom_mappings,
            None,
        );

        assert!(result.is_none());
    }

    #[test]
    fn disabled_custom_mapping_is_ignored() {
        let document = custom_only_document();
        let filing = sample_filing_ref();
        let identity = sample_identity();
        let mut mapping = CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("AAPL".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        );
        mapping.enabled = false;

        let result = map_filing_to_record(
            &document,
            &filing,
            &identity,
            Frequency::Quarterly,
            "AAPL",
            &[mapping],
            None,
        );

        assert!(result.is_none());
    }

    #[test]
    fn is_clearly_quarter_length_should_accept_standard_quarters() {
        let period = EarningsPeriod {
            label: String::new(),
            display_label: String::new(),
            fiscal_year: Some("2026".to_string()),
            fiscal_period: Some("Q1".to_string()),
            period_start: Some("2025-01-27".to_string()),
            period_end: "2025-04-27".to_string(),
            filed_date: "2025-05-30".to_string(),
            form: "10-Q".to_string(),
            total_revenues: None,
            total_revenues_yoy_change_percent: None,
            cost_of_sales: None,
            gross_profit: None,
            gross_profit_margin: None,
            selling_general_and_administrative_expenses: None,
            research_and_development_expenses: None,
            other_operating_expenses: None,
            operating_profit: None,
            operating_margin: None,
            non_operating_income: None,
            total_non_operating_income: None,
            income_before_provision_for_income_taxes: None,
            provision_for_income_taxes: None,
            consolidated_net_income: None,
            net_income_attributable_to_common_shareholders: None,
            total_shares_outstanding: None,
            basic_weighted_average_shares_outstanding: None,
            diluted_weighted_average_shares_outstanding: None,
            ebitda: None,
            effective_tax_rate: None,
            revenue: None,
            net_income: None,
            basic_eps: None,
            diluted_eps: None,
            diluted_weighted_average_shares: None,
            revenue_yoy_change_percent: None,
            diluted_eps_yoy_change_percent: None,
        };

        assert!(is_clearly_quarter_length(&period));
    }

    fn sample_document() -> FilingXbrlDocument {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:us-gaap="http://fasb.org/us-gaap/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217">
  <xbrli:context id="quarter"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2025-01-27</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:context id="ytd"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2024-10-28</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="quarter">2026</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="quarter">Q1</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="quarter">2025-04-27</dei:DocumentPeriodEndDate>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="quarter" unitRef="usd">100</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="ytd" unitRef="usd">200</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:NetIncomeLoss contextRef="quarter" unitRef="usd">25</us-gaap:NetIncomeLoss>
</xbrli:xbrl>"#;

        parse_filing_xbrl_instance(xml.as_bytes()).expect("document should parse")
    }

    fn custom_only_document() -> FilingXbrlDocument {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:custom="http://example.com/custom/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217">
  <xbrli:context id="quarter"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2025-01-27</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="quarter">2026</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="quarter">Q1</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="quarter">2025-04-27</dei:DocumentPeriodEndDate>
  <custom:AlternativeRevenue contextRef="quarter" unitRef="usd">123</custom:AlternativeRevenue>
</xbrli:xbrl>"#;

        parse_filing_xbrl_instance(xml.as_bytes()).expect("document should parse")
    }

    fn custom_cost_of_sales_document() -> FilingXbrlDocument {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:us-gaap="http://fasb.org/us-gaap/2024" xmlns:custom="http://example.com/custom/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217">
  <xbrli:context id="quarter"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2025-01-27</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="quarter">2026</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="quarter">Q1</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="quarter">2025-04-27</dei:DocumentPeriodEndDate>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="quarter" unitRef="usd">100</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:NetIncomeLoss contextRef="quarter" unitRef="usd">25</us-gaap:NetIncomeLoss>
  <custom:AlternativeCostOfSales contextRef="quarter" unitRef="usd">55</custom:AlternativeCostOfSales>
</xbrli:xbrl>"#;

        parse_filing_xbrl_instance(xml.as_bytes()).expect("document should parse")
    }

    fn custom_other_operating_expenses_document() -> FilingXbrlDocument {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:us-gaap="http://fasb.org/us-gaap/2024" xmlns:custom="http://example.com/custom/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217">
  <xbrli:context id="quarter"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2025-01-27</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="quarter">2026</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="quarter">Q1</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="quarter">2025-04-27</dei:DocumentPeriodEndDate>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="quarter" unitRef="usd">100</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:NetIncomeLoss contextRef="quarter" unitRef="usd">25</us-gaap:NetIncomeLoss>
  <custom:AlternativeOtherOperatingExpenses contextRef="quarter" unitRef="usd">12</custom:AlternativeOtherOperatingExpenses>
</xbrli:xbrl>"#;

        parse_filing_xbrl_instance(xml.as_bytes()).expect("document should parse")
    }

    fn custom_total_shares_document() -> FilingXbrlDocument {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:us-gaap="http://fasb.org/us-gaap/2024" xmlns:custom="http://example.com/custom/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217" xmlns:shares="http://www.xbrl.org/2003/instance">
  <xbrli:context id="quarter"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2025-01-27</xbrli:startDate><xbrli:endDate>2025-04-27</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:context id="instant"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:instant>2025-04-27</xbrli:instant></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <xbrli:unit id="shares"><xbrli:measure>xbrli:shares</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="quarter">2026</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="quarter">Q1</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="quarter">2025-04-27</dei:DocumentPeriodEndDate>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="quarter" unitRef="usd">100</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:NetIncomeLoss contextRef="quarter" unitRef="usd">25</us-gaap:NetIncomeLoss>
  <custom:AlternativeSharesOutstanding contextRef="instant" unitRef="shares">101</custom:AlternativeSharesOutstanding>
</xbrli:xbrl>"#;

        parse_filing_xbrl_instance(xml.as_bytes()).expect("document should parse")
    }

    fn sample_filing_ref() -> FilingRef {
        FilingRef {
            accession_number: "0001".to_string(),
            filing_date: "2025-05-30".to_string(),
            report_date: Some("2025-04-27".to_string()),
            form: "10-Q".to_string(),
            primary_document: None,
        }
    }

    fn sample_identity() -> PeriodIdentity {
        PeriodIdentity {
            key: EarningsPeriodKey::Quarterly {
                fiscal_year: 2026,
                quarter: 1,
            },
            fiscal_year: "2026".to_string(),
            fiscal_period: Some("Q1".to_string()),
            period_end: "2025-04-27".to_string(),
            display_label: "FY2026 Q1 (Apr)".to_string(),
        }
    }

    #[test]
    fn calculate_gross_profit_margin_should_return_correct_percentage() {
        // Normal case: 68.8% margin (like MSFT fiscal data)
        let result = calculate_gross_profit_margin(Some(193_893.0), Some(281_724.0));
        assert_eq!(result.unwrap().round(), 69.0); // 68.83% rounds to 69%

        // Zero revenue should return None
        assert_eq!(calculate_gross_profit_margin(Some(100.0), Some(0.0)), None);
        assert_eq!(calculate_gross_profit_margin(Some(100.0), None), None);

        // Negative gross profit (loss) should still calculate
        assert!(calculate_gross_profit_margin(Some(-50.0), Some(100.0)).unwrap() < 0.0);
    }

    #[test]
    fn calculate_operating_margin_should_return_correct_percentage() {
        // Normal case: 45.6% margin (like MSFT fiscal data)
        let result = calculate_operating_margin(Some(128_528.0), Some(281_724.0));
        assert_eq!(result.unwrap().round(), 46.0); // 45.62% rounds to 46%

        // Zero revenue should return None
        assert_eq!(calculate_operating_margin(Some(100.0), Some(0.0)), None);

        // Negative operating profit should still calculate
        assert!(calculate_operating_margin(Some(-20.0), Some(100.0)).unwrap() < 0.0);
    }

    #[test]
    fn calculate_other_operating_expenses_should_derive_correctly() {
        // Case: operating_profit = 100, gross_profit = 200, sga = 50, rd = 30
        // Formula: GP - OpProfit - SGA - R&D = 200 - 100 - 50 - 30 = 20
        // Verify: GP - SGA - R&D - Other = 200 - 50 - 30 - 20 = 100 = OpProfit ✓
        assert_eq!(
            calculate_other_operating_expenses(Some(100.0), Some(200.0), Some(50.0), Some(30.0)),
            Some(20.0)
        );

        // Missing SGA/R&D should default to 0
        // Formula: 200 - 100 - 0 - 0 = 100
        assert_eq!(
            calculate_other_operating_expenses(Some(100.0), Some(200.0), None, None),
            Some(100.0)
        );

        // Near-zero result should return Some(0.0)
        let near_zero = calculate_other_operating_expenses(
            Some(100.0),
            Some(100.0),
            Some(f64::EPSILON / 2.0),
            Some(0.0),
        );
        assert_eq!(near_zero, Some(0.0));

        // Missing required components should return None
        assert_eq!(
            calculate_other_operating_expenses(None, Some(200.0), Some(50.0), Some(30.0)),
            None
        );

        // NIO-like case: negative operating profit
        // OpProfit = -14.04, GP = 11.92, SGA = 16.09, R&D = 10.6
        // Formula: 11.92 - (-14.04) - 16.09 - 10.6 = -0.73
        // Verify: GP - SGA - R&D - Other = 11.92 - 16.09 - 10.6 - (-0.73) = -14.04 ✓
        let nio_result =
            calculate_other_operating_expenses(Some(-14.04), Some(11.92), Some(16.09), Some(10.6));
        assert!(nio_result.is_some());
        assert!((nio_result.unwrap() - (-0.73)).abs() < 0.01);
    }
}
