use serde_json::{json, Map, Value};

use rig::completion::Message;

use crate::agent::ChatPanelContext;
use crate::error::AppError;
use crate::news::NewsArticle;
use crate::terminal::{
    BalanceSheetPanelData, CashFlowPanelData, CashFlowPeriod, Company, CompanyPricePoint,
    DividendEvent, DividendsPanelData, EarningsPanelData, EarningsPeriod, ErrorPanel,
    FilingListItem, FilingsPanelData, FinancialsPanelData, Holding, PanelPayload, Portfolio,
    SourceStatus, StatementPeriod, StockAnalysis,
};

const MAX_TEXT_FIELD_LENGTH: usize = 600;
const MAX_NEWS_ITEMS: usize = 5;
const MAX_PERIODS: usize = 4;
const MAX_DIVIDEND_EVENTS: usize = 4;
const MAX_FILING_ITEMS: usize = 10;
const MAX_HOLDINGS: usize = 25;
const PANEL_CONTEXT_INSTRUCTION: &str = "Hidden workspace panel context. This is current UI state, not a user-authored message. Use it only when relevant to the current request. Do not mention hidden context unless it directly supports the answer.";

pub(crate) fn build_panel_context_message(
    panel_context: &ChatPanelContext,
) -> Result<Message, AppError> {
    let payload = json!({
        "kind": "workspacePanelContext",
        "sourceCommand": panel_context.source_command,
        "capturedAt": panel_context.captured_at,
        "panelType": panel_type(&panel_context.panel),
        "panel": compact_panel_payload(&panel_context.panel),
    });

    let serialized = serde_json::to_string(&payload)
        .map_err(|error| AppError::PanelContext(error.to_string()))?;

    Ok(Message::system(format!(
        "{PANEL_CONTEXT_INSTRUCTION}\n{serialized}"
    )))
}

pub(crate) fn compact_panel_payload(panel: &PanelPayload) -> Value {
    match panel {
        PanelPayload::Company { data } => compact_company_panel(data),
        PanelPayload::Error { data } => compact_error_panel(data),
        PanelPayload::Portfolio { data } => compact_portfolio_panel(data),
        PanelPayload::News { data, ticker } => compact_news_panel(data, ticker.as_deref()),
        PanelPayload::Analysis { data } => compact_analysis_panel(data),
        PanelPayload::Financials { data } => compact_financials_panel(data),
        PanelPayload::BalanceSheet { data } => compact_balance_sheet_panel(data),
        PanelPayload::CashFlow { data } => compact_cash_flow_panel(data),
        PanelPayload::Dividends { data } => compact_dividends_panel(data),
        PanelPayload::Earnings { data } => compact_earnings_panel(data),
        PanelPayload::Filings { data } => compact_filings_panel(data),
    }
}

pub(crate) fn panel_type(panel: &PanelPayload) -> &'static str {
    match panel {
        PanelPayload::Company { .. } => "company",
        PanelPayload::Error { .. } => "error",
        PanelPayload::Portfolio { .. } => "portfolio",
        PanelPayload::News { .. } => "news",
        PanelPayload::Analysis { .. } => "analysis",
        PanelPayload::Financials { .. } => "financials",
        PanelPayload::BalanceSheet { .. } => "balanceSheet",
        PanelPayload::CashFlow { .. } => "cashFlow",
        PanelPayload::Dividends { .. } => "dividends",
        PanelPayload::Earnings { .. } => "earnings",
        PanelPayload::Filings { .. } => "filings",
    }
}

