use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use chrono::Utc;
use futures::future::BoxFuture;
use serde_json::json;
use tauri::{AppHandle, Runtime};
use tauri_plugin_store::StoreExt;
use thiserror::Error;
use tokio::sync::Mutex;

use crate::portfolio::engine::{replay_ledger, PortfolioEngineError};
use crate::portfolio::events::PortfolioEventEmitter;
use crate::portfolio::{
    CashConfirmation, PortfolioHolding, PortfolioLedger, PortfolioSnapshot, PortfolioStats,
    PortfolioTransaction, ReplaySnapshot, TradeConfirmation, TransactionKind, PORTFOLIO_LEDGER_KEY,
    PORTFOLIO_LEDGER_STORE_PATH,
};
use crate::terminal::security_lookup::{
    SecurityKind, SecurityLookup, SecurityLookupError, SecurityMatch,
};
use crate::terminal::{Company, Holding, Portfolio};

const QUOTE_CACHE_TTL_SECONDS: u64 = 300; // 5 minutes
const TRANSACTION_ID_PREFIX: &str = "portfolio-tx-";

fn build_holding(
    position: &crate::portfolio::PositionSnapshot,
    current_price: f64,
    day_change_value: f64,
    stale_pricing: bool,
) -> PortfolioHolding {
    let current_value = current_price * position.quantity;
    let unrealized_gain = current_value - position.cost_basis;
    PortfolioHolding {
        symbol: position.symbol.clone(),
        name: position.company_name.clone(),
        quantity: position.quantity,
        cost_basis: position.cost_basis,
        current_price,
        current_value,
        unrealized_gain,
        gain_loss_percent: percent_change(unrealized_gain, position.cost_basis),
        latest_trade_at: position.latest_trade_at.clone(),
        stale_pricing,
        day_change: day_change_value,
    }
}

fn build_snapshot(
    mut holdings: Vec<PortfolioHolding>,
    stale_pricing_symbols: Vec<String>,
    cash_balance: f64,
    realized_gain: f64,
) -> PortfolioSnapshot {
    holdings.sort_by(|left, right| left.symbol.cmp(&right.symbol));

    let invested_cost_basis: f64 = holdings.iter().map(|h| h.cost_basis).sum();
    let equities_market_value: f64 = holdings.iter().map(|h| h.current_value).sum();
    let unrealized_gain: f64 = holdings.iter().map(|h| h.unrealized_gain).sum();
    let day_change: f64 = holdings.iter().map(|h| h.day_change).sum();
    let total_portfolio_value = equities_market_value + cash_balance;
    let baseline_value = total_portfolio_value - day_change;

    PortfolioSnapshot {
        cash_balance,
        day_change,
        day_change_percent: if baseline_value > 0.0 {
            (day_change / baseline_value) * 100.0
        } else {
            0.0
        },
        equities_market_value,
        holdings,
        invested_cost_basis,
        realized_gain,
        stale_pricing_symbols,
        total_portfolio_value,
        unrealized_gain,
    }
}

fn snapshot_to_portfolio(snapshot: &PortfolioSnapshot) -> Portfolio {
    let holdings_count = snapshot.holdings.len();

    Portfolio {
        cash_balance: Some(snapshot.cash_balance),
        day_change: snapshot.day_change,
        day_change_percent: snapshot.day_change_percent,
        holdings: snapshot
            .holdings
            .iter()
            .map(|holding| Holding {
                avg_cost: if holding.quantity > 0.0 {
                    holding.cost_basis / holding.quantity
                } else {
                    0.0
                },
                cost_basis: Some(holding.cost_basis),
                current_price: holding.current_price,
                current_value: holding.current_value,
                gain_loss: holding.unrealized_gain,
                gain_loss_percent: holding.gain_loss_percent,
                latest_trade_at: holding.latest_trade_at.clone(),
                name: holding.name.clone(),
                quantity: holding.quantity,
                symbol: holding.symbol.clone(),
                unrealized_gain: Some(holding.unrealized_gain),
            })
            .collect(),
        holdings_count: Some(holdings_count),
        invested_cost_basis: Some(snapshot.invested_cost_basis),
        realized_gain: Some(snapshot.realized_gain),
        stale_pricing_symbols: Some(snapshot.stale_pricing_symbols.clone()),
        total_gain: snapshot.unrealized_gain,
        total_gain_percent: percent_change(snapshot.unrealized_gain, snapshot.invested_cost_basis),
        total_value: snapshot.total_portfolio_value,
        unrealized_gain: Some(snapshot.unrealized_gain),
    }
}

