"""Execution engine end-to-end with a FAKE data provider + FAKE broker (no network, no IBKR).
Proves the multi-strategy rebalance: netting → reconcile vs broker → whole-share orders → gates."""
from __future__ import annotations

import numpy as np

from fxhnt.application import ExecutionService
from fxhnt.config import ExecutionSettings, Settings
from fxhnt.domain.models import AssetClass, Market, PriceSeries, StrategySpec
from fxhnt.domain.portfolio import Book, StrategyAllocation
from fxhnt.ports.broker import AccountState, Order

SPY = Market(symbol="SPY", asset_class=AssetClass.ETF)


class FakeData:
    name = "fake"

    def fetch(self, market: Market, start=None, end=None) -> PriceSeries:
        close = 100.0 * np.cumprod(1.0 + np.full(400, 0.001))  # uptrend -> trend long
        return PriceSeries(market=market, dates=tuple(str(i) for i in range(400)), close=close)


class FakeBroker:
    name = "fake"

    def __init__(self, state: AccountState) -> None:
        self.state = state
        self.placed: list[Order] = []

    def account_state(self) -> AccountState:
        return self.state

    def place_order(self, order: Order) -> str:
        self.placed.append(order)
        return "Filled"


def _book() -> Book:
    return Book(name="b", max_gross_leverage=1.0, allocations=[
        StrategyAllocation(spec=StrategySpec(kind="trend", params={"window": 50}), market=SPY, weight=1.0),
    ])


def _settings() -> Settings:
    return Settings(execution=ExecutionSettings(allow_live=False))


def test_rebalance_from_flat_places_buy() -> None:
    broker = FakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True))
    svc = ExecutionService(FakeData(), broker, _settings())
    plan = svc.rebalance(_book(), execute=True)
    assert not plan.blocked
    assert plan.executed and len(broker.placed) == 1
    o = broker.placed[0]
    assert o.symbol == "SPY" and o.side == "BUY"
    # ~100% of 100k at price ~149 (100*1.001^399) -> ~670 shares
    assert 600 < o.quantity < 750


def test_in_band_when_already_on_target_no_orders() -> None:
    # already holding ~full target -> hysteresis says do nothing
    broker = FakeBroker(AccountState(nlv=100_000, cash=0, positions={"SPY": 670}, is_paper=True))
    svc = ExecutionService(FakeData(), broker, _settings())
    plan = svc.rebalance(_book(), execute=True)
    assert not plan.executed and not broker.placed
    assert any("in band" in n for n in plan.notes)


def test_live_account_blocked_without_allow_live() -> None:
    broker = FakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=False))
    svc = ExecutionService(FakeData(), broker, _settings())
    plan = svc.rebalance(_book(), execute=True)
    assert plan.blocked and not broker.placed


def test_inconsistent_account_data_blocks() -> None:
    # NLV says 100k but cash+positions says 60k -> 40% gap > data_tol -> refuse
    broker = FakeBroker(AccountState(nlv=100_000, cash=10_000, positions={}, is_paper=True, gross_position_value=50_000))
    svc = ExecutionService(FakeData(), broker, _settings())
    plan = svc.rebalance(_book(), execute=True)
    assert plan.blocked and not broker.placed