fn compact_company_panel(data: &Company) -> Value {
    let mut chart_range_summaries = data
        .price_chart_ranges
        .as_ref()
        .map(|ranges| {
            let mut entries = ranges.iter().collect::<Vec<_>>();
            entries.sort_by(|(left, _), (right, _)| left.cmp(right));

            entries
                .into_iter()
                .map(|(range, points)| {
                    json!({
                        "range": range,
                        "summary": summarize_chart_points(points),
                    })
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();

    if let Some(points) = data.price_chart.as_ref() {
        chart_range_summaries.push(json!({
            "range": "default",
            "summary": summarize_chart_points(points),
        }));
    }

    json!({
        "symbol": data.symbol,
        "name": data.name,
        "price": data.price,
        "change": data.change,
        "changePercent": data.change_percent,
        "marketCap": data.market_cap,
        "volume": data.volume,
        "volumeLabel": data.volume_label,
        "pe": data.pe,
        "eps": data.eps,
        "high52Week": data.high52_week,
        "low52Week": data.low52_week,
        "profile": data.profile.as_ref().map(|profile| json!({
            "description": profile.description.as_deref().map(truncate_text),
            "wikiUrl": profile.wiki_url,
            "ceo": profile.ceo.as_deref().map(truncate_text),
            "headquarters": profile.headquarters.as_deref().map(truncate_text),
            "employees": profile.employees,
            "founded": profile.founded,
            "sector": profile.sector.as_deref().map(truncate_text),
            "website": profile.website,
        })),
        "chartRangeSummaries": chart_range_summaries,
    })
}

fn compact_error_panel(data: &ErrorPanel) -> Value {
    json!({
        "title": truncate_text(&data.title),
        "message": truncate_text(&data.message),
        "detail": data.detail.as_deref().map(truncate_text),
        "provider": data.provider,
        "query": data.query.as_deref().map(truncate_text),
        "symbol": data.symbol,
    })
}

fn compact_portfolio_panel(data: &Portfolio) -> Value {
    json!({
        "summary": {
            "totalValue": data.total_value,
            "dayChange": data.day_change,
            "dayChangePercent": data.day_change_percent,
            "totalGain": data.total_gain,
            "totalGainPercent": data.total_gain_percent,
            "cashBalance": data.cash_balance,
            "investedCostBasis": data.invested_cost_basis,
            "realizedGain": data.realized_gain,
            "unrealizedGain": data.unrealized_gain,
            "holdingsCount": data.holdings_count,
            "stalePricingSymbols": data.stale_pricing_symbols,
        },
        "holdings": data
            .holdings
            .iter()
            .take(MAX_HOLDINGS)
            .map(compact_holding)
            .collect::<Vec<_>>(),
    })
}

fn compact_news_panel(data: &[NewsArticle], ticker: Option<&str>) -> Value {
    json!({
        "ticker": ticker,
        "items": data
            .iter()
            .take(MAX_NEWS_ITEMS)
            .map(|item| json!({
                "id": item.id,
                "sourceId": item.source_id,
                "source": truncate_text(&item.source),
                "headline": truncate_text(&item.headline),
                "summary": truncate_text(&item.summary),
                "url": item.url,
                "publishedAt": item.published_at,
                "publishedTs": item.published_ts,
                "sentiment": item.sentiment,
                "highlightReason": item.highlight_reason,
                "tickers": item.tickers,
                "isRead": item.is_read,
                "isSaved": item.is_saved,
            }))
            .collect::<Vec<_>>(),
    })
}

fn compact_analysis_panel(data: &StockAnalysis) -> Value {
    json!({
        "symbol": data.symbol,
        "summary": truncate_text(&data.summary),
        "sentiment": data.sentiment,
        "keyPoints": truncate_text_items(&data.key_points),
        "risks": truncate_text_items(&data.risks),
        "opportunities": truncate_text_items(&data.opportunities),
        "recommendation": data.recommendation,
        "targetPrice": data.target_price,
    })
}

fn compact_financials_panel(data: &FinancialsPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "frequency": data.frequency,
        "latestFiling": data.latest_filing,
        "sourceStatus": compact_source_status(&data.source_status),
        "periods": data
            .periods
            .iter()
            .take(MAX_PERIODS)
            .map(compact_statement_period)
            .collect::<Vec<_>>(),
    })
}

fn compact_balance_sheet_panel(data: &BalanceSheetPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "frequency": data.frequency,
        "selectedCurrency": data.selected_currency,
        "availableCurrencies": data.available_currencies,
        "latestFiling": data.latest_filing,
        "sourceStatus": compact_source_status(&data.source_status),
        "periods": data
            .periods
            .iter()
            .take(MAX_PERIODS)
            .map(compact_statement_period)
            .collect::<Vec<_>>(),
    })
}

fn compact_cash_flow_panel(data: &CashFlowPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "frequency": data.frequency,
        "latestFiling": data.latest_filing,
        "sourceStatus": compact_source_status(&data.source_status),
        "periods": data
            .periods
            .iter()
            .take(MAX_PERIODS)
            .map(compact_cash_flow_period)
            .collect::<Vec<_>>(),
    })
}

fn compact_dividends_panel(data: &DividendsPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "ttmDividendsPerShare": data.ttm_dividends_per_share,
        "ttmCommonDividendsPaid": data.ttm_common_dividends_paid,
        "latestFiling": data.latest_filing,
        "sourceStatus": compact_source_status(&data.source_status),
        "latestEvent": data.latest_event.as_ref().map(compact_dividend_event),
        "events": data
            .events
            .iter()
            .take(MAX_DIVIDEND_EVENTS)
            .map(compact_dividend_event)
            .collect::<Vec<_>>(),
    })
}

fn compact_earnings_panel(data: &EarningsPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "frequency": data.frequency,
        "availablePeriods": data.available_periods,
        "selectedPeriodStart": data.selected_period_start,
        "selectedPeriodEnd": data.selected_period_end,
        "selectedCurrency": data.selected_currency,
        "availableCurrencies": data.available_currencies,
        "latestFiling": data.latest_filing,
        "sourceStatus": compact_source_status(&data.source_status),
        "periods": data
            .periods
            .iter()
            .take(MAX_PERIODS)
            .map(compact_earnings_period)
            .collect::<Vec<_>>(),
    })
}

fn compact_filings_panel(data: &FilingsPanelData) -> Value {
    json!({
        "symbol": data.symbol,
        "companyName": data.company_name,
        "cik": data.cik,
        "page": data.page,
        "pageSize": data.page_size,
        "totalResults": data.total_results,
        "totalPages": data.total_pages,
        "query": data.query,
        "latestFiling": data.latest_filing.as_ref().map(compact_filing_item),
        "items": data
            .items
            .iter()
            .take(MAX_FILING_ITEMS)
            .map(compact_filing_item)
            .collect::<Vec<_>>(),
    })
}

fn compact_filing_item(item: &FilingListItem) -> Value {
    json!({
        "accessionNumber": item.accession_number,
        "filingDate": item.filing_date,
        "reportDate": item.report_date,
        "form": item.form,
        "primaryDocument": item.primary_document,
        "filingUrl": item.filing_url,
        "overview": truncate_text(&item.overview),
    })
}