/// Cached quote with timestamp
#[derive(Debug)]
struct CachedQuote {
    company: Company,
    cached_at: Instant,
}

impl Clone for CachedQuote {
    fn clone(&self) -> Self {
        Self {
            company: self.company.clone(),
            cached_at: self.cached_at,
        }
    }
}

/// Thread-safe quote cache
#[derive(Debug, Clone)]
struct QuoteCache {
    inner: Arc<Mutex<HashMap<String, CachedQuote>>>,
}

impl QuoteCache {
    fn new() -> Self {
        Self {
            inner: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    /// Get a cached quote if it exists and is not stale
    async fn get(&self, symbol: &str) -> Option<Company> {
        let cache = self.inner.lock().await;
        let entry = cache.get(symbol)?;

        // Check if cache entry is still valid
        if entry.cached_at.elapsed() < Duration::from_secs(QUOTE_CACHE_TTL_SECONDS) {
            Some(entry.company.clone())
        } else {
            None
        }
    }

    /// Insert a quote into the cache
    async fn insert(&self, symbol: String, company: Company) {
        let mut cache = self.inner.lock().await;
        cache.insert(
            symbol,
            CachedQuote {
                company,
                cached_at: Instant::now(),
            },
        );
    }

    /// Clear stale entries from the cache
    #[allow(dead_code)]
    async fn clear_stale(&self) {
        let mut cache = self.inner.lock().await;
        cache.retain(|_, entry| {
            entry.cached_at.elapsed() < Duration::from_secs(QUOTE_CACHE_TTL_SECONDS)
        });
    }

    /// Get all cached symbols
    async fn cached_symbols(&self) -> Vec<String> {
        let cache = self.inner.lock().await;
        cache.keys().cloned().collect()
    }

    /// Check if a symbol is cached and fresh
    #[allow(dead_code)]
    async fn is_fresh(&self, symbol: &str) -> bool {
        let cache = self.inner.lock().await;
        match cache.get(symbol) {
            Some(entry) => entry.cached_at.elapsed() < Duration::from_secs(QUOTE_CACHE_TTL_SECONDS),
            None => false,
        }
    }
}

#[derive(Debug, Error, Clone, PartialEq)]
pub enum PortfolioStoreError {
    #[error("portfolio store unavailable: {0}")]
    StoreUnavailable(String),
    #[error("portfolio ledger is not valid JSON: {0}")]
    Deserialize(String),
    #[error("portfolio ledger could not be saved: {0}")]
    SaveFailed(String),
    #[error(transparent)]
    Engine(#[from] PortfolioEngineError),
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PortfolioValidationError {
    #[error("cash balance is insufficient for this trade")]
    TradeCash,
    #[error("cash balance is insufficient for this withdrawal")]
    WithdrawalCash,
    #[error("not enough shares are available to sell for {symbol}")]
    ShareShortfall { symbol: String },
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PortfolioQuoteError {
    #[error("quote unavailable for {symbol}: {detail}")]
    Unavailable { symbol: String, detail: String },
}

#[derive(Debug, Error, Clone, PartialEq)]
pub enum PortfolioCommandError {
    #[error(transparent)]
    Store(#[from] PortfolioStoreError),
    #[error(transparent)]
    Validation(#[from] PortfolioValidationError),
    #[error(transparent)]
    Quote(#[from] PortfolioQuoteError),
}

pub trait PortfolioManagement: Send + Sync {
    fn portfolio<'a>(&'a self) -> BoxFuture<'a, Result<Portfolio, PortfolioCommandError>>;
    fn stats<'a>(&'a self) -> BoxFuture<'a, Result<PortfolioStats, PortfolioCommandError>>;
    fn history<'a>(
        &'a self,
        limit: usize,
    ) -> BoxFuture<'a, Result<Vec<PortfolioTransaction>, PortfolioCommandError>>;
    fn buy<'a>(
        &'a self,
        symbol: &'a str,
        quantity: f64,
        price_override: Option<f64>,
    ) -> BoxFuture<'a, Result<TradeConfirmation, PortfolioCommandError>>;
    fn sell<'a>(
        &'a self,
        symbol: &'a str,
        quantity: f64,
        price_override: Option<f64>,
    ) -> BoxFuture<'a, Result<TradeConfirmation, PortfolioCommandError>>;
    fn deposit_cash<'a>(
        &'a self,
        amount: f64,
    ) -> BoxFuture<'a, Result<CashConfirmation, PortfolioCommandError>>;
    fn withdraw_cash<'a>(
        &'a self,
        amount: f64,
    ) -> BoxFuture<'a, Result<CashConfirmation, PortfolioCommandError>>;
}

pub struct PortfolioService<R: Runtime> {
    app_handle: AppHandle<R>,
    event_emitter: PortfolioEventEmitter<R>,
    security_lookup: Arc<dyn SecurityLookup>,
    quote_cache: QuoteCache,
    write_lock: Mutex<()>,
    next_transaction_id: AtomicU64,
}

impl<R: Runtime> PortfolioService<R> {
    pub fn new(app_handle: &AppHandle<R>, security_lookup: Arc<dyn SecurityLookup>) -> Self {
        let next_transaction_id = load_ledger_from_handle(app_handle)
            .map(|ledger| next_transaction_sequence(&ledger))
            .unwrap_or(1);

        Self {
            app_handle: app_handle.clone(),
            event_emitter: PortfolioEventEmitter::new(app_handle),
            security_lookup,
            quote_cache: QuoteCache::new(),
            write_lock: Mutex::new(()),
            next_transaction_id: AtomicU64::new(next_transaction_id),
        }
    }

