use std::fmt::{Display, Formatter};
use std::sync::Arc;

use futures::future::BoxFuture;

use crate::mappings::MappingsService;
use crate::terminal::{
    CashFlowPanelData, DividendsPanelData, EarningsPanelData, FilingRef, FinancialsPanelData,
    Frequency,
};

use super::client::SecEdgarClient;
use super::earnings::{build_panel_data as build_earnings_panel_data, load_earnings_data};
use super::facts::{
    build_cash_flow_periods, build_dividend_events, build_source_status, build_statement_periods,
    company_name, limit_dividend_ttm, normalize_all_facts, select_latest_filing,
};
use super::types::{CompanySubmissions, EarningsQuery};
use super::xbrl::pick_instance_document;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum EdgarLookupError {
    MissingUserAgent,
    UnknownTicker {
        ticker: String,
    },
    NoFactsAvailable,
    NoEligibleFilings {
        ticker: String,
        frequency: Frequency,
    },
    InvalidPeriodRange {
        detail: String,
    },
    RequestFailed {
        provider: &'static str,
        detail: String,
    },
    InvalidResponse {
        provider: &'static str,
        detail: String,
    },
    XbrlParseFailed {
        detail: String,
    },
}

impl Display for EdgarLookupError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MissingUserAgent => formatter.write_str(
                "Set SEC_EDGAR_USER_AGENT to a value like `MosaicIQ admin@example.com`.",
            ),
            Self::UnknownTicker { ticker } => {
                write!(formatter, "No SEC CIK mapping found for {ticker}.")
            }
            Self::NoFactsAvailable => {
                formatter.write_str("SEC companyfacts did not contain matching disclosures.")
            }
            Self::InvalidPeriodRange { detail } => formatter.write_str(detail),
            Self::NoEligibleFilings { ticker, frequency } => write!(
                formatter,
                "No eligible {} filings were found for {ticker}.",
                frequency.as_str()
            ),
            Self::RequestFailed { detail, .. }
            | Self::InvalidResponse { detail, .. }
            | Self::XbrlParseFailed { detail } => formatter.write_str(detail),
        }
    }
}

pub(crate) trait EdgarDataLookup: Send + Sync {
    fn financials<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<FinancialsPanelData, EdgarLookupError>>;

    fn cash_flow<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<CashFlowPanelData, EdgarLookupError>>;

    fn dividends<'a>(
        &'a self,
        ticker: &'a str,
    ) -> BoxFuture<'a, Result<DividendsPanelData, EdgarLookupError>>;

    fn earnings<'a>(
        &'a self,
        ticker: &'a str,
        query: EarningsQuery,
    ) -> BoxFuture<'a, Result<EarningsPanelData, EdgarLookupError>>;
}

pub(crate) struct SecEdgarLookup {
    client: Arc<SecEdgarClient>,
    mappings_service: Arc<MappingsService>,
}

impl SecEdgarLookup {
    pub(crate) fn new(client: Arc<SecEdgarClient>, mappings_service: Arc<MappingsService>) -> Self {
        Self {
            client,
            mappings_service,
        }
    }

    pub(crate) async fn load_concepts_for_ticker(
        &self,
        ticker: &str,
    ) -> Result<Vec<crate::terminal::types::AvailableXbrlConcept>, EdgarLookupError> {
        super::earnings::load_concepts_for_ticker(&self.client, ticker).await
    }

    async fn context_for(
        &self,
        ticker: &str,
        frequency: Frequency,
    ) -> Result<LookupContext, EdgarLookupError> {
        let company = self.client.resolve_company(ticker).await?;
        let submissions = self.client.load_submissions(&company.cik).await?;
        let companyfacts = self.client.load_companyfacts(&company.cik).await?;
        let latest_filing = select_latest_filing(&submissions.filings.recent.rows(), frequency)
            .ok_or_else(|| EdgarLookupError::NoEligibleFilings {
                ticker: company.ticker.clone(),
                frequency,
            })?;

        Ok(LookupContext {
            company,
            submissions,
            companyfacts,
            latest_filing,
        })
    }