fn compact_holding(holding: &Holding) -> Value {
    json!({
        "symbol": holding.symbol,
        "name": truncate_text(&holding.name),
        "quantity": holding.quantity,
        "avgCost": holding.avg_cost,
        "currentPrice": holding.current_price,
        "currentValue": holding.current_value,
        "gainLoss": holding.gain_loss,
        "gainLossPercent": holding.gain_loss_percent,
        "costBasis": holding.cost_basis,
        "unrealizedGain": holding.unrealized_gain,
        "latestTradeAt": holding.latest_trade_at,
    })
}

fn compact_statement_period(period: &StatementPeriod) -> Value {
    json!({
        "label": truncate_text(&period.label),
        "fiscalYear": period.fiscal_year,
        "fiscalPeriod": period.fiscal_period,
        "periodStart": period.period_start,
        "periodEnd": period.period_end,
        "filedDate": period.filed_date,
        "form": period.form,
        "revenue": period.revenue,
        "grossProfit": period.gross_profit,
        "operatingIncome": period.operating_income,
        "netIncome": period.net_income,
        "dilutedEps": period.diluted_eps,
        "cashAndEquivalents": period.cash_and_equivalents,
        "shortTermInvestments": period.short_term_investments,
        "accountsReceivableNet": period.accounts_receivable_net,
        "inventory": period.inventory,
        "otherCurrentAssets": period.other_current_assets,
        "totalCurrentAssets": period.total_current_assets,
        "propertyPlantAndEquipmentNet": period.property_plant_and_equipment_net,
        "goodwill": period.goodwill,
        "intangibleAssets": period.intangible_assets,
        "otherNonCurrentAssets": period.other_non_current_assets,
        "totalAssets": period.total_assets,
        "accountsPayable": period.accounts_payable,
        "accruedExpensesAndOtherCurrentLiabilities": period
            .accrued_expenses_and_other_current_liabilities,
        "shortTermDebt": period.short_term_debt,
        "totalCurrentLiabilities": period.total_current_liabilities,
        "longTermDebt": period.long_term_debt,
        "otherNonCurrentLiabilities": period.other_non_current_liabilities,
        "totalLiabilities": period.total_liabilities,
        "retainedEarnings": period.retained_earnings,
        "accumulatedOtherComprehensiveIncomeLoss": period
            .accumulated_other_comprehensive_income_loss,
        "totalEquity": period.total_equity,
        "sharesOutstanding": period.shares_outstanding,
        "bookValuePerShare": period.book_value_per_share,
    })
}

fn compact_cash_flow_period(period: &CashFlowPeriod) -> Value {
    let mut value = Map::new();
    value.insert("label".to_string(), json!(truncate_text(&period.label)));
    value.insert("fiscalYear".to_string(), json!(&period.fiscal_year));
    value.insert("fiscalPeriod".to_string(), json!(&period.fiscal_period));
    value.insert("periodStart".to_string(), json!(&period.period_start));
    value.insert("periodEnd".to_string(), json!(&period.period_end));
    value.insert("filedDate".to_string(), json!(&period.filed_date));
    value.insert("form".to_string(), json!(&period.form));
    value.insert("netIncome".to_string(), json!(period.net_income));
    value.insert(
        "depreciationAndAmortization".to_string(),
        json!(period.depreciation_and_amortization),
    );
    value.insert(
        "stockBasedCompensation".to_string(),
        json!(period.stock_based_compensation),
    );
    value.insert(
        "deferredIncomeTaxes".to_string(),
        json!(period.deferred_income_taxes),
    );
    value.insert(
        "impairmentCharges".to_string(),
        json!(period.impairment_charges),
    );
    value.insert(
        "lossGainOnSaleOfAssets".to_string(),
        json!(period.loss_gain_on_sale_of_assets),
    );
    value.insert(
        "otherNonCashItems".to_string(),
        json!(period.other_non_cash_items),
    );
    value.insert(
        "accountsReceivable".to_string(),
        json!(period.accounts_receivable),
    );
    value.insert("inventory".to_string(), json!(period.inventory));
    value.insert(
        "prepaidExpensesAndOtherCurrentAssets".to_string(),
        json!(period.prepaid_expenses_and_other_current_assets),
    );
    value.insert(
        "accountsPayable".to_string(),
        json!(period.accounts_payable),
    );
    value.insert(
        "accruedExpenses".to_string(),
        json!(period.accrued_expenses),
    );
    value.insert(
        "deferredRevenue".to_string(),
        json!(period.deferred_revenue),
    );
    value.insert(
        "otherWorkingCapitalChanges".to_string(),
        json!(period.other_working_capital_changes),
    );
    value.insert(
        "changeInOperatingAssets".to_string(),
        json!(period.change_in_operating_assets),
    );
    value.insert(
        "changeInOperatingLiabilities".to_string(),
        json!(period.change_in_operating_liabilities),
    );
    value.insert(
        "otherOperatingAdjustments".to_string(),
        json!(period.other_operating_adjustments),
    );
    value.insert(
        "operatingCashFlow".to_string(),
        json!(period.operating_cash_flow),
    );
    value.insert("capex".to_string(), json!(period.capex));
    value.insert(
        "proceedsFromSaleOfPpe".to_string(),
        json!(period.proceeds_from_sale_of_ppe),
    );
    value.insert(
        "purchasesOfInvestments".to_string(),
        json!(period.purchases_of_investments),
    );
    value.insert(
        "salesOfInvestments".to_string(),
        json!(period.sales_of_investments),
    );
    value.insert("acquisitions".to_string(), json!(period.acquisitions));
    value.insert(
        "otherInvestingActivities".to_string(),
        json!(period.other_investing_activities),
    );
    value.insert(
        "investingCashFlow".to_string(),
        json!(period.investing_cash_flow),
    );
    value.insert("debtIssuance".to_string(), json!(period.debt_issuance));
    value.insert("debtRepayment".to_string(), json!(period.debt_repayment));
    value.insert("equityIssuance".to_string(), json!(period.equity_issuance));
    value.insert(
        "stockRepurchases".to_string(),
        json!(period.stock_repurchases),
    );
    value.insert("dividendsPaid".to_string(), json!(period.dividends_paid));
    value.insert(
        "financeLeaseObligations".to_string(),
        json!(period.finance_lease_obligations),
    );
    value.insert(
        "otherFinancingActivities".to_string(),
        json!(period.other_financing_activities),
    );
    value.insert(
        "financingCashFlow".to_string(),
        json!(period.financing_cash_flow),
    );
    value.insert("freeCashFlow".to_string(), json!(period.free_cash_flow));
    value.insert(
        "exchangeRateEffects".to_string(),
        json!(period.exchange_rate_effects),
    );
    value.insert(
        "netIncreaseDecreaseCash".to_string(),
        json!(period.net_increase_decrease_cash),
    );
    value.insert("beginningCash".to_string(), json!(period.beginning_cash));
    value.insert("endingCash".to_string(), json!(period.ending_cash));
    value.insert(
        "cashPaidInterest".to_string(),
        json!(period.cash_paid_interest),
    );
    value.insert(
        "cashPaidIncomeTaxes".to_string(),
        json!(period.cash_paid_income_taxes),
    );
    Value::Object(value)
}