    fn load_ledger(&self) -> Result<PortfolioLedger, PortfolioStoreError> {
        load_ledger_from_handle(&self.app_handle)
    }

    fn save_ledger(&self, ledger: &PortfolioLedger) -> Result<(), PortfolioStoreError> {
        let store = self
            .app_handle
            .store(PORTFOLIO_LEDGER_STORE_PATH)
            .map_err(|error| PortfolioStoreError::StoreUnavailable(error.to_string()))?;

        store.set(PORTFOLIO_LEDGER_KEY.to_string(), json!(ledger));
        store
            .save()
            .map_err(|error| PortfolioStoreError::SaveFailed(error.to_string()))
    }

    fn replay(&self, ledger: &PortfolioLedger) -> Result<ReplaySnapshot, PortfolioStoreError> {
        replay_ledger(ledger).map_err(map_engine_error)
    }

    fn next_transaction_id(&self, ledger: &PortfolioLedger) -> String {
        let ledger_next = next_transaction_sequence(ledger);
        let next_id = reserve_transaction_sequence(&self.next_transaction_id, ledger_next);
        format!("{TRANSACTION_ID_PREFIX}{next_id}")
    }

    async fn resolve_trade_input(
        &self,
        symbol: &str,
        price_override: Option<f64>,
    ) -> Result<(String, String, f64), PortfolioCommandError> {
        let normalized_symbol = symbol.trim().to_ascii_uppercase();
        let security_match = SecurityMatch {
            symbol: normalized_symbol.clone(),
            name: None,
            exchange: None,
            kind: SecurityKind::Equity,
        };

        let company = self
            .security_lookup
            .load_company(&security_match)
            .await
            .map_err(|error| map_quote_error(&normalized_symbol, error))?;

        Ok((
            normalized_symbol,
            company.name,
            price_override.unwrap_or(company.price),
        ))
    }

