"""Tests for the fund module: NAV, allocation, risk, settlement, accounting, scoring, controller."""

import threading
import time

import pytest

from horizon._horizon import Engine, Market, RiskConfig
from horizon.fund._types import (
    AllocationMethod,
    FeeType,
    FundConfig,
    MarketScore,
    NAVSnapshot,
    SettlementEvent,
    StrategyConfig,
    StrategyState,
    StrategyStatus,
    FundReport,
)
from horizon.fund._nav import NAVEngine
from horizon.fund._allocator import CapitalAllocator
from horizon.fund._fund_risk import FundRiskMonitor, RiskAlert
from horizon.fund._settlement import SettlementMonitor
from horizon.fund._accounting import FundAccounting, FeeRecord
from horizon.fund._scorer import MarketScorer
from horizon.fund._controller import StrategyController
from horizon.fund import FundManager


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def engine():
    return Engine(risk_config=RiskConfig(max_order_size=1000.0, max_portfolio_notional=10_000.0))


@pytest.fixture
def fund_config():
    return FundConfig(
        total_capital=100_000.0,
        max_fund_drawdown_pct=15.0,
        max_strategy_drawdown_pct=25.0,
    )


# ===========================================================================
# Types
# ===========================================================================

class TestTypes:
    def test_strategy_state_values(self):
        assert StrategyState.PENDING.value == "pending"
        assert StrategyState.RUNNING.value == "running"
        assert StrategyState.STOPPED.value == "stopped"
        assert StrategyState.FAILED.value == "failed"
        assert StrategyState.RETIRED.value == "retired"

    def test_allocation_method_values(self):
        assert AllocationMethod.EQUAL.value == "equal"
        assert AllocationMethod.RISK_PARITY.value == "risk_parity"
        assert AllocationMethod.PERFORMANCE.value == "performance"
        assert AllocationMethod.CUSTOM.value == "custom"

    def test_fee_type_values(self):
        assert FeeType.EXCHANGE.value == "exchange"
        assert FeeType.MANAGEMENT.value == "management"
        assert FeeType.PERFORMANCE.value == "performance"

    def test_nav_snapshot_frozen(self):
        snap = NAVSnapshot(
            timestamp=1.0, total_nav=100.0, cash=50.0,
            positions_value=50.0, unrealized_pnl=10.0,
            realized_pnl=5.0, high_water_mark=100.0, drawdown_pct=0.0,
        )
        with pytest.raises(AttributeError):
            snap.total_nav = 200.0  # type: ignore

    def test_strategy_status_frozen(self):
        s = StrategyStatus(
            name="test", state=StrategyState.RUNNING,
            allocated_capital=10_000.0, current_nav=10_500.0,
            pnl=500.0, drawdown_pct=2.0,
            open_orders=3, active_positions=2, uptime_secs=60.0,
        )
        assert s.name == "test"
        assert s.state == StrategyState.RUNNING

    def test_market_score_frozen(self):
        ms = MarketScore(
            market_id="m1", exchange="poly",
            composite_score=0.8, liquidity_score=0.7,
            edge_score=0.9, fit_score=0.5,
            volume_24h=50_000.0, spread=0.02, price=0.55,
        )
        assert ms.composite_score == 0.8

    def test_settlement_event_frozen(self):
        se = SettlementEvent(
            market_id="m1", strategy_name="s1",
            timestamp=1.0, outcome="yes",
            pnl=100.0, position_size=10.0, side="buy",
        )
        assert se.outcome == "yes"

    def test_fund_config_defaults(self):
        cfg = FundConfig()
        assert cfg.total_capital == 100_000.0
        assert cfg.allocation_method == AllocationMethod.EQUAL
        assert cfg.max_strategies == 20
        assert cfg.rebalance_interval_secs == 60.0

    def test_strategy_config(self):
        cfg = StrategyConfig(
            name="test_strat",
            pipeline=[lambda ctx: None],
            markets=["m1", "m2"],
            mode="paper",
        )
        assert cfg.name == "test_strat"
        assert len(cfg.markets) == 2
        assert cfg.mode == "paper"

    def test_fund_report(self):
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=100_000.0, cash=50_000.0,
            positions_value=50_000.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        report = FundReport(
            timestamp=1.0, nav=nav, strategies=[],
            total_strategies=0, running_strategies=0,
            recent_settlements=[], allocation={},
        )
        assert report.nav.total_nav == 100_000.0