fn compact_dividend_event(event: &DividendEvent) -> Value {
    json!({
        "endDate": event.end_date,
        "filedDate": event.filed_date,
        "form": event.form,
        "frequencyGuess": truncate_text(&event.frequency_guess),
        "dividendPerShare": event.dividend_per_share,
        "totalCashDividends": event.total_cash_dividends,
    })
}

fn compact_earnings_period(period: &EarningsPeriod) -> Value {
    json!({
        "label": truncate_text(&period.label),
        "displayLabel": truncate_text(&period.display_label),
        "fiscalYear": period.fiscal_year,
        "fiscalPeriod": period.fiscal_period,
        "periodStart": period.period_start,
        "periodEnd": period.period_end,
        "filedDate": period.filed_date,
        "form": period.form,
        "totalRevenues": period.total_revenues,
        "totalRevenuesYoyChangePercent": period.total_revenues_yoy_change_percent,
        "costOfSales": period.cost_of_sales,
        "grossProfit": period.gross_profit,
        "grossProfitMargin": period.gross_profit_margin,
        "sellingGeneralAndAdministrativeExpenses": period.selling_general_and_administrative_expenses,
        "researchAndDevelopmentExpenses": period.research_and_development_expenses,
        "otherOperatingExpenses": period.other_operating_expenses,
        "operatingProfit": period.operating_profit,
        "operatingMargin": period.operating_margin,
        "nonOperatingIncome": period.non_operating_income,
        "totalNonOperatingIncome": period.total_non_operating_income,
        "incomeBeforeProvisionForIncomeTaxes": period.income_before_provision_for_income_taxes,
        "provisionForIncomeTaxes": period.provision_for_income_taxes,
        "consolidatedNetIncome": period.consolidated_net_income,
        "netIncomeAttributableToCommonShareholders": period.net_income_attributable_to_common_shareholders,
        "totalSharesOutstanding": period.total_shares_outstanding,
        "basicWeightedAverageSharesOutstanding": period.basic_weighted_average_shares_outstanding,
        "dilutedWeightedAverageSharesOutstanding": period.diluted_weighted_average_shares_outstanding,
        "ebitda": period.ebitda,
        "effectiveTaxRate": period.effective_tax_rate,
        "revenue": period.revenue,
        "netIncome": period.net_income,
        "basicEps": period.basic_eps,
        "dilutedEps": period.diluted_eps,
        "dilutedWeightedAverageShares": period.diluted_weighted_average_shares,
        "revenueYoyChangePercent": period.revenue_yoy_change_percent,
        "dilutedEpsYoyChangePercent": period.diluted_eps_yoy_change_percent,
    })
}

fn compact_source_status(source_status: &SourceStatus) -> Value {
    json!({
        "companyfactsUsed": source_status.companyfacts_used,
        "degradedReason": source_status.degraded_reason.as_deref().map(truncate_text),
    })
}