    async fn latest_xbrl(
        &self,
        cik: &str,
        filing: &FilingRef,
    ) -> Result<Option<super::types::ParsedXbrlDocument>, EdgarLookupError> {
        let index = self
            .client
            .load_filing_index(cik, &filing.accession_number)
            .await?;
        let filenames = index
            .directory
            .item
            .into_iter()
            .map(|item| item.name)
            .collect::<Vec<_>>();
        let Some(filename) = pick_instance_document(&filenames) else {
            return Ok(None);
        };
        let parsed = self
            .client
            .load_parsed_xbrl(cik, &filing.accession_number, &filename)
            .await?;
        Ok(Some(parsed))
    }
}

impl EdgarDataLookup for SecEdgarLookup {
    fn financials<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<FinancialsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, frequency).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let periods = build_statement_periods(
                &facts,
                frequency,
                4,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );

            Ok(FinancialsPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                frequency,
                periods,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn cash_flow<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<CashFlowPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, frequency).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let periods = build_cash_flow_periods(
                &facts,
                frequency,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );

            Ok(CashFlowPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                frequency,
                periods,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn dividends<'a>(
        &'a self,
        ticker: &'a str,
    ) -> BoxFuture<'a, Result<DividendsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, Frequency::Quarterly).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let events = build_dividend_events(
                &facts,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );
            let (ttm_dividends_per_share, ttm_common_dividends_paid) = limit_dividend_ttm(&events);

            Ok(DividendsPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                ttm_dividends_per_share,
                ttm_common_dividends_paid,
                latest_event: events.first().cloned(),
                events,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn earnings<'a>(
        &'a self,
        ticker: &'a str,
        query: EarningsQuery,
    ) -> BoxFuture<'a, Result<EarningsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let company = self.client.resolve_company(ticker).await?;
            let submissions = self.client.load_submissions(&company.cik).await?;
            let custom_mappings = self
                .mappings_service
                .list_mappings()
                .await
                .map_err(|error| EdgarLookupError::InvalidResponse {
                    provider: "custom mappings",
                    detail: error.to_string(),
                })?;
            let data = load_earnings_data(
                &self.client,
                &company,
                &submissions,
                &query,
                &custom_mappings,
            )
            .await?;
            Ok(build_earnings_panel_data(
                &company,
                &submissions,
                &query,
                data,
            ))
        })
    }
}

struct LookupContext {
    company: super::types::ResolvedCompany,
    submissions: CompanySubmissions,
    companyfacts: super::types::CompanyFactsResponse,
    latest_filing: FilingRef,
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;

    use futures::future::BoxFuture;
    use tempfile::TempDir;

    use super::{EdgarDataLookup, EdgarLookupError, SecEdgarLookup};
    use crate::mappings::{
        CustomConcept, CustomConceptMapping, MappingScope, MappingsService, MappingsStorage,
        TargetField,
    };
    use crate::terminal::sec_edgar::client::{SecEdgarClient, SecFetch};
    use crate::terminal::sec_edgar::{EarningsPeriodKey, EarningsQuery, EarningsRange, UnitFamily};
    use crate::terminal::Frequency;

    const TICKERS_URL: &str = "https://www.sec.gov/files/company_tickers.json";

    struct FixtureFetcher {
        text: HashMap<String, String>,
        bytes: HashMap<String, Vec<u8>>,
    }

    impl SecFetch for FixtureFetcher {
        fn get_text<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<String, EdgarLookupError>> {
            Box::pin(async move {
                self.text
                    .get(url)
                    .cloned()
                    .ok_or_else(|| EdgarLookupError::RequestFailed {
                        provider: "SEC EDGAR",
                        detail: format!("missing fixture for {url}"),
                    })
            })
        }