# ===========================================================================
# NAVEngine
# ===========================================================================

class TestNAVEngine:
    def test_init(self):
        nav = NAVEngine(100_000.0)
        assert nav.high_water_mark == 100_000.0
        assert nav.latest() is None

    def test_compute_nav_empty(self):
        nav = NAVEngine(100_000.0)
        snapshot = nav.compute_nav({})
        assert snapshot.total_nav == 100_000.0
        assert snapshot.cash == 100_000.0
        assert snapshot.drawdown_pct == 0.0

    def test_compute_nav_with_engine(self, engine):
        nav = NAVEngine(10_000.0)
        nav.set_allocation("strat1", 10_000.0)
        snapshot = nav.compute_nav({"strat1": engine})
        assert snapshot.total_nav == 10_000.0
        assert "strat1" in snapshot.strategy_navs

    def test_high_water_mark_updates(self):
        nav = NAVEngine(100.0)
        # Simulate profit via mock
        class MockEngine:
            def status(self):
                class S:
                    total_realized_pnl = 50.0
                    total_unrealized_pnl = 0.0
                return S()
            def positions(self):
                return []

        nav.set_allocation("s1", 100.0)
        snap = nav.compute_nav({"s1": MockEngine()})
        assert snap.total_nav == 150.0
        assert nav.high_water_mark == 150.0

    def test_drawdown_computation(self):
        nav = NAVEngine(100.0)
        class MockEngine:
            def __init__(self, pnl):
                self._pnl = pnl
            def status(self):
                pnl = self._pnl
                class S:
                    total_realized_pnl = pnl
                    total_unrealized_pnl = 0.0
                return S()
            def positions(self):
                return []

        nav.set_allocation("s1", 100.0)
        # First: profit to set HWM
        nav.compute_nav({"s1": MockEngine(50.0)})
        assert nav.high_water_mark == 150.0

        # Second: loss from HWM
        snap = nav.compute_nav({"s1": MockEngine(-10.0)})
        assert snap.total_nav == 90.0
        expected_dd = (150.0 - 90.0) / 150.0 * 100.0
        assert abs(snap.drawdown_pct - expected_dd) < 0.01

    def test_history(self):
        nav = NAVEngine(100.0)
        nav.compute_nav({})
        nav.compute_nav({})
        nav.compute_nav({})
        history = nav.history(2)
        assert len(history) == 2

    def test_latest(self):
        nav = NAVEngine(100.0)
        nav.compute_nav({})
        assert nav.latest() is not None
        assert nav.latest().total_nav == 100.0

    def test_remove_strategy(self):
        nav = NAVEngine(100.0)
        nav.set_allocation("s1", 50.0)
        nav.remove_strategy("s1")
        # Should not error
        nav.remove_strategy("nonexistent")

    def test_thread_safety(self):
        nav = NAVEngine(100.0)
        errors = []

        def writer():
            for i in range(100):
                try:
                    nav.compute_nav({})
                except Exception as e:
                    errors.append(e)

        def reader():
            for i in range(100):
                try:
                    nav.history()
                    nav.latest()
                except Exception as e:
                    errors.append(e)

        threads = [threading.Thread(target=writer), threading.Thread(target=reader)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        assert not errors


# ===========================================================================
# CapitalAllocator
# ===========================================================================

class TestCapitalAllocator:
    def test_equal_allocation(self, fund_config):
        alloc = CapitalAllocator(fund_config)
        result = alloc.allocate(["s1", "s2", "s3"])
        expected = 100_000.0 / 3
        for v in result.values():
            assert abs(v - expected) < 0.01

    def test_equal_single_strategy(self, fund_config):
        alloc = CapitalAllocator(fund_config)
        result = alloc.allocate(["s1"])
        assert abs(result["s1"] - 100_000.0) < 0.01

    def test_empty_strategies(self, fund_config):
        alloc = CapitalAllocator(fund_config)
        result = alloc.allocate([])
        assert result == {}

    def test_custom_allocation(self):
        config = FundConfig(
            total_capital=100_000.0,
            allocation_method=AllocationMethod.CUSTOM,
            custom_weights={"s1": 0.6, "s2": 0.4},
        )
        alloc = CapitalAllocator(config)
        result = alloc.allocate(["s1", "s2"])
        assert abs(result["s1"] - 60_000.0) < 0.01
        assert abs(result["s2"] - 40_000.0) < 0.01

    def test_custom_with_missing_weights(self):
        config = FundConfig(
            total_capital=100_000.0,
            allocation_method=AllocationMethod.CUSTOM,
            custom_weights={"s1": 0.5},
        )
        alloc = CapitalAllocator(config)
        result = alloc.allocate(["s1", "s2", "s3"])
        assert abs(result["s1"] - 50_000.0) < 0.01
        # Remaining 50k split equally between s2 and s3
        assert abs(result["s2"] - 25_000.0) < 0.01
        assert abs(result["s3"] - 25_000.0) < 0.01

    def test_performance_allocation_fallback(self, fund_config):
        fund_config.allocation_method = AllocationMethod.PERFORMANCE
        alloc = CapitalAllocator(fund_config)
        # No engines -> falls back to equal
        result = alloc.allocate(["s1", "s2"])
        assert abs(result["s1"] - 50_000.0) < 0.01

    def test_risk_parity_fallback(self, fund_config):
        fund_config.allocation_method = AllocationMethod.RISK_PARITY
        alloc = CapitalAllocator(fund_config)
        # No engines -> falls back to equal
        result = alloc.allocate(["s1", "s2"])
        assert abs(result["s1"] - 50_000.0) < 0.01

    def test_set_custom_weight(self):
        config = FundConfig(
            total_capital=100_000.0,
            allocation_method=AllocationMethod.CUSTOM,
        )
        alloc = CapitalAllocator(config)
        alloc.set_custom_weight("s1", 0.7)
        alloc.set_custom_weight("s2", 0.3)
        result = alloc.allocate(["s1", "s2"])
        assert abs(result["s1"] - 70_000.0) < 0.01

    def test_remove_weight(self):
        config = FundConfig(
            total_capital=100_000.0,
            allocation_method=AllocationMethod.CUSTOM,
            custom_weights={"s1": 0.5},
        )
        alloc = CapitalAllocator(config)
        alloc.remove_weight("s1")
        result = alloc.allocate(["s1", "s2"])
        # Both unweighted now, equal split
        assert abs(result["s1"] - 50_000.0) < 0.01


# ===========================================================================
# FundRiskMonitor
# ===========================================================================

class TestFundRiskMonitor:
    def test_no_alerts_when_healthy(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=100_000.0, cash=50_000.0,
            positions_value=50_000.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        alerts = risk.check(nav, {}, [])
        assert len(alerts) == 0
        assert not risk.fund_kill_switch_active

    def test_fund_drawdown_triggers_kill_switch(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=80_000.0, cash=80_000.0,
            positions_value=0.0, unrealized_pnl=-20_000.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=20.0,
        )
        alerts = risk.check(nav, {}, [])
        assert risk.fund_kill_switch_active
        assert any(a.level == "critical" for a in alerts)

    def test_fund_drawdown_warning(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        # 80% of 15% limit = 12% triggers warning
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=87_000.0, cash=87_000.0,
            positions_value=0.0, unrealized_pnl=-13_000.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=13.0,
        )
        alerts = risk.check(nav, {}, [])
        assert not risk.fund_kill_switch_active
        assert any(a.level == "warning" for a in alerts)

    def test_strategy_drawdown_pauses(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=100_000.0, cash=100_000.0,
            positions_value=0.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        alerts = risk.check(nav, {"s1": 30.0}, [])
        assert "s1" in risk.paused_strategies

    def test_multiple_failures_alert(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=100_000.0, cash=100_000.0,
            positions_value=0.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        alerts = risk.check(nav, {}, ["s1", "s2", "s3"])
        assert any(a.category == "strategy_failure" for a in alerts)

    def test_reset_kill_switch(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=80_000.0, cash=80_000.0,
            positions_value=0.0, unrealized_pnl=-20_000.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=20.0,
        )
        risk.check(nav, {}, [])
        assert risk.fund_kill_switch_active
        risk.reset_kill_switch()
        assert not risk.fund_kill_switch_active

    def test_clear_strategy_pause(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=100_000.0, cash=100_000.0,
            positions_value=0.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        risk.check(nav, {"s1": 30.0}, [])
        assert "s1" in risk.paused_strategies
        risk.clear_strategy_pause("s1")
        assert "s1" not in risk.paused_strategies

    def test_recent_alerts(self, fund_config):
        risk = FundRiskMonitor(fund_config)
        nav = NAVSnapshot(
            timestamp=1.0, total_nav=80_000.0, cash=80_000.0,
            positions_value=0.0, unrealized_pnl=-20_000.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=20.0,
        )
        risk.check(nav, {"s1": 30.0}, ["a", "b", "c"])
        alerts = risk.recent_alerts()
        assert len(alerts) >= 2  # At least fund drawdown + strategy drawdown


# ===========================================================================
# SettlementMonitor
# ===========================================================================

class TestSettlementMonitor:
    def test_empty_check(self):
        sm = SettlementMonitor()
        events = sm.check_settlements("s1", Engine())
        assert events == []
        assert sm.total_settlements == 0

    def test_total_pnl_empty(self):
        sm = SettlementMonitor()
        assert sm.total_pnl == 0.0

    def test_recent_events_empty(self):
        sm = SettlementMonitor()
        assert sm.recent_events() == []


# ===========================================================================
# FundAccounting
# ===========================================================================

class TestFundAccounting:
    def test_record_fee(self, fund_config):
        acct = FundAccounting(fund_config)
        acct.record_fee(FeeType.EXCHANGE, 10.0, "s1", "trading fee")
        assert acct.total_fees() == 10.0
        assert acct.total_fees(FeeType.EXCHANGE) == 10.0
        assert acct.total_fees(FeeType.MANAGEMENT) == 0.0

    def test_management_fee(self, fund_config):
        acct = FundAccounting(fund_config)
        # Force elapsed time
        acct._last_mgmt_fee_time = time.time() - 86400  # 1 day ago
        nav = NAVSnapshot(
            timestamp=time.time(), total_nav=100_000.0, cash=100_000.0,
            positions_value=0.0, unrealized_pnl=0.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=0.0,
        )
        fee = acct.compute_management_fee(nav)
        # 200 bps = 2% annual, for 1 day: 100k * 0.02 / 365 ~= 5.48
        assert 4.0 < fee < 7.0

    def test_performance_fee_no_profit(self, fund_config):
        acct = FundAccounting(fund_config)
        nav = NAVSnapshot(
            timestamp=time.time(), total_nav=90_000.0, cash=90_000.0,
            positions_value=0.0, unrealized_pnl=-10_000.0,
            realized_pnl=0.0, high_water_mark=100_000.0, drawdown_pct=10.0,
        )
        fee = acct.compute_performance_fee(nav)
        assert fee == 0.0

    def test_performance_fee_with_profit(self, fund_config):
        acct = FundAccounting(fund_config)
        nav = NAVSnapshot(
            timestamp=time.time(), total_nav=120_000.0, cash=120_000.0,
            positions_value=0.0, unrealized_pnl=20_000.0,
            realized_pnl=0.0, high_water_mark=120_000.0, drawdown_pct=0.0,
        )
        fee = acct.compute_performance_fee(nav)
        # 20% of 20k profit = 4k
        assert abs(fee - 4_000.0) < 0.01

    def test_fees_by_strategy(self, fund_config):
        acct = FundAccounting(fund_config)
        acct.record_fee(FeeType.EXCHANGE, 10.0, "s1")
        acct.record_fee(FeeType.EXCHANGE, 20.0, "s2")
        acct.record_fee(FeeType.SLIPPAGE, 5.0, "s1")
        by_strat = acct.fees_by_strategy()
        assert abs(by_strat["s1"] - 15.0) < 0.01
        assert abs(by_strat["s2"] - 20.0) < 0.01

    def test_recent_fees(self, fund_config):
        acct = FundAccounting(fund_config)
        for i in range(5):
            acct.record_fee(FeeType.EXCHANGE, float(i), "s1")
        recent = acct.recent_fees(3)
        assert len(recent) == 3

    def test_pnl_attribution(self, fund_config, engine):
        acct = FundAccounting(fund_config)
        result = acct.pnl_attribution({"s1": engine})
        assert "s1" in result
        assert "realized" in result["s1"]
        assert "net" in result["s1"]


# ===========================================================================
# MarketScorer
# ===========================================================================

class TestMarketScorer:
    def test_score_market(self):
        scorer = MarketScorer()
        score = scorer.score_market(
            market_id="m1", exchange="poly",
            price=0.55, volume_24h=50_000.0,
            spread=0.02, edge=0.1, fit=0.8,
        )
        assert score.market_id == "m1"
        assert 0.0 <= score.composite_score <= 1.0
        assert 0.0 <= score.liquidity_score <= 1.0

    def test_score_zero_volume(self):
        scorer = MarketScorer()
        score = scorer.score_market(
            market_id="m1", exchange="poly",
            price=0.5, volume_24h=0.0,
            spread=0.1,
        )
        assert score.liquidity_score < 0.5  # Poor liquidity

    def test_score_tight_spread(self):
        scorer = MarketScorer()
        score = scorer.score_market(
            market_id="m1", exchange="poly",
            price=0.5, volume_24h=100_000.0,
            spread=0.001, edge=0.5,
        )
        assert score.composite_score > 0.5  # Good market

    def test_rank_markets(self):
        scorer = MarketScorer()
        scores = [
            scorer.score_market("m1", "poly", 0.5, 1000, 0.1, edge=0.1),
            scorer.score_market("m2", "poly", 0.5, 50000, 0.01, edge=0.5),
            scorer.score_market("m3", "poly", 0.5, 10000, 0.05, edge=0.3),
        ]
        ranked = scorer.rank_markets(scores, top_n=2)
        assert len(ranked) == 2
        assert ranked[0].composite_score >= ranked[1].composite_score

    def test_filter_tradeable(self):
        scorer = MarketScorer()
        scores = [
            scorer.score_market("m1", "poly", 0.5, 0, 0.2, edge=0.0),
            scorer.score_market("m2", "poly", 0.5, 100000, 0.01, edge=0.8),
        ]
        filtered = scorer.filter_tradeable(scores, min_score=0.3)
        assert any(s.market_id == "m2" for s in filtered)

    def test_edge_clamping(self):
        scorer = MarketScorer()
        score = scorer.score_market(
            "m1", "poly", 0.5, 10000, 0.05,
            edge=1.5,  # Above 1.0
        )
        assert score.edge_score == 1.0

    def test_fit_clamping(self):
        scorer = MarketScorer()
        score = scorer.score_market(
            "m1", "poly", 0.5, 10000, 0.05,
            fit=-0.5,  # Below 0.0
        )
        assert score.fit_score == 0.0


# ===========================================================================
# StrategyController
# ===========================================================================

class TestStrategyController:
    def test_deploy_strategy(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(
            name="test_strat",
            pipeline=[lambda ctx: None],
            markets=["m1"],
            mode="paper",
        )
        name = ctrl.deploy(config, 10_000.0)
        assert name == "test_strat"
        assert "test_strat" in ctrl.strategy_names

        # Let thread start
        time.sleep(0.1)
        status = ctrl.status("test_strat")
        assert status.name == "test_strat"
        assert status.allocated_capital == 10_000.0

        ctrl.stop("test_strat")

    def test_deploy_duplicate_raises(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        with pytest.raises(ValueError, match="already exists"):
            ctrl.deploy(config, 1000.0)
        ctrl.stop("s1")

    def test_deploy_max_strategies(self):
        config = FundConfig(max_strategies=1)
        ctrl = StrategyController(config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        with pytest.raises(ValueError, match="Max strategies"):
            ctrl.deploy(StrategyConfig(name="s2", pipeline=[lambda ctx: None]), 1000.0)
        ctrl.stop("s1")

    def test_stop_strategy(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        time.sleep(0.1)
        ctrl.stop("s1")
        status = ctrl.status("s1")
        assert status.state in (StrategyState.STOPPED, StrategyState.STOPPING)

    def test_pause_resume(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        time.sleep(0.1)

        ctrl.pause("s1")
        assert ctrl.status("s1").state == StrategyState.PAUSED

        ctrl.resume("s1")
        assert ctrl.status("s1").state == StrategyState.RUNNING

        ctrl.stop("s1")

    def test_retire(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        time.sleep(0.1)
        ctrl.retire("s1")
        assert ctrl.status("s1").state == StrategyState.RETIRED

    def test_remove(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        time.sleep(0.1)
        ctrl.stop("s1")
        ctrl.remove("s1")
        assert "s1" not in ctrl.strategy_names

    def test_remove_running_raises(self, fund_config):
        ctrl = StrategyController(fund_config)
        config = StrategyConfig(name="s1", pipeline=[lambda ctx: None])
        ctrl.deploy(config, 1000.0)
        time.sleep(0.1)
        with pytest.raises(ValueError, match="Cannot remove"):
            ctrl.remove("s1")
        ctrl.stop("s1")

    def test_all_statuses(self, fund_config):
        ctrl = StrategyController(fund_config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        ctrl.deploy(StrategyConfig(name="s2", pipeline=[lambda ctx: None]), 2000.0)
        time.sleep(0.1)
        statuses = ctrl.all_statuses()
        assert len(statuses) == 2
        ctrl.stop_all()

    def test_running_count(self, fund_config):
        ctrl = StrategyController(fund_config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        time.sleep(0.1)
        assert ctrl.running_count() == 1
        ctrl.stop("s1")

    def test_engines_property(self, fund_config):
        ctrl = StrategyController(fund_config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        time.sleep(0.1)
        engines = ctrl.engines
        assert "s1" in engines
        assert isinstance(engines["s1"], Engine)
        ctrl.stop("s1")

    def test_strategy_drawdowns(self, fund_config):
        ctrl = StrategyController(fund_config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        time.sleep(0.1)
        dd = ctrl.strategy_drawdowns()
        assert "s1" in dd
        ctrl.stop("s1")

    def test_stop_nonexistent_raises(self, fund_config):
        ctrl = StrategyController(fund_config)
        with pytest.raises(ValueError, match="not found"):
            ctrl.stop("nonexistent")

    def test_pause_stopped_raises(self, fund_config):
        ctrl = StrategyController(fund_config)
        ctrl.deploy(StrategyConfig(name="s1", pipeline=[lambda ctx: None]), 1000.0)
        time.sleep(0.1)
        ctrl.stop("s1")
        with pytest.raises(ValueError, match="not running"):
            ctrl.pause("s1")


# ===========================================================================
# FundManager (integration)
# ===========================================================================

class TestFundManager:
    def test_init_defaults(self):
        fund = FundManager()
        assert fund.config.total_capital == 100_000.0
        assert not fund.is_running

    def test_init_custom_config(self):
        config = FundConfig(total_capital=500_000.0, max_strategies=5)
        fund = FundManager(config)
        assert fund.config.total_capital == 500_000.0

    def test_add_strategy_before_start(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        # Not started yet, strategy is queued
        assert not fund.is_running
        assert len(fund._pending_configs) == 1

    def test_start_stop(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        assert fund.is_running
        time.sleep(0.2)

        fund.stop()
        assert not fund.is_running

    def test_status(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        status = fund.status()
        assert status["running"] is True
        assert status["total_capital"] == 10_000.0
        assert status["total_strategies"] >= 1

        fund.stop()

    def test_report(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        report = fund.report()
        assert isinstance(report, FundReport)
        assert report.nav.total_nav > 0

        fund.stop()

    def test_properties(self):
        fund = FundManager()
        assert isinstance(fund.nav_engine, NAVEngine)
        assert isinstance(fund.allocator, CapitalAllocator)
        assert isinstance(fund.controller, StrategyController)
        assert isinstance(fund.risk_monitor, FundRiskMonitor)
        assert isinstance(fund.settlement_monitor, SettlementMonitor)
        assert isinstance(fund.accounting, FundAccounting)
        assert isinstance(fund.scorer, MarketScorer)

    def test_nav_history(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.start()
        time.sleep(0.2)
        history = fund.nav_history()
        # Oversight loop may or may not have run yet
        assert isinstance(history, list)
        fund.stop()

    def test_double_start_warns(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.start()
        fund.start()  # Should warn but not error
        fund.stop()

    def test_pnl_attribution(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        attr = fund.pnl_attribution()
        assert "s1" in attr

        fund.stop()

    def test_pause_resume_strategy(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        fund.pause_strategy("s1")
        status = fund.controller.status("s1")
        assert status.state == StrategyState.PAUSED

        fund.resume_strategy("s1")
        status = fund.controller.status("s1")
        assert status.state == StrategyState.RUNNING

        fund.stop()

    def test_remove_strategy(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        fund.remove_strategy("s1")
        assert "s1" not in fund.controller.strategy_names

        fund.stop()

    def test_scale_strategy(self):
        fund = FundManager(FundConfig(total_capital=10_000.0))
        fund.add_strategy(StrategyConfig(
            name="s1", pipeline=[lambda ctx: None], markets=["m1"],
        ))
        fund.start()
        time.sleep(0.2)

        fund.scale_strategy("s1", 5_000.0)
        # Verify allocation updated

        fund.stop()


# ===========================================================================
# Import test
# ===========================================================================

class TestImports:
    def test_fund_imports_from_horizon(self):
        """All fund types should be importable from horizon directly."""
        from horizon import (
            FundManager,
            FundConfig,
            StrategyConfig,
            AllocationMethod,
            FeeType,
            StrategyState,
            FundReport,
            MarketScore,
            NAVSnapshot,
            SettlementEvent,
            StrategyStatus,
            RiskAlert,
            NAVEngine,
            CapitalAllocator,
            StrategyController,
            FundRiskMonitor,
            SettlementMonitor,
            FundAccounting,
            MarketScorer,
        )
        assert FundManager is not None
        assert FundConfig is not None

    def test_fund_submodule_imports(self):
        """Sub-modules should be importable directly."""
        from horizon.fund._types import FundConfig
        from horizon.fund._nav import NAVEngine
        from horizon.fund._allocator import CapitalAllocator
        from horizon.fund._controller import StrategyController
        from horizon.fund._fund_risk import FundRiskMonitor
        from horizon.fund._settlement import SettlementMonitor
        from horizon.fund._accounting import FundAccounting
        from horizon.fund._scorer import MarketScorer
        assert True