fn summarize_chart_points(points: &[CompanyPricePoint]) -> Value {
    let min_price = points.iter().map(|p| p.price).reduce(f64::min);
    let max_price = points.iter().map(|p| p.price).reduce(f64::max);
    let first_point = points.first();
    let last_point = points.last();

    json!({
        "points": points.len(),
        "startLabel": first_point.map(|point| truncate_text(&point.label)),
        "endLabel": last_point.map(|point| truncate_text(&point.label)),
        "startTimestamp": first_point.and_then(|point| point.timestamp.clone()),
        "endTimestamp": last_point.and_then(|point| point.timestamp.clone()),
        "startPrice": first_point.map(|point| point.price),
        "endPrice": last_point.map(|point| point.price),
        "minPrice": min_price,
        "maxPrice": max_price,
    })
}

fn truncate_text_items(items: &[String]) -> Vec<String> {
    items.iter().map(|item| truncate_text(item)).collect()
}

fn truncate_text(text: &str) -> String {
    if text.chars().count() <= MAX_TEXT_FIELD_LENGTH {
        return text.to_string();
    }

    let mut truncated = text
        .chars()
        .take(MAX_TEXT_FIELD_LENGTH.saturating_sub(3))
        .collect::<String>();
    if MAX_TEXT_FIELD_LENGTH >= 3 {
        truncated.push_str("...");
    }
    truncated
}

#[cfg(test)]
mod tests {
    use serde_json::Value;

    use super::{build_panel_context_message, compact_panel_payload, truncate_text};
    use crate::agent::ChatPanelContext;
    use crate::news::types::{HighlightReason, NewsArticle, NewsSentiment};
    use crate::terminal::{
        BalanceSheetPanelData, CashFlowPanelData, CashFlowPeriod, Company, CompanyProfile,
        DividendEvent, DividendsPanelData, EarningsPanelData, EarningsPeriod, ErrorPanel,
        FilingRef, FinancialsPanelData, Frequency, Holding, PanelPayload, Portfolio, SourceStatus,
        StatementPeriod, StockAnalysis,
    };

    #[test]
    fn company_context_summarizes_chart_data_without_raw_points() {
        let value = compact_panel_payload(&PanelPayload::Company {
            data: sample_company(),
        });

        assert_eq!(value["symbol"], "AAPL");
        assert!(value["chartRangeSummaries"].is_array());
        assert!(value.get("priceChart").is_none());
        assert!(value.to_string().contains("\"range\":\"1D\""));
        assert!(!value.to_string().contains("\"priceChartRanges\""));
    }

    #[test]
    fn news_context_caps_items_and_truncates_text() {
        let value = compact_panel_payload(&PanelPayload::News {
            data: (0..6).map(sample_news_item).collect(),
            ticker: Some("AAPL".to_string()),
        });

        let items = value["items"].as_array().unwrap();
        assert_eq!(items.len(), 5);
        assert!(items[0]["summary"].as_str().unwrap().len() <= 603);
    }

    #[test]
    fn analysis_context_keeps_key_fields() {
        let value = compact_panel_payload(&PanelPayload::Analysis {
            data: sample_analysis(),
        });

        assert_eq!(value["symbol"], "AAPL");
        assert_eq!(value["recommendation"], "buy");
        assert_eq!(value["keyPoints"].as_array().unwrap().len(), 2);
    }

    #[test]
    fn financial_panels_keep_only_recent_periods() {
        let financials = compact_panel_payload(&PanelPayload::Financials {
            data: sample_financials(),
        });
        let balance_sheet = compact_panel_payload(&PanelPayload::BalanceSheet {
            data: sample_balance_sheet(),
        });
        let cash_flow = compact_panel_payload(&PanelPayload::CashFlow {
            data: sample_cash_flow(),
        });
        let earnings = compact_panel_payload(&PanelPayload::Earnings {
            data: sample_earnings(),
        });

        assert_eq!(financials["periods"].as_array().unwrap().len(), 4);
        assert_eq!(balance_sheet["periods"].as_array().unwrap().len(), 4);
        assert_eq!(balance_sheet["selectedCurrency"], "USD");
        assert_eq!(
            balance_sheet["availableCurrencies"]
                .as_array()
                .unwrap()
                .len(),
            2
        );
        assert_eq!(cash_flow["periods"].as_array().unwrap().len(), 4);
        assert_eq!(earnings["periods"].as_array().unwrap().len(), 4);
        assert_eq!(earnings["selectedCurrency"], "USD");
        assert_eq!(earnings["availableCurrencies"].as_array().unwrap().len(), 2);
    }

    #[test]
    fn dividends_context_keeps_latest_event_and_recent_history() {
        let value = compact_panel_payload(&PanelPayload::Dividends {
            data: sample_dividends(),
        });

        assert!(value["latestEvent"].is_object());
        assert_eq!(value["events"].as_array().unwrap().len(), 4);
    }

    #[test]
    fn earnings_context_keeps_expanded_income_statement_fields() {
        let value = compact_panel_payload(&PanelPayload::Earnings {
            data: sample_earnings(),
        });

        let latest = &value["periods"].as_array().unwrap()[0];
        assert_eq!(value["selectedPeriodStart"], "2020");
        assert_eq!(value["selectedPeriodEnd"], "2024");
        assert_eq!(value["availablePeriods"].as_array().unwrap().len(), 5);
        assert_eq!(latest["totalRevenues"], 100.0);
        assert_eq!(latest["operatingProfit"], 40.0);
        assert_eq!(latest["effectiveTaxRate"], 20.0);
    }

