"""Tests for cpz.strategy — Strategy base class and lifecycle."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from cpz.events.bus import EventBus
from cpz.model.events import BarEvent, FillEvent
from cpz.strategy.base import Strategy
from cpz.strategy.config import StrategyConfig
from cpz.strategy.context import Cache, Clock, Portfolio


class SimpleStrategy(Strategy):
    """Test strategy that records lifecycle calls."""

    def __init__(self, config=None):
        super().__init__(config)
        self.started = False
        self.stopped = False
        self.bars_received = []
        self.fills_received = []

    def on_start(self):
        self.started = True

    def on_stop(self):
        self.stopped = True

    def on_bar(self, bar):
        self.bars_received.append(bar)

    def on_order_filled(self, event):
        self.fills_received.append(event)


class TestStrategyConfig:
    def test_defaults(self):
        cfg = StrategyConfig()
        assert cfg.instruments == []
        assert cfg.broker == "alpaca"
        assert cfg.environment == "paper"

    def test_custom_fields(self):
        class MyConfig(StrategyConfig):
            fast: int = 10
            slow: int = 30

        cfg = MyConfig(instruments=["AAPL"], fast=5)
        assert cfg.fast == 5
        assert cfg.slow == 30
        assert cfg.instruments == ["AAPL"]

    def test_serialization(self):
        cfg = StrategyConfig(instruments=["AAPL"], strategy_id="test-123")
        d = cfg.to_dict()
        assert d["strategy_id"] == "test-123"
        assert d["instruments"] == ["AAPL"]

        restored = StrategyConfig.from_dict(d)
        assert restored.strategy_id == "test-123"


class TestStrategy:
    def test_auto_id(self):
        s = SimpleStrategy()
        assert s.strategy_id.startswith("SimpleStrategy-")

    def test_explicit_id(self):
        cfg = StrategyConfig(strategy_id="my-strat-001")
        s = SimpleStrategy(cfg)
        assert s.strategy_id == "my-strat-001"

    def test_lifecycle(self):
        s = SimpleStrategy()
        s.on_start()
        assert s.started
        s.on_stop()
        assert s.stopped

    def test_bar_handler(self):
        s = SimpleStrategy()
        bar = BarEvent(symbol="AAPL", close=150.0)
        s._handle_bar(bar)
        assert len(s.bars_received) == 1
        assert s.bars_received[0].symbol == "AAPL"

    def test_bar_cached(self):
        s = SimpleStrategy()
        bar = BarEvent(symbol="AAPL", close=150.0)
        s._handle_bar(bar)
        cached = s.cache.bars("AAPL")
        assert len(cached) == 1
        assert cached[0].close == 150.0

    def test_fill_updates_portfolio(self):
        s = SimpleStrategy()
        fill = FillEvent(
            symbol="AAPL", side="buy", fill_qty=10, fill_price=150.0,
            commission=0.5, strategy_id=s.strategy_id,
        )
        s._handle_fill(fill)
        assert not s.portfolio.is_flat("AAPL")
        assert s.portfolio.is_long("AAPL")
        assert len(s.fills_received) == 1

    def test_subscribe_bars(self):
        s = SimpleStrategy()
        s.subscribe_bars("AAPL", "1D")
        assert len(s._subscriptions) == 1
        assert s._subscriptions[0] == {"type": "bar", "symbol": "AAPL", "timeframe": "1D"}

    def test_subscribe_bars_dedup(self):
        s = SimpleStrategy()
        s.subscribe_bars("AAPL", "1D")
        s.subscribe_bars("AAPL", "1D")
        assert len(s._subscriptions) == 1

    def test_buy_sell_convenience(self):
        s = SimpleStrategy()
        bus = EventBus()
        s._bus = bus
        submitted = []
        bus.subscribe("order.submit", lambda req: submitted.append(req))

        s.buy("AAPL", qty=10)
        assert len(submitted) == 1
        assert submitted[0].side.value == "buy"

        s.sell("AAPL", qty=10)
        assert len(submitted) == 2
        assert submitted[1].side.value == "sell"

    def test_flatten(self):
        s = SimpleStrategy()
        bus = EventBus()
        s._bus = bus

        fill = FillEvent(
            symbol="AAPL", side="buy", fill_qty=10, fill_price=150.0,
            commission=0, strategy_id=s.strategy_id,
        )
        s._handle_fill(fill)
        assert s.portfolio.is_long("AAPL")

        submitted = []
        bus.subscribe("order.submit", lambda req: submitted.append(req))
        s.flatten("AAPL")
        assert len(submitted) == 1
        assert submitted[0].side.value == "sell"
        assert submitted[0].qty == 10.0

    def test_flatten_when_flat_noop(self):
        s = SimpleStrategy()
        result = s.flatten("AAPL")
        assert result is None

    def test_emit_signal(self):
        s = SimpleStrategy()
        bus = EventBus()
        s._bus = bus
        received = []
        bus.subscribe("signal.my_signal", lambda sig: received.append(sig))

        s.emit_signal("my_signal", value=1.5, data={"key": "val"})
        assert len(received) == 1
        assert received[0].name == "my_signal"
        assert received[0].value == 1.5


class TestPortfolio:
    def test_initial_state(self):
        p = Portfolio(100_000)
        assert p.cash == 100_000
        assert p.equity() == 100_000
        assert p.is_completely_flat()

    def test_apply_fill_buy(self):
        p = Portfolio(100_000)
        fill = FillEvent(symbol="AAPL", side="buy", fill_qty=10, fill_price=150.0, commission=1.0)
        p.apply_fill(fill)
        assert p.is_long("AAPL")
        assert p.cash == 100_000 - 10 * 150 - 1.0
        assert p.commission_total == 1.0

    def test_apply_fill_sell_closes(self):
        p = Portfolio(100_000)
        buy = FillEvent(symbol="AAPL", side="buy", fill_qty=10, fill_price=150.0, commission=0)
        p.apply_fill(buy)

        sell = FillEvent(symbol="AAPL", side="sell", fill_qty=10, fill_price=160.0, commission=0)
        p.apply_fill(sell)
        assert p.is_flat("AAPL")
        assert p.realized_pnl == 100.0  # 10 * (160 - 150)

    def test_positions(self):
        p = Portfolio(100_000)
        fill = FillEvent(symbol="AAPL", side="buy", fill_qty=5, fill_price=100.0, commission=0)
        p.apply_fill(fill)
        positions = p.positions()
        assert "AAPL" in positions
        assert positions["AAPL"].qty == 5

    def test_reset(self):
        p = Portfolio(100_000)
        fill = FillEvent(symbol="AAPL", side="buy", fill_qty=5, fill_price=100.0, commission=0)
        p.apply_fill(fill)
        p.reset()
        assert p.cash == 100_000
        assert p.is_completely_flat()


class TestClock:
    def test_real_time(self):
        c = Clock()
        assert not c.is_simulated
        ts = c.timestamp_ns()
        assert ts > 0

    def test_simulated(self):
        c = Clock()
        c.set_time(1_000_000_000_000)
        assert c.is_simulated
        assert c.timestamp_ns() == 1_000_000_000_000

    def test_reset(self):
        c = Clock()
        c.set_time(1_000)
        c.reset()
        assert not c.is_simulated


class TestCache:
    def test_push_and_retrieve_bars(self):
        c = Cache()
        bar1 = BarEvent(symbol="AAPL", close=150.0)
        bar2 = BarEvent(symbol="AAPL", close=151.0)
        c.push_bar(bar1)
        c.push_bar(bar2)
        bars = c.bars("AAPL")
        assert len(bars) == 2
        assert bars[-1].close == 151.0

    def test_latest_bar(self):
        c = Cache()
        c.push_bar(BarEvent(symbol="AAPL", close=150.0))
        c.push_bar(BarEvent(symbol="AAPL", close=155.0))
        assert c.latest_bar("AAPL").close == 155.0

    def test_empty_latest_bar(self):
        c = Cache()
        assert c.latest_bar("AAPL") is None

    def test_custom_storage(self):
        c = Cache()
        c.set("key1", "value1")
        assert c.get("key1") == "value1"
        assert c.get("missing", "default") == "default"