        fn get_bytes<'a>(
            &'a self,
            url: &'a str,
        ) -> BoxFuture<'a, Result<Vec<u8>, EdgarLookupError>> {
            Box::pin(async move {
                self.bytes
                    .get(url)
                    .cloned()
                    .ok_or_else(|| EdgarLookupError::RequestFailed {
                        provider: "SEC EDGAR",
                        detail: format!("missing fixture for {url}"),
                    })
            })
        }
    }

    fn create_mappings_service() -> (Arc<MappingsService>, TempDir) {
        let temp_dir = tempfile::tempdir().unwrap();
        let storage = MappingsStorage::new(temp_dir.path().to_path_buf()).unwrap();
        let service = Arc::new(MappingsService::new(storage).unwrap());
        (service, temp_dir)
    }

    fn lookup() -> SecEdgarLookup {
        let mut text = HashMap::new();
        let mut bytes = HashMap::new();
        let (mappings_service, _temp_dir) = create_mappings_service();

        text.insert(
            TICKERS_URL.to_string(),
            include_str!("../../../tests/fixtures/sec/company_tickers.json").to_string(),
        );

        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000320193",
                accession_number: "0000320193-24-000123",
                submissions: include_str!("../../../tests/fixtures/sec/aapl/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/aapl/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/aapl/index_annual.json"),
                xml_name: "aapl-20240928_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/aapl/instance_annual.xml"),
            },
        );
        text.insert(
            "https://www.sec.gov/Archives/edgar/data/320193/000032019325000010/index.json"
                .to_string(),
            include_str!("../../../tests/fixtures/sec/aapl/index_quarterly.json").to_string(),
        );
        bytes.insert(
            "https://www.sec.gov/Archives/edgar/data/320193/000032019325000010/aapl-20241228_htm.xml".to_string(),
            include_bytes!("../../../tests/fixtures/sec/aapl/instance_quarterly.xml").to_vec(),
        );

        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000789019",
                accession_number: "0000950170-24-087843",
                submissions: include_str!("../../../tests/fixtures/sec/msft/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/msft/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/msft/index.json"),
                xml_name: "msft-20240630_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/msft/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000021344",
                accession_number: "0000021344-25-000010",
                submissions: include_str!("../../../tests/fixtures/sec/ko/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/ko/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/ko/index.json"),
                xml_name: "ko-20250328_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/ko/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0001045810",
                accession_number: "0001045810-25-000020",
                submissions: include_str!("../../../tests/fixtures/sec/nvda/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/nvda/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/nvda/index.json"),
                xml_name: "nvda-20250427_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/nvda/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0001000184",
                accession_number: "0001000184-24-000001",
                submissions: include_str!("../../../tests/fixtures/sec/sap/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/sap/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/sap/index.json"),
                xml_name: "sap-20231231_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/sap/instance.xml"),
            },
        );

        SecEdgarLookup::new(
            Arc::new(SecEdgarClient::new(Box::new(FixtureFetcher {
                text,
                bytes,
            }))),
            mappings_service,
        )
    }

    fn earnings_lookup() -> SecEdgarLookup {
        let (mappings_service, _temp_dir) = create_mappings_service();
        earnings_lookup_with_mappings(mappings_service)
    }

    fn earnings_lookup_with_mappings(mappings_service: Arc<MappingsService>) -> SecEdgarLookup {
        let mut text = HashMap::new();
        let mut bytes = HashMap::new();

        text.insert(
            TICKERS_URL.to_string(),
            serde_json::json!({
                "0": {
                    "cik_str": 1234567,
                    "ticker": "EARN",
                    "title": "Earnings Annual Inc."
                },
                "1": {
                    "cik_str": 7654321,
                    "ticker": "QEM",
                    "title": "Quarterly Earnings Inc."
                },
                "2": {
                    "cik_str": 2468135,
                    "ticker": "CMAP",
                    "title": "Custom Mapping Earnings Inc."
                }
            })
            .to_string(),
        );

        add_earnings_company(
            &mut text,
            &mut bytes,
            "0001234567",
            "Earnings Annual Inc.",
            &annual_earnings_filings(),
        );
        add_earnings_company(
            &mut text,
            &mut bytes,
            "0007654321",
            "Quarterly Earnings Inc.",
            &quarterly_earnings_filings(),
        );
        add_earnings_company(
            &mut text,
            &mut bytes,
            "0002468135",
            "Custom Mapping Earnings Inc.",
            &custom_mapped_earnings_filings(),
        );

        SecEdgarLookup::new(
            Arc::new(SecEdgarClient::new(Box::new(FixtureFetcher {
                text,
                bytes,
            }))),
            mappings_service,
        )
    }

    struct CompanyFixture<'a> {
        cik: &'a str,
        accession_number: &'a str,
        submissions: &'a str,
        companyfacts: &'a str,
        index: &'a str,
        xml_name: &'a str,
        xml: &'a str,
    }

    fn add_company(
        text: &mut HashMap<String, String>,
        bytes: &mut HashMap<String, Vec<u8>>,
        fixture: CompanyFixture<'_>,
    ) {
        text.insert(
            format!("https://data.sec.gov/submissions/CIK{}.json", fixture.cik),
            fixture.submissions.to_string(),
        );
        text.insert(
            format!(
                "https://data.sec.gov/api/xbrl/companyfacts/CIK{}.json",
                fixture.cik
            ),
            fixture.companyfacts.to_string(),
        );
        text.insert(
            format!(
                "https://www.sec.gov/Archives/edgar/data/{}/{}/index.json",
                fixture.cik.trim_start_matches('0'),
                fixture.accession_number.replace('-', "")
            ),
            fixture.index.to_string(),
        );
        bytes.insert(
            format!(
                "https://www.sec.gov/Archives/edgar/data/{}/{}/{}",
                fixture.cik.trim_start_matches('0'),
                fixture.accession_number.replace('-', ""),
                fixture.xml_name
            ),
            fixture.xml.as_bytes().to_vec(),
        );
    }

    #[derive(Clone)]
    struct SyntheticEarningsFiling {
        accession_number: &'static str,
        filing_date: &'static str,
        report_date: &'static str,
        form: &'static str,
        primary_document: &'static str,
        xml_name: &'static str,
        xml: String,
    }

    fn add_earnings_company(
        text: &mut HashMap<String, String>,
        bytes: &mut HashMap<String, Vec<u8>>,
        cik: &str,
        name: &str,
        filings: &[SyntheticEarningsFiling],
    ) {
        text.insert(
            format!("https://data.sec.gov/submissions/CIK{cik}.json"),
            serde_json::json!({
                "name": name,
                "filings": {
                    "recent": {
                        "accessionNumber": filings.iter().map(|filing| filing.accession_number).collect::<Vec<_>>(),
                        "filingDate": filings.iter().map(|filing| filing.filing_date).collect::<Vec<_>>(),
                        "reportDate": filings.iter().map(|filing| Some(filing.report_date)).collect::<Vec<_>>(),
                        "form": filings.iter().map(|filing| filing.form).collect::<Vec<_>>(),
                        "primaryDocument": filings.iter().map(|filing| filing.primary_document).collect::<Vec<_>>()
                    },
                    "files": []
                }
            })
            .to_string(),
        );

        for filing in filings {
            text.insert(
                format!(
                    "https://www.sec.gov/Archives/edgar/data/{}/{}/index.json",
                    cik.trim_start_matches('0'),
                    filing.accession_number.replace('-', "")
                ),
                serde_json::json!({
                    "directory": {
                        "item": [
                            {
                                "name": filing.xml_name
                            }
                        ]
                    }
                })
                .to_string(),
            );
            bytes.insert(
                format!(
                    "https://www.sec.gov/Archives/edgar/data/{}/{}/{}",
                    cik.trim_start_matches('0'),
                    filing.accession_number.replace('-', ""),
                    filing.xml_name
                ),
                filing.xml.as_bytes().to_vec(),
            );
        }
    }

    fn annual_earnings_filings() -> Vec<SyntheticEarningsFiling> {
        vec![
            synthetic_annual_filing(
                "0001234567-24-000004",
                "2024-11-01",
                "2024-09-28",
                2024,
                "2023-09-30",
                "2024-09-28",
                300.0,
                90.0,
                120.0,
                3.0,
            ),
            synthetic_annual_filing(
                "0001234567-23-000003",
                "2023-11-03",
                "2023-09-30",
                2023,
                "2022-10-01",
                "2023-09-30",
                280.0,
                84.0,
                110.0,
                2.8,
            ),
            synthetic_annual_filing(
                "0001234567-22-000002",
                "2022-11-04",
                "2022-09-24",
                2022,
                "2021-09-26",
                "2022-09-24",
                260.0,
                78.0,
                100.0,
                2.6,
            ),
            synthetic_annual_filing(
                "0001234567-21-000001",
                "2021-10-29",
                "2021-09-25",
                2021,
                "2020-09-27",
                "2021-09-25",
                240.0,
                72.0,
                96.0,
                2.4,
            ),
        ]
    }

    fn quarterly_earnings_filings() -> Vec<SyntheticEarningsFiling> {
        vec![
            synthetic_quarterly_filing(
                "0007654321-25-000008",
                "2025-05-30",
                "2025-04-27",
                2026,
                "Q1",
                "2025-01-27",
                "2025-04-27",
                Some("2024-10-28"),
                100.0,
                30.0,
                40.0,
                1.0,
            ),
            synthetic_quarterly_filing(
                "0007654321-25-000007",
                "2025-02-28",
                "2025-01-26",
                2025,
                "Q4",
                "2024-10-28",
                "2025-01-26",
                Some("2024-01-29"),
                92.0,
                28.0,
                36.0,
                0.92,
            ),
            synthetic_quarterly_filing(
                "0007654321-24-000006",
                "2024-11-27",
                "2024-10-27",
                2025,
                "Q3",
                "2024-07-29",
                "2024-10-27",
                Some("2024-01-29"),
                84.0,
                24.0,
                32.0,
                0.84,
            ),
            synthetic_quarterly_filing(
                "0007654321-24-000005",
                "2024-08-28",
                "2024-07-28",
                2025,
                "Q2",
                "2024-04-29",
                "2024-07-28",
                Some("2024-01-29"),
                76.0,
                22.0,
                29.0,
                0.76,
            ),
            synthetic_quarterly_filing(
                "0007654321-24-000004",
                "2024-05-29",
                "2024-04-28",
                2025,
                "Q1",
                "2024-01-29",
                "2024-04-28",
                Some("2023-10-30"),
                25.0,
                7.0,
                10.0,
                0.25,
            ),
            synthetic_quarterly_filing(
                "0007654321-24-000003",
                "2024-02-28",
                "2024-01-28",
                2024,
                "Q4",
                "2023-10-30",
                "2024-01-28",
                Some("2023-01-30"),
                23.0,
                6.5,
                9.0,
                0.23,
            ),
            synthetic_quarterly_filing(
                "0007654321-23-000002",
                "2023-11-29",
                "2023-10-29",
                2024,
                "Q3",
                "2023-07-31",
                "2023-10-29",
                Some("2023-01-30"),
                21.0,
                6.0,
                8.0,
                0.21,
            ),
            synthetic_quarterly_filing(
                "0007654321-23-000001",
                "2023-08-30",
                "2023-07-30",
                2024,
                "Q2",
                "2023-05-01",
                "2023-07-30",
                Some("2023-01-30"),
                19.0,
                5.0,
                7.0,
                0.19,
            ),
        ]
    }

    fn custom_mapped_earnings_filings() -> Vec<SyntheticEarningsFiling> {
        vec![synthetic_custom_annual_filing(
            "0002468135-24-000001",
            "2024-11-05",
            "2024-09-30",
            2024,
            "2023-10-01",
            "2024-09-30",
            555.0,
        )]
    }

    #[expect(
        clippy::too_many_arguments,
        reason = "test fixture builder mirrors filing fields"
    )]
    fn synthetic_annual_filing(
        accession_number: &'static str,
        filing_date: &'static str,
        report_date: &'static str,
        fiscal_year: i32,
        period_start: &'static str,
        period_end: &'static str,
        revenue: f64,
        net_income: f64,
        operating_profit: f64,
        diluted_eps: f64,
    ) -> SyntheticEarningsFiling {
        SyntheticEarningsFiling {
            accession_number,
            filing_date,
            report_date,
            form: "10-K",
            primary_document: "earn-annual.htm",
            xml_name: "earn-annual_htm.xml",
            xml: synthetic_earnings_instance(
                fiscal_year,
                "FY",
                period_start,
                period_end,
                None,
                revenue,
                net_income,
                operating_profit,
                diluted_eps,
            ),
        }
    }

    #[expect(
        clippy::too_many_arguments,
        reason = "test fixture builder mirrors filing fields"
    )]
    fn synthetic_quarterly_filing(
        accession_number: &'static str,
        filing_date: &'static str,
        report_date: &'static str,
        fiscal_year: i32,
        fiscal_period: &'static str,
        period_start: &'static str,
        period_end: &'static str,
        ytd_start: Option<&'static str>,
        revenue: f64,
        net_income: f64,
        operating_profit: f64,
        diluted_eps: f64,
    ) -> SyntheticEarningsFiling {
        SyntheticEarningsFiling {
            accession_number,
            filing_date,
            report_date,
            form: "10-Q",
            primary_document: "earn-quarter.htm",
            xml_name: "earn-quarter_htm.xml",
            xml: synthetic_earnings_instance(
                fiscal_year,
                fiscal_period,
                period_start,
                period_end,
                ytd_start,
                revenue,
                net_income,
                operating_profit,
                diluted_eps,
            ),
        }
    }

    #[expect(
        clippy::too_many_arguments,
        reason = "test fixture builder mirrors filing facts"
    )]
    fn synthetic_earnings_instance(
        fiscal_year: i32,
        fiscal_period: &str,
        period_start: &str,
        period_end: &str,
        ytd_start: Option<&str>,
        revenue: f64,
        net_income: f64,
        operating_profit: f64,
        diluted_eps: f64,
    ) -> String {
        let ytd_context = ytd_start.map(|ytd_start| {
            format!(
                r#"<xbrli:context id="ytd"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>{ytd_start}</xbrli:startDate><xbrli:endDate>{period_end}</xbrli:endDate></xbrli:period></xbrli:context>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="ytd" unitRef="usd">{}</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:NetIncomeLoss contextRef="ytd" unitRef="usd">{}</us-gaap:NetIncomeLoss>"#,
                revenue * 2.0,
                net_income * 2.0
            )
        }).unwrap_or_default();

        format!(
            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="duration"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>{period_start}</xbrli:startDate><xbrli:endDate>{period_end}</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>{period_end}</xbrli:instant></xbrli:period></xbrli:context>
  {ytd_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>
  <xbrli:unit id="pure"><xbrli:measure>xbrli:pure</xbrli:measure></xbrli:unit>
  <xbrli:unit id="usdPerShare"><xbrli:divide><xbrli:unitNumerator><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unitNumerator><xbrli:unitDenominator><xbrli:measure>xbrli:shares</xbrli:measure></xbrli:unitDenominator></xbrli:divide></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="duration">{fiscal_year}</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="duration">{fiscal_period}</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="duration">{period_end}</dei:DocumentPeriodEndDate>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="duration" unitRef="usd">{revenue}</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
  <us-gaap:OperatingIncomeLoss contextRef="duration" unitRef="usd">{operating_profit}</us-gaap:OperatingIncomeLoss>
  <us-gaap:NetIncomeLoss contextRef="duration" unitRef="usd">{net_income}</us-gaap:NetIncomeLoss>
  <us-gaap:EarningsPerShareBasic contextRef="duration" unitRef="usdPerShare">{diluted_eps}</us-gaap:EarningsPerShareBasic>
  <us-gaap:EarningsPerShareDiluted contextRef="duration" unitRef="usdPerShare">{diluted_eps}</us-gaap:EarningsPerShareDiluted>
  <us-gaap:WeightedAverageNumberOfSharesOutstandingBasic contextRef="duration" unitRef="shares">100</us-gaap:WeightedAverageNumberOfSharesOutstandingBasic>
  <us-gaap:WeightedAverageNumberOfDilutedSharesOutstanding contextRef="duration" unitRef="shares">100</us-gaap:WeightedAverageNumberOfDilutedSharesOutstanding>
  <dei:EntityCommonStockSharesOutstanding contextRef="instant" unitRef="shares">101</dei:EntityCommonStockSharesOutstanding>
  <us-gaap:EffectiveIncomeTaxRateContinuingOperations contextRef="duration" unitRef="pure">0.21</us-gaap:EffectiveIncomeTaxRateContinuingOperations>
</xbrli:xbrl>"#
        )
    }

    fn synthetic_custom_annual_filing(
        accession_number: &'static str,
        filing_date: &'static str,
        report_date: &'static str,
        fiscal_year: i32,
        period_start: &'static str,
        period_end: &'static str,
        revenue: f64,
    ) -> SyntheticEarningsFiling {
        SyntheticEarningsFiling {
            accession_number,
            filing_date,
            report_date,
            form: "10-K",
            primary_document: "earn-custom.htm",
            xml_name: "earn-custom_htm.xml",
            xml: synthetic_custom_earnings_instance(fiscal_year, period_start, period_end, revenue),
        }
    }

    fn synthetic_custom_earnings_instance(
        fiscal_year: i32,
        period_start: &str,
        period_end: &str,
        revenue: f64,
    ) -> String {
        format!(
            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="duration"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>{period_start}</xbrli:startDate><xbrli:endDate>{period_end}</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="duration">{fiscal_year}</dei:DocumentFiscalYearFocus>
  <dei:DocumentFiscalPeriodFocus contextRef="duration">FY</dei:DocumentFiscalPeriodFocus>
  <dei:DocumentPeriodEndDate contextRef="duration">{period_end}</dei:DocumentPeriodEndDate>
  <custom:AlternativeRevenue contextRef="duration" unitRef="usd">{revenue}</custom:AlternativeRevenue>
</xbrli:xbrl>"#
        )
    }

    #[test]
    fn financials_returns_four_annual_periods() {
        let data = futures::executor::block_on(lookup().financials("AAPL", Frequency::Annual))
            .expect("financials should load");
        assert_eq!(data.periods.len(), 4);
        assert_eq!(data.periods[0].revenue, Some(395000000000.0));
    }

    #[test]
    fn financials_supports_quarterly_periods() {
        let data = futures::executor::block_on(lookup().financials("AAPL", Frequency::Quarterly))
            .expect("quarterly financials should load");
        assert_eq!(data.periods.len(), 4);
        assert_eq!(data.periods[0].revenue, Some(125000000000.0));
    }

    #[test]
    fn cash_flow_computes_free_cash_flow() {
        let data = futures::executor::block_on(lookup().cash_flow("MSFT", Frequency::Annual))
            .expect("cash flow should load");
        assert_eq!(data.periods[0].operating_cash_flow, Some(120000000000.0));
        assert_eq!(data.periods[0].free_cash_flow, Some(75523000000.0));
    }

    #[test]
    fn dividends_returns_history_and_ttm_summary() {
        let data =
            futures::executor::block_on(lookup().dividends("KO")).expect("dividends should load");
        assert_eq!(data.events.len(), 8);
        let ttm_dividends_per_share = data.ttm_dividends_per_share.unwrap_or_default();
        assert!((ttm_dividends_per_share - 1.985).abs() < 0.000_1);
        assert_eq!(
            data.latest_event
                .as_ref()
                .map(|value| value.frequency_guess.as_str()),
            Some("quarterly")
        );
    }

    #[test]
    fn earnings_returns_filing_native_quarterly_values() {
        let data = futures::executor::block_on(earnings_lookup().earnings(
            "QEM",
            EarningsQuery {
                frequency: Frequency::Quarterly,
                range: None,
            },
        ))
        .expect("earnings should load");
        assert_eq!(data.periods.len(), 8);
        assert_eq!(data.periods[0].revenue, Some(100.0));
        assert_eq!(data.periods[0].total_revenues, Some(100.0));
        assert_eq!(data.periods[0].operating_profit, Some(40.0));
        assert!(!data.available_periods.is_empty());
        assert!(!data.source_status.companyfacts_used);
        assert_eq!(
            data.available_periods
                .last()
                .map(|period| period.id.as_str()),
            Some("2026Q1")
        );
        assert_eq!(data.selected_period_start.as_deref(), Some("2024Q2"));
        assert_eq!(data.selected_period_end.as_deref(), Some("2026Q1"));
        assert!(
            data.periods[0]
                .revenue_yoy_change_percent
                .unwrap_or_default()
                > 299.0
        );
        assert_eq!(
            data.periods[0].revenue_yoy_change_percent,
            data.periods[0].total_revenues_yoy_change_percent
        );
        assert_eq!(data.periods[0].display_label, "FY2026 Q1 (Apr)");
    }

    #[test]
    fn earnings_supports_explicit_annual_ranges() {
        let data = futures::executor::block_on(earnings_lookup().earnings(
            "EARN",
            EarningsQuery {
                frequency: Frequency::Annual,
                range: Some(EarningsRange {
                    start: EarningsPeriodKey::Annual { fiscal_year: 2022 },
                    end: EarningsPeriodKey::Annual { fiscal_year: 2024 },
                }),
            },
        ))
        .expect("earnings should load");

        assert_eq!(data.frequency, Frequency::Annual);
        assert_eq!(data.periods.len(), 3);
        assert_eq!(data.selected_period_start.as_deref(), Some("2022"));
        assert_eq!(data.selected_period_end.as_deref(), Some("2024"));
        assert_eq!(data.periods[0].display_label, "FY2024 (Sep)");
        assert_eq!(data.periods[0].period_end, "2024-09-28");
    }

    #[test]
    fn earnings_applies_custom_revenue_mapping() {
        let (mappings_service, _temp_dir) = create_mappings_service();
        futures::executor::block_on(mappings_service.add_mapping(
            CustomConceptMapping::test_mapping(
                TargetField::TotalRevenues,
                CustomConcept {
                    taxonomy: "custom".to_string(),
                    concept: "AlternativeRevenue".to_string(),
                    unit_family: UnitFamily::Currency,
                },
                MappingScope {
                    ticker: Some("CMAP".to_string()),
                    period_start: None,
                    period_end: None,
                },
                None,
            ),
        ))
        .expect("custom mapping should persist");

        let data =
            futures::executor::block_on(earnings_lookup_with_mappings(mappings_service).earnings(
                "CMAP",
                EarningsQuery {
                    frequency: Frequency::Annual,
                    range: None,
                },
            ))
            .expect("earnings should load through the custom mapping");

        assert_eq!(data.periods.len(), 1);
        assert_eq!(data.periods[0].total_revenues, Some(555.0));
        assert_eq!(data.periods[0].revenue, Some(555.0));
    }

    #[test]
    fn ifrs_financials_fallback_works() {
        let data = futures::executor::block_on(lookup().financials("SAP", Frequency::Annual))
            .expect("ifrs annual financials should load");
        assert_eq!(data.periods[0].revenue, Some(35200000000.0));
        assert_eq!(data.periods[0].net_income, Some(6200000000.0));
    }
}