    #[test]
    fn portfolio_context_caps_holdings() {
        let value = compact_panel_payload(&PanelPayload::Portfolio {
            data: sample_portfolio(),
        });

        assert_eq!(value["holdings"].as_array().unwrap().len(), 25);
        assert_eq!(value["summary"]["totalValue"], 1000.0);
    }

    #[test]
    fn error_context_keeps_expected_fields() {
        let value = compact_panel_payload(&PanelPayload::Error {
            data: sample_error(),
        });

        assert_eq!(value["title"], "Lookup failed");
        assert_eq!(value["symbol"], "AAPL");
    }

    #[test]
    fn builds_hidden_system_message_with_metadata() {
        let message = build_panel_context_message(&ChatPanelContext {
            source_command: Some("/search AAPL".to_string()),
            captured_at: Some("2026-04-06T10:00:00Z".to_string()),
            panel: PanelPayload::Company {
                data: sample_company(),
            },
        })
        .unwrap();

        let Value::String(content) = serde_json::to_value(&message).unwrap()["content"].clone()
        else {
            panic!("expected string content");
        };

        assert!(content.contains("Hidden workspace panel context"));
        assert!(content.contains("\"panelType\":\"company\""));
        assert!(content.contains("\"sourceCommand\":\"/search AAPL\""));
    }

    #[test]
    fn truncate_text_appends_ellipsis_when_needed() {
        let long_text = "x".repeat(650);
        let truncated = truncate_text(&long_text);

        assert_eq!(truncated.len(), 600);
        assert!(truncated.ends_with("..."));
    }

    fn sample_company() -> Company {
        Company {
            symbol: "AAPL".to_string(),
            name: "Apple Inc.".to_string(),
            price: 200.0,
            change: 2.5,
            change_percent: 1.2,
            market_cap: 3_000_000_000_000.0,
            volume: Some(100_000),
            volume_label: Some("Volume".to_string()),
            pe: Some(30.0),
            eps: Some(6.5),
            high52_week: Some(210.0),
            low52_week: Some(150.0),
            profile: Some(CompanyProfile {
                description: Some("A".repeat(650)),
                wiki_url: Some("https://example.com".to_string()),
                ceo: Some("Tim Cook".to_string()),
                headquarters: Some("Cupertino".to_string()),
                employees: Some(10),
                founded: Some(1976),
                sector: Some("Technology".to_string()),
                website: Some("https://apple.com".to_string()),
            }),
            price_chart: Some(vec![
                sample_chart_point("Open", 195.0, Some("2026-04-06T09:30:00Z")),
                sample_chart_point("Close", 200.0, Some("2026-04-06T16:00:00Z")),
            ]),
            price_chart_ranges: Some(
                [(
                    "1D".to_string(),
                    vec![
                        sample_chart_point("09:30", 195.0, Some("2026-04-06T09:30:00Z")),
                        sample_chart_point("16:00", 200.0, Some("2026-04-06T16:00:00Z")),
                    ],
                )]
                .into_iter()
                .collect(),
            ),
        }
    }

    fn sample_news_item(index: usize) -> NewsArticle {
        NewsArticle {
            id: format!("news-{index}"),
            source_id: "source-id".to_string(),
            source: "Source".to_string(),
            headline: format!("Headline {index}"),
            summary: "S".repeat(650),
            url: Some("https://example.com/story".to_string()),
            canonical_url: Some("https://example.com/story".to_string()),
            published_at: "2026-04-06T10:00:00Z".to_string(),
            published_ts: 1_775_469_600,
            fetched_at: "2026-04-06T10:05:00Z".to_string(),
            sentiment: NewsSentiment::Bull,
            sentiment_score: 0.66,
            highlight_reason: Some(HighlightReason::TickerDetected),
            tickers: vec!["AAPL".to_string()],
            is_read: false,
            is_saved: false,
        }
    }

    fn sample_analysis() -> StockAnalysis {
        StockAnalysis {
            symbol: "AAPL".to_string(),
            summary: "Strong execution".to_string(),
            sentiment: "bullish".to_string(),
            key_points: vec![
                "Services growth".to_string(),
                "Margin expansion".to_string(),
            ],
            risks: vec!["China demand".to_string()],
            opportunities: vec!["AI features".to_string()],
            recommendation: "buy".to_string(),
            target_price: Some(225.0),
        }
    }

    fn sample_financials() -> FinancialsPanelData {
        FinancialsPanelData {
            symbol: "AAPL".to_string(),
            company_name: "Apple Inc.".to_string(),
            cik: "0000320193".to_string(),
            frequency: Frequency::Annual,
            periods: (0..5).map(sample_statement_period).collect(),
            latest_filing: Some(sample_filing_ref()),
            source_status: sample_source_status(),
            available_concepts: Vec::new(),
            mapping_warnings: Vec::new(),
        }
    }

    fn sample_balance_sheet() -> BalanceSheetPanelData {
        BalanceSheetPanelData {
            symbol: "AAPL".to_string(),
            company_name: "Apple Inc.".to_string(),
            cik: "0000320193".to_string(),
            frequency: Frequency::Annual,
            periods: (0..5).map(sample_statement_period).collect(),
            latest_filing: Some(sample_filing_ref()),
            source_status: sample_source_status(),
            available_concepts: Vec::new(),
            selected_currency: Some("USD".to_string()),
            available_currencies: vec!["EUR".to_string(), "USD".to_string()],
            mapping_warnings: Vec::new(),
            available_periods: Vec::new(),
            selected_period_start: None,
            selected_period_end: None,
        }
    }

