"""Tests for strategy/playbook Pydantic models."""

from __future__ import annotations

from datetime import date

import pytest
from pydantic import ValidationError

from src.strategy.models import (
    CrossMarketContext,
    DayPlaybook,
    GlobalRule,
    MarketOutlook,
    PlaybookStatus,
    ScenarioAction,
    StockCondition,
    StockPlaybook,
    StockScenario,
)

# ---------------------------------------------------------------------------
# StockCondition
# ---------------------------------------------------------------------------


class TestStockCondition:
    def test_empty_condition(self) -> None:
        cond = StockCondition()
        assert not cond.has_any_condition()

    def test_single_field(self) -> None:
        cond = StockCondition(rsi_below=30.0)
        assert cond.has_any_condition()

    def test_multiple_fields(self) -> None:
        cond = StockCondition(rsi_below=25.0, volume_ratio_above=3.0)
        assert cond.has_any_condition()

    def test_all_fields(self) -> None:
        cond = StockCondition(
            rsi_below=30,
            rsi_above=10,
            volume_ratio_above=2.0,
            volume_ratio_below=10.0,
            price_above=1000,
            price_below=50000,
            price_change_pct_above=-5.0,
            price_change_pct_below=5.0,
        )
        assert cond.has_any_condition()


# ---------------------------------------------------------------------------
# StockScenario
# ---------------------------------------------------------------------------


class TestStockScenario:
    def test_valid_scenario(self) -> None:
        s = StockScenario(
            condition=StockCondition(rsi_below=25.0),
            action=ScenarioAction.BUY,
            confidence=85,
            allocation_pct=15.0,
            stop_loss_pct=-2.0,
            take_profit_pct=3.0,
            rationale="Oversold bounce expected",
        )
        assert s.action == ScenarioAction.BUY
        assert s.confidence == 85

    def test_confidence_too_high(self) -> None:
        with pytest.raises(ValidationError):
            StockScenario(
                condition=StockCondition(),
                action=ScenarioAction.BUY,
                confidence=101,
            )

    def test_confidence_too_low(self) -> None:
        with pytest.raises(ValidationError):
            StockScenario(
                condition=StockCondition(),
                action=ScenarioAction.BUY,
                confidence=-1,
            )

    def test_allocation_too_high(self) -> None:
        with pytest.raises(ValidationError):
            StockScenario(
                condition=StockCondition(),
                action=ScenarioAction.BUY,
                confidence=80,
                allocation_pct=101.0,
            )

    def test_stop_loss_must_be_negative(self) -> None:
        with pytest.raises(ValidationError):
            StockScenario(
                condition=StockCondition(),
                action=ScenarioAction.BUY,
                confidence=80,
                stop_loss_pct=1.0,
            )

    def test_take_profit_must_be_positive(self) -> None:
        with pytest.raises(ValidationError):
            StockScenario(
                condition=StockCondition(),
                action=ScenarioAction.BUY,
                confidence=80,
                take_profit_pct=-1.0,
            )

    def test_defaults(self) -> None:
        s = StockScenario(
            condition=StockCondition(),
            action=ScenarioAction.HOLD,
            confidence=50,
        )
        assert s.allocation_pct == 10.0
        assert s.stop_loss_pct == -2.0
        assert s.take_profit_pct == 3.0
        assert s.rationale == ""


# ---------------------------------------------------------------------------
# StockPlaybook
# ---------------------------------------------------------------------------


class TestStockPlaybook:
    def test_valid_playbook(self) -> None:
        pb = StockPlaybook(
            stock_code="005930",
            stock_name="Samsung Electronics",
            scenarios=[
                StockScenario(
                    condition=StockCondition(rsi_below=25.0),
                    action=ScenarioAction.BUY,
                    confidence=85,
                ),
            ],
        )
        assert pb.stock_code == "005930"
        assert len(pb.scenarios) == 1

    def test_empty_scenarios_rejected(self) -> None:
        with pytest.raises(ValidationError):
            StockPlaybook(
                stock_code="005930",
                scenarios=[],
            )

    def test_multiple_scenarios(self) -> None:
        pb = StockPlaybook(
            stock_code="AAPL",
            scenarios=[
                StockScenario(
                    condition=StockCondition(rsi_below=25.0),
                    action=ScenarioAction.BUY,
                    confidence=85,
                ),
                StockScenario(
                    condition=StockCondition(rsi_above=75.0),
                    action=ScenarioAction.SELL,
                    confidence=80,
                ),
            ],
        )
        assert len(pb.scenarios) == 2


# ---------------------------------------------------------------------------
# GlobalRule
# ---------------------------------------------------------------------------


class TestGlobalRule:
    def test_valid_rule(self) -> None:
        rule = GlobalRule(
            condition="portfolio_pnl_pct < -2.0",
            action=ScenarioAction.REDUCE_ALL,
            rationale="Risk limit approaching",
        )
        assert rule.action == ScenarioAction.REDUCE_ALL

    def test_hold_rule(self) -> None:
        rule = GlobalRule(
            condition="volatility_index > 30",
            action=ScenarioAction.HOLD,
        )
        assert rule.rationale == ""


# ---------------------------------------------------------------------------
# CrossMarketContext
# ---------------------------------------------------------------------------