    async fn candidate_trade_result(
        &self,
        symbol: &str,
        company_name: &str,
        quantity: f64,
        price: f64,
        kind: TransactionKind,
    ) -> Result<TradeConfirmation, PortfolioCommandError> {
        let _guard = self.write_lock.lock().await;
        let mut ledger = self.load_ledger()?;
        let before_snapshot = self.replay(&ledger)?;

        let transaction = PortfolioTransaction {
            id: self.next_transaction_id(&ledger),
            kind: kind.clone(),
            symbol: Some(symbol.to_string()),
            company_name: Some(company_name.to_string()),
            quantity: Some(quantity),
            price: Some(price),
            gross_amount: quantity * price,
            fee: 0.0,
            executed_at: Utc::now().to_rfc3339(),
            note: None,
        };
        ledger.transactions.push(transaction);

        let after_snapshot = self
            .replay(&ledger)
            .map_err(|error| map_trade_validation_error(error, symbol))?;

        self.save_ledger(&ledger)?;

        Ok(TradeConfirmation {
            symbol: symbol.to_string(),
            company_name: company_name.to_string(),
            quantity,
            price,
            gross_amount: quantity * price,
            cash_balance: after_snapshot.cash_balance,
            realized_gain: matches!(kind, TransactionKind::Sell)
                .then_some(after_snapshot.realized_gain - before_snapshot.realized_gain),
        })
    }

    async fn candidate_cash_result(
        &self,
        amount: f64,
        kind: TransactionKind,
    ) -> Result<CashConfirmation, PortfolioCommandError> {
        let _guard = self.write_lock.lock().await;
        let mut ledger = self.load_ledger()?;

        ledger.transactions.push(PortfolioTransaction {
            id: self.next_transaction_id(&ledger),
            kind: kind.clone(),
            symbol: None,
            company_name: None,
            quantity: None,
            price: None,
            gross_amount: amount,
            fee: 0.0,
            executed_at: Utc::now().to_rfc3339(),
            note: None,
        });

        let snapshot = self.replay(&ledger).map_err(map_cash_validation_error)?;

        self.save_ledger(&ledger)?;

        Ok(CashConfirmation {
            amount,
            cash_balance: snapshot.cash_balance,
            kind,
        })
    }

    /// Load portfolio with cached quotes (fast path)
    async fn value_portfolio_with_cache(&self) -> Result<PortfolioSnapshot, PortfolioCommandError> {
        let ledger = self.load_ledger()?;
        let replay_snapshot = self.replay(&ledger)?;

        let mut holdings = Vec::with_capacity(replay_snapshot.positions.len());
        let mut stale_pricing_symbols = Vec::new();
        let mut needs_fresh_quotes = Vec::new();

        for position in &replay_snapshot.positions {
            let (current_price, day_change_value, stale_pricing, is_cached) =
                match self.quote_cache.get(&position.symbol).await {
                    Some(cached_company) => (
                        cached_company.price,
                        cached_company.change * position.quantity,
                        false,
                        true,
                    ),
                    None => {
                        needs_fresh_quotes.push(position.clone());
                        let fallback_price = position.latest_trade_price.unwrap_or(0.0);
                        (fallback_price, 0.0, true, false)
                    }
                };

            holdings.push(build_holding(
                position,
                current_price,
                day_change_value,
                stale_pricing || !is_cached,
            ));

            if stale_pricing {
                stale_pricing_symbols.push(position.symbol.clone());
            }
        }

        Ok(build_snapshot(
            holdings,
            stale_pricing_symbols,
            replay_snapshot.cash_balance,
            replay_snapshot.realized_gain,
        ))
    }