    fn sample_cash_flow() -> CashFlowPanelData {
        CashFlowPanelData {
            symbol: "AAPL".to_string(),
            company_name: "Apple Inc.".to_string(),
            cik: "0000320193".to_string(),
            frequency: Frequency::Annual,
            periods: (0..5).map(sample_cash_flow_period).collect(),
            latest_filing: Some(sample_filing_ref()),
            source_status: sample_source_status(),
            available_periods: Vec::new(),
            selected_period_start: None,
            selected_period_end: None,
            mapping_warnings: Vec::new(),
        }
    }

    fn sample_dividends() -> DividendsPanelData {
        DividendsPanelData {
            symbol: "AAPL".to_string(),
            company_name: "Apple Inc.".to_string(),
            cik: "0000320193".to_string(),
            ttm_dividends_per_share: Some(1.0),
            ttm_common_dividends_paid: Some(10.0),
            latest_event: Some(sample_dividend_event(0)),
            events: (0..5).map(sample_dividend_event).collect(),
            latest_filing: Some(sample_filing_ref()),
            source_status: sample_source_status(),
        }
    }

    fn sample_earnings() -> EarningsPanelData {
        EarningsPanelData {
            symbol: "AAPL".to_string(),
            company_name: "Apple Inc.".to_string(),
            cik: "0000320193".to_string(),
            frequency: Frequency::Annual,
            periods: (0..5).map(sample_earnings_period).collect(),
            available_periods: (0..5)
                .map(|index| crate::terminal::EarningsPeriodOption {
                    id: format!("202{}", index),
                    label: format!("FY202{} (Sep)", index),
                    frequency: Frequency::Annual,
                    fiscal_year: Some(format!("202{}", index)),
                    fiscal_period: Some("FY".to_string()),
                    period_end: "2026-03-31".to_string(),
                    sort_key: format!("202{}", index),
                })
                .collect(),
            selected_period_start: Some("2020".to_string()),
            selected_period_end: Some("2024".to_string()),
            selected_currency: Some("USD".to_string()),
            available_currencies: vec!["EUR".to_string(), "USD".to_string()],
            latest_filing: Some(sample_filing_ref()),
            source_status: sample_source_status(),
            available_concepts: Vec::new(),
            mapping_warnings: Vec::new(),
        }
    }

    fn sample_portfolio() -> Portfolio {
        Portfolio {
            holdings: (0..30)
                .map(|index| Holding {
                    symbol: format!("SYM{index}"),
                    name: format!("Holding {index}"),
                    quantity: 1.0,
                    avg_cost: 10.0,
                    current_price: 20.0,
                    current_value: 20.0,
                    gain_loss: 10.0,
                    gain_loss_percent: 100.0,
                    cost_basis: Some(10.0),
                    unrealized_gain: Some(10.0),
                    latest_trade_at: Some("2026-04-06T10:00:00Z".to_string()),
                })
                .collect(),
            total_value: 1000.0,
            day_change: 10.0,
            day_change_percent: 1.0,
            total_gain: 100.0,
            total_gain_percent: 10.0,
            cash_balance: Some(100.0),
            invested_cost_basis: Some(900.0),
            realized_gain: Some(50.0),
            unrealized_gain: Some(50.0),
            holdings_count: Some(30),
            stale_pricing_symbols: Some(vec!["SYM1".to_string()]),
        }
    }

    fn sample_error() -> ErrorPanel {
        ErrorPanel {
            title: "Lookup failed".to_string(),
            message: "The lookup failed".to_string(),
            detail: Some("detail".to_string()),
            provider: Some("provider".to_string()),
            query: Some("apple".to_string()),
            symbol: Some("AAPL".to_string()),
        }
    }

    fn sample_source_status() -> SourceStatus {
        SourceStatus {
            companyfacts_used: true,
            degraded_reason: None,
        }
    }

    fn sample_filing_ref() -> FilingRef {
        FilingRef {
            accession_number: "0000000000".to_string(),
            filing_date: "2026-04-01".to_string(),
            report_date: Some("2026-03-31".to_string()),
            form: "10-K".to_string(),
            primary_document: Some("doc.htm".to_string()),
        }
    }

    fn sample_statement_period(index: usize) -> StatementPeriod {
        StatementPeriod {
            label: format!("FY{index}"),
            fiscal_year: Some(format!("20{index:02}")),
            fiscal_period: Some("FY".to_string()),
            period_start: Some("2025-01-01".to_string()),
            period_end: "2025-12-31".to_string(),
            filed_date: "2026-01-31".to_string(),
            form: "10-K".to_string(),
            revenue: Some(100.0),
            gross_profit: Some(50.0),
            operating_income: Some(40.0),
            net_income: Some(30.0),
            diluted_eps: Some(2.0),
            cash_and_equivalents: Some(20.0),
            short_term_investments: Some(5.0),
            accounts_receivable_net: Some(7.0),
            inventory: Some(3.0),
            other_current_assets: Some(2.0),
            total_current_assets: Some(37.0),
            property_plant_and_equipment_net: Some(12.0),
            goodwill: Some(1.0),
            intangible_assets: Some(1.5),
            other_non_current_assets: Some(0.5),
            total_assets: Some(10.0),
            accounts_payable: Some(2.0),
            accrued_expenses_and_other_current_liabilities: Some(1.0),
            short_term_debt: Some(0.5),
            total_current_liabilities: Some(3.5),
            long_term_debt: Some(1.0),
            other_non_current_liabilities: Some(0.5),
            total_liabilities: Some(5.0),
            retained_earnings: Some(4.0),
            accumulated_other_comprehensive_income_loss: Some(0.2),
            total_equity: Some(5.0),
            shares_outstanding: Some(1.0),
            book_value_per_share: Some(5.0),
        }
    }