class TestCrossMarketContext:
    def test_valid_context(self) -> None:
        ctx = CrossMarketContext(
            market="US",
            date="2026-02-07",
            total_pnl=-1.5,
            win_rate=40.0,
            index_change_pct=-2.3,
            key_events=["Fed rate decision"],
            lessons=["Avoid tech sector on rate hike days"],
        )
        assert ctx.market == "US"
        assert len(ctx.key_events) == 1

    def test_defaults(self) -> None:
        ctx = CrossMarketContext(market="KR", date="2026-02-07")
        assert ctx.total_pnl == 0.0
        assert ctx.key_events == []
        assert ctx.lessons == []


# ---------------------------------------------------------------------------
# DayPlaybook
# ---------------------------------------------------------------------------


def _make_scenario(rsi_below: float = 25.0) -> StockScenario:
    return StockScenario(
        condition=StockCondition(rsi_below=rsi_below),
        action=ScenarioAction.BUY,
        confidence=85,
    )


def _make_playbook(**kwargs) -> DayPlaybook:
    defaults = {
        "date": date(2026, 2, 7),
        "market": "KR",
        "stock_playbooks": [
            StockPlaybook(stock_code="005930", scenarios=[_make_scenario()]),
        ],
    }
    defaults.update(kwargs)
    return DayPlaybook(**defaults)


class TestDayPlaybook:
    def test_valid_playbook(self) -> None:
        pb = _make_playbook()
        assert pb.market == "KR"
        assert pb.date == date(2026, 2, 7)
        assert pb.default_action == ScenarioAction.HOLD
        assert pb.scenario_count == 1
        assert pb.stock_count == 1

    def test_generated_at_auto_set(self) -> None:
        pb = _make_playbook()
        assert pb.generated_at != ""

    def test_explicit_generated_at(self) -> None:
        pb = _make_playbook(generated_at="2026-02-07T08:30:00")
        assert pb.generated_at == "2026-02-07T08:30:00"

    def test_duplicate_stocks_rejected(self) -> None:
        with pytest.raises(ValidationError):
            DayPlaybook(
                date=date(2026, 2, 7),
                market="KR",
                stock_playbooks=[
                    StockPlaybook(stock_code="005930", scenarios=[_make_scenario()]),
                    StockPlaybook(stock_code="005930", scenarios=[_make_scenario(30)]),
                ],
            )

    def test_empty_stock_playbooks_allowed(self) -> None:
        pb = DayPlaybook(
            date=date(2026, 2, 7),
            market="KR",
            stock_playbooks=[],
        )
        assert pb.stock_count == 0
        assert pb.scenario_count == 0

    def test_get_stock_playbook_found(self) -> None:
        pb = _make_playbook()
        result = pb.get_stock_playbook("005930")
        assert result is not None
        assert result.stock_code == "005930"

    def test_get_stock_playbook_not_found(self) -> None:
        pb = _make_playbook()
        result = pb.get_stock_playbook("AAPL")
        assert result is None

    def test_with_global_rules(self) -> None:
        pb = _make_playbook(
            global_rules=[
                GlobalRule(
                    condition="portfolio_pnl_pct < -2.0",
                    action=ScenarioAction.REDUCE_ALL,
                ),
            ],
        )
        assert len(pb.global_rules) == 1

    def test_with_cross_market_context(self) -> None:
        ctx = CrossMarketContext(market="US", date="2026-02-07", total_pnl=-1.5)
        pb = _make_playbook(cross_market=ctx)
        assert pb.cross_market is not None
        assert pb.cross_market.market == "US"

    def test_market_outlook(self) -> None:
        pb = _make_playbook(market_outlook=MarketOutlook.BEARISH)
        assert pb.market_outlook == MarketOutlook.BEARISH

    def test_multiple_stocks_multiple_scenarios(self) -> None:
        pb = DayPlaybook(
            date=date(2026, 2, 7),
            market="US",
            stock_playbooks=[
                StockPlaybook(
                    stock_code="AAPL",
                    scenarios=[_make_scenario(), _make_scenario(30)],
                ),
                StockPlaybook(
                    stock_code="MSFT",
                    scenarios=[_make_scenario()],
                ),
            ],
        )
        assert pb.stock_count == 2
        assert pb.scenario_count == 3

    def test_serialization_roundtrip(self) -> None:
        pb = _make_playbook(
            market_outlook=MarketOutlook.BULLISH,
            cross_market=CrossMarketContext(market="US", date="2026-02-07"),
        )
        json_str = pb.model_dump_json()
        restored = DayPlaybook.model_validate_json(json_str)
        assert restored.market == pb.market
        assert restored.date == pb.date
        assert restored.scenario_count == pb.scenario_count
        assert restored.cross_market is not None


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


class TestEnums:
    def test_scenario_action_values(self) -> None:
        assert ScenarioAction.BUY.value == "BUY"
        assert ScenarioAction.SELL.value == "SELL"
        assert ScenarioAction.HOLD.value == "HOLD"
        assert ScenarioAction.REDUCE_ALL.value == "REDUCE_ALL"

    def test_market_outlook_values(self) -> None:
        assert len(MarketOutlook) == 5

    def test_playbook_status_values(self) -> None:
        assert PlaybookStatus.READY.value == "ready"
        assert PlaybookStatus.EXPIRED.value == "expired"