    async fn value_portfolio(&self) -> Result<PortfolioSnapshot, PortfolioCommandError> {
        let ledger = self.load_ledger()?;
        let replay_snapshot = self.replay(&ledger)?;

        self.event_emitter.loading_started();

        let cached_snapshot = self.value_portfolio_with_cache().await?;

        let cached_symbols = self.quote_cache.cached_symbols().await;
        let positions_need_refresh: Vec<_> = replay_snapshot
            .positions
            .iter()
            .filter(|pos| {
                !cached_symbols.contains(&pos.symbol)
                    || pos.latest_trade_price.is_none_or(|price| price == 0.0)
            })
            .cloned()
            .collect();

        if positions_need_refresh.is_empty() {
            self.event_emitter
                .ready(&snapshot_to_portfolio(&cached_snapshot));
            return Ok(cached_snapshot);
        }

        self.event_emitter
            .loaded(&snapshot_to_portfolio(&cached_snapshot));

        let event_emitter = self.event_emitter.clone();
        let security_lookup = Arc::clone(&self.security_lookup);
        let quote_cache = self.quote_cache.clone();
        let cash_balance = replay_snapshot.cash_balance;
        let realized_gain = replay_snapshot.realized_gain;

        tauri::async_runtime::spawn(async move {
            let mut holdings = Vec::new();
            let mut stale_pricing_symbols = Vec::new();

            for position in positions_need_refresh {
                let security_match = SecurityMatch {
                    symbol: position.symbol.clone(),
                    name: Some(position.company_name.clone()),
                    exchange: None,
                    kind: SecurityKind::Equity,
                };

                let quote_result = security_lookup.load_company(&security_match).await;
                let (current_price, day_change_value, stale_pricing) = match quote_result {
                    Ok(company) => {
                        quote_cache
                            .insert(position.symbol.clone(), company.clone())
                            .await;
                        (company.price, company.change * position.quantity, false)
                    }
                    Err(_) => {
                        let fallback_price = position.latest_trade_price.unwrap_or(0.0);
                        stale_pricing_symbols.push(position.symbol.clone());
                        (fallback_price, 0.0, true)
                    }
                };

                holdings.push(build_holding(
                    &position,
                    current_price,
                    day_change_value,
                    stale_pricing,
                ));
            }

            let fresh_snapshot =
                build_snapshot(holdings, stale_pricing_symbols, cash_balance, realized_gain);

            event_emitter.ready(&snapshot_to_portfolio(&fresh_snapshot));
        });

        Ok(cached_snapshot)
    }
}

impl<R: Runtime> PortfolioManagement for PortfolioService<R> {
    fn portfolio<'a>(&'a self) -> BoxFuture<'a, Result<Portfolio, PortfolioCommandError>> {
        Box::pin(async move {
            let snapshot = self.value_portfolio().await?;
            Ok(snapshot_to_portfolio(&snapshot))
        })
    }

    fn stats<'a>(&'a self) -> BoxFuture<'a, Result<PortfolioStats, PortfolioCommandError>> {
        Box::pin(async move {
            let snapshot = self.value_portfolio().await?;

            Ok(PortfolioStats {
                cash_balance: snapshot.cash_balance,
                day_change: snapshot.day_change,
                equities_market_value: snapshot.equities_market_value,
                holdings_count: snapshot.holdings.len(),
                invested_cost_basis: snapshot.invested_cost_basis,
                realized_gain: snapshot.realized_gain,
                stale_pricing_symbols: snapshot.stale_pricing_symbols,
                total_portfolio_value: snapshot.total_portfolio_value,
                unrealized_gain: snapshot.unrealized_gain,
            })
        })
    }

    fn history<'a>(
        &'a self,
        limit: usize,
    ) -> BoxFuture<'a, Result<Vec<PortfolioTransaction>, PortfolioCommandError>> {
        Box::pin(async move {
            let ledger = self.load_ledger()?;
            let mut transactions = ledger.transactions;
            transactions.sort_by(|left, right| {
                right
                    .executed_at
                    .cmp(&left.executed_at)
                    .then_with(|| right.id.cmp(&left.id))
            });
            transactions.truncate(limit);
            Ok(transactions)
        })
    }

    fn buy<'a>(
        &'a self,
        symbol: &'a str,
        quantity: f64,
        price_override: Option<f64>,
    ) -> BoxFuture<'a, Result<TradeConfirmation, PortfolioCommandError>> {
        Box::pin(async move {
            let (symbol, company_name, price) =
                self.resolve_trade_input(symbol, price_override).await?;
            self.candidate_trade_result(
                &symbol,
                &company_name,
                quantity,
                price,
                TransactionKind::Buy,
            )
            .await
        })
    }

    fn sell<'a>(
        &'a self,
        symbol: &'a str,
        quantity: f64,
        price_override: Option<f64>,
    ) -> BoxFuture<'a, Result<TradeConfirmation, PortfolioCommandError>> {
        Box::pin(async move {
            let (symbol, company_name, price) =
                self.resolve_trade_input(symbol, price_override).await?;
            self.candidate_trade_result(
                &symbol,
                &company_name,
                quantity,
                price,
                TransactionKind::Sell,
            )
            .await
        })
    }

    fn deposit_cash<'a>(
        &'a self,
        amount: f64,
    ) -> BoxFuture<'a, Result<CashConfirmation, PortfolioCommandError>> {
        Box::pin(async move {
            self.candidate_cash_result(amount, TransactionKind::CashDeposit)
                .await
        })
    }

    fn withdraw_cash<'a>(
        &'a self,
        amount: f64,
    ) -> BoxFuture<'a, Result<CashConfirmation, PortfolioCommandError>> {
        Box::pin(async move {
            self.candidate_cash_result(amount, TransactionKind::CashWithdrawal)
                .await
        })
    }
}