    fn sample_cash_flow_period(index: usize) -> CashFlowPeriod {
        CashFlowPeriod {
            label: format!("FY{index}"),
            fiscal_year: Some(format!("20{index:02}")),
            fiscal_period: Some("FY".to_string()),
            period_start: Some("2025-01-01".to_string()),
            period_end: "2025-12-31".to_string(),
            filed_date: "2026-01-31".to_string(),
            form: "10-K".to_string(),
            net_income: Some(30.0),
            depreciation_and_amortization: Some(15.0),
            stock_based_compensation: Some(5.0),
            deferred_income_taxes: Some(2.0),
            impairment_charges: None,
            loss_gain_on_sale_of_assets: None,
            other_non_cash_items: Some(1.0),
            accounts_receivable: Some(-4.0),
            inventory: Some(-2.0),
            prepaid_expenses_and_other_current_assets: Some(-1.0),
            accounts_payable: Some(3.0),
            accrued_expenses: Some(2.0),
            deferred_revenue: Some(1.0),
            other_working_capital_changes: None,
            change_in_operating_assets: Some(-8.0),
            change_in_operating_liabilities: Some(3.0),
            other_operating_adjustments: None,
            operating_cash_flow: Some(100.0),
            capex: Some(-10.0),
            proceeds_from_sale_of_ppe: Some(1.0),
            purchases_of_investments: Some(-40.0),
            sales_of_investments: Some(20.0),
            acquisitions: None,
            other_investing_activities: None,
            investing_cash_flow: Some(-50.0),
            debt_issuance: Some(50.0),
            debt_repayment: Some(-30.0),
            equity_issuance: Some(10.0),
            stock_repurchases: Some(-15.0),
            dividends_paid: Some(-20.0),
            finance_lease_obligations: Some(-5.0),
            other_financing_activities: None,
            financing_cash_flow: Some(-25.0),
            free_cash_flow: Some(90.0),
            exchange_rate_effects: None,
            net_increase_decrease_cash: Some(5.0),
            beginning_cash: Some(15.0),
            ending_cash: Some(20.0),
            cash_paid_interest: Some(4.0),
            cash_paid_income_taxes: Some(6.0),
        }
    }

    fn sample_dividend_event(index: usize) -> DividendEvent {
        DividendEvent {
            end_date: format!("2026-03-0{}", index + 1),
            filed_date: "2026-03-15".to_string(),
            form: "10-Q".to_string(),
            frequency_guess: "Quarterly".to_string(),
            dividend_per_share: Some(0.25),
            total_cash_dividends: Some(100.0),
        }
    }

    fn sample_earnings_period(index: usize) -> EarningsPeriod {
        EarningsPeriod {
            label: format!("FY202{} (Sep)", index),
            display_label: format!("FY202{} (Sep)", index),
            fiscal_year: Some(format!("202{}", index)),
            fiscal_period: Some("FY".to_string()),
            period_start: Some(format!("202{}-01-01", index)),
            period_end: format!("202{}-09-30", index),
            filed_date: "2026-04-30".to_string(),
            form: "10-K".to_string(),
            total_revenues: Some(100.0),
            total_revenues_yoy_change_percent: Some(5.0),
            cost_of_sales: Some(45.0),
            gross_profit: Some(55.0),
            gross_profit_margin: Some(55.0),
            selling_general_and_administrative_expenses: Some(10.0),
            research_and_development_expenses: Some(5.0),
            other_operating_expenses: Some(0.0),
            operating_profit: Some(40.0),
            operating_margin: Some(40.0),
            non_operating_income: Some(2.0),
            total_non_operating_income: Some(2.0),
            income_before_provision_for_income_taxes: Some(42.0),
            provision_for_income_taxes: Some(8.4),
            consolidated_net_income: Some(30.0),
            net_income_attributable_to_common_shareholders: Some(29.5),
            total_shares_outstanding: Some(101.0),
            basic_weighted_average_shares_outstanding: Some(99.0),
            diluted_weighted_average_shares_outstanding: Some(100.0),
            ebitda: Some(44.0),
            effective_tax_rate: Some(20.0),
            revenue: Some(100.0),
            net_income: Some(30.0),
            basic_eps: Some(1.0),
            diluted_eps: Some(0.9),
            diluted_weighted_average_shares: Some(100.0),
            revenue_yoy_change_percent: Some(5.0),
            diluted_eps_yoy_change_percent: Some(2.0),
        }
    }

    fn sample_chart_point(
        label: &str,
        price: f64,
        timestamp: Option<&str>,
    ) -> crate::terminal::CompanyPricePoint {
        crate::terminal::CompanyPricePoint {
            label: label.to_string(),
            price,
            volume: Some(10),
            timestamp: timestamp.map(str::to_string),
        }
    }
}