fn load_ledger_from_handle<R: Runtime>(
    app_handle: &AppHandle<R>,
) -> Result<PortfolioLedger, PortfolioStoreError> {
    let store = app_handle
        .store(PORTFOLIO_LEDGER_STORE_PATH)
        .map_err(|error| PortfolioStoreError::StoreUnavailable(error.to_string()))?;

    match store.get(PORTFOLIO_LEDGER_KEY) {
        Some(value) => serde_json::from_value(value.clone())
            .map_err(|error| PortfolioStoreError::Deserialize(error.to_string())),
        None => Ok(PortfolioLedger::default()),
    }
}

fn parse_transaction_sequence(id: &str) -> Option<u64> {
    id.strip_prefix(TRANSACTION_ID_PREFIX)
        .and_then(|value| value.parse::<u64>().ok())
}

fn next_transaction_sequence(ledger: &PortfolioLedger) -> u64 {
    ledger
        .transactions
        .iter()
        .filter_map(|transaction| parse_transaction_sequence(&transaction.id))
        .max()
        .unwrap_or(0)
        + 1
}

fn reserve_transaction_sequence(counter: &AtomicU64, ledger_next: u64) -> u64 {
    loop {
        let current = counter.load(Ordering::Relaxed);
        let next_id = current.max(ledger_next);

        if counter
            .compare_exchange(current, next_id + 1, Ordering::Relaxed, Ordering::Relaxed)
            .is_ok()
        {
            return next_id;
        }
    }
}

fn map_engine_error(error: PortfolioEngineError) -> PortfolioStoreError {
    PortfolioStoreError::Engine(error)
}

fn map_trade_validation_error(error: PortfolioStoreError, symbol: &str) -> PortfolioCommandError {
    match error {
        PortfolioStoreError::Engine(PortfolioEngineError::NegativeCash { .. }) => {
            PortfolioCommandError::Validation(PortfolioValidationError::TradeCash)
        }
        PortfolioStoreError::Engine(PortfolioEngineError::InsufficientShares { .. }) => {
            PortfolioCommandError::Validation(PortfolioValidationError::ShareShortfall {
                symbol: symbol.to_string(),
            })
        }
        other => PortfolioCommandError::Store(other),
    }
}

fn map_cash_validation_error(error: PortfolioStoreError) -> PortfolioCommandError {
    match error {
        PortfolioStoreError::Engine(PortfolioEngineError::NegativeCash { .. }) => {
            PortfolioCommandError::Validation(PortfolioValidationError::WithdrawalCash)
        }
        other => PortfolioCommandError::Store(other),
    }
}

fn map_quote_error(symbol: &str, error: SecurityLookupError) -> PortfolioCommandError {
    let detail = match error {
        SecurityLookupError::DetailUnavailable { detail, .. }
        | SecurityLookupError::SearchUnavailable { detail, .. } => detail,
    };

    PortfolioCommandError::Quote(PortfolioQuoteError::Unavailable {
        symbol: symbol.to_string(),
        detail,
    })
}

fn percent_change(delta: f64, base: f64) -> f64 {
    if base > 0.0 {
        (delta / base) * 100.0
    } else {
        0.0
    }
}

#[cfg(test)]
mod tests {
    use std::env;
    use std::fs;
    use std::sync::Arc;
    use std::time::{SystemTime, UNIX_EPOCH};

    use futures::future::BoxFuture;
    use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};

    use super::{
        PortfolioCommandError, PortfolioManagement, PortfolioService, PortfolioValidationError,
    };
    use crate::portfolio::TransactionKind;
    use crate::terminal::security_lookup::{SecurityLookup, SecurityLookupError, SecurityMatch};
    use crate::terminal::Company;

    #[derive(Debug)]
    struct FakeSecurityLookup;

    impl SecurityLookup for FakeSecurityLookup {
        fn provider_name(&self) -> &'static str {
            "Google Finance"
        }

        fn search<'a>(
            &'a self,
            _query: &'a str,
        ) -> BoxFuture<'a, Result<Vec<SecurityMatch>, SecurityLookupError>> {
            Box::pin(async { Ok(Vec::new()) })
        }

        fn load_company<'a>(
            &'a self,
            security_match: &'a SecurityMatch,
        ) -> BoxFuture<'a, Result<Company, SecurityLookupError>> {
            Box::pin(async move {
                Ok(Company {
                    symbol: security_match.symbol.clone(),
                    name: format!("{} Corp", security_match.symbol),
                    price: 100.0,
                    change: 2.0,
                    change_percent: 2.0,
                    market_cap: 1_000_000.0,
                    volume: None,
                    volume_label: None,
                    pe: None,
                    eps: None,
                    high52_week: None,
                    low52_week: None,
                    profile: None,
                    price_chart: None,
                    price_chart_ranges: None,
                })
            })
        }
    }

    #[test]
    fn loads_default_empty_ledger_when_store_is_empty() {
        with_test_home("empty-ledger", || {
            let service = build_service();

            let history =
                futures::executor::block_on(service.history(10)).expect("history should load");

            assert!(history.is_empty());
        });
    }

    #[test]
    fn persists_appended_transactions() {
        with_test_home("persisted-ledger", || {
            let service = build_service();

            futures::executor::block_on(service.deposit_cash(1_000.0))
                .expect("deposit should succeed");

            let history =
                futures::executor::block_on(service.history(10)).expect("history should load");

            assert_eq!(history.len(), 1);
            assert_eq!(history[0].kind, TransactionKind::CashDeposit);
        });
    }

    #[test]
    fn rejects_buy_when_cash_is_insufficient() {
        with_test_home("buy-insufficient-cash", || {
            let service = build_service();

            let error = futures::executor::block_on(service.buy("AAPL", 10.0, Some(100.0)))
                .expect_err("buy should fail");

            assert!(matches!(
                error,
                PortfolioCommandError::Validation(PortfolioValidationError::TradeCash)
            ));
        });
    }

    #[test]
    fn rejects_sell_when_shares_are_insufficient() {
        with_test_home("sell-insufficient-shares", || {
            let service = build_service();

            futures::executor::block_on(service.deposit_cash(1_000.0))
                .expect("deposit should succeed");

            let error = futures::executor::block_on(service.sell("AAPL", 1.0, Some(100.0)))
                .expect_err("sell should fail");

            assert!(matches!(
                error,
                PortfolioCommandError::Validation(PortfolioValidationError::ShareShortfall { .. })
            ));
        });
    }

    #[test]
    fn rejects_withdrawal_when_cash_is_insufficient() {
        with_test_home("withdrawal-insufficient-cash", || {
            let service = build_service();

            let error = futures::executor::block_on(service.withdraw_cash(100.0))
                .expect_err("withdrawal should fail");

            assert!(matches!(
                error,
                PortfolioCommandError::Validation(PortfolioValidationError::WithdrawalCash)
            ));
        });
    }

    fn build_service() -> PortfolioService<MockRuntime> {
        let app = mock_builder()
            .plugin(tauri_plugin_store::Builder::new().build())
            .build(mock_context(noop_assets()))
            .expect("test app should build");

        PortfolioService::new(app.handle(), Arc::new(FakeSecurityLookup))
    }

    fn unique_identifier(prefix: &str) -> String {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock should work")
            .as_nanos();
        format!("com.mosaiciq.portfolio.tests.{prefix}.{nanos}")
    }

    fn with_test_home<T>(prefix: &str, test: impl FnOnce() -> T) -> T {
        let _lock = crate::test_support::env_lock()
            .lock()
            .expect("env lock should succeed");
        let home = env::temp_dir().join(unique_identifier(prefix));
        fs::create_dir_all(&home).expect("home dir should exist");

        let original_home = env::var_os("HOME");
        env::set_var("HOME", &home);

        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(test));

        match original_home {
            Some(value) => env::set_var("HOME", value),
            None => env::remove_var("HOME"),
        }

        let _ = fs::remove_dir_all(&home);

        match result {
            Ok(value) => value,
            Err(payload) => std::panic::resume_unwind(payload),
        }
    }
}
