"""Tests for PlaybookRegistry.search().

業務目標
--------
search() 讓用戶在不手動遍歷所有已註冊策略的情況下，透過多維度條件快速定位目標
策略與 blueprint 組合：

- 找出特定 broker（instrument partial match）下所有 LONG 策略
- 找出特定時區 + Trend 類型的 blueprint
- 跨策略對比相同 direction/trend_type 的 blueprint 清單

搜尋語意
--------
- ``instrument`` / ``tz``：策略層級，支援 list（聯集，符合任一即通過）。
  ``instrument`` 為 partial match；``tz`` 為 exact match。
- ``direction`` / ``trend_type``：**blueprint 層級**。每個符合策略的
  ``advanced_blueprints`` 只保留通過篩選的項目；base_blueprint 不符合則
  整個策略被排除。

測試目標
--------
1. instrument list：``["FTMO", "PEPPER"]`` → 兩家 broker 的策略都要回傳
2. direction=LONG → 排除 SHORT advanced_blueprints；base 是 SHORT 的策略整個排除
3. trend_type list：``["Trend", "Range"]`` → 兩種 trend_type 的 blueprint 都保留
4. 多條件 AND：instrument="FTMO" + direction="Long" → 只有 FTMO 且含 LONG bp 的策略
5. 所有參數必須以 keyword 方式傳入（強制 keyword-only）
"""

from __future__ import annotations

import pytest
from tradepose_client.registry.core import PlaybookRegistry
from tradepose_models.enums import Freq, TradeDirection, TrendType
from tradepose_models.strategy.blueprint import Blueprint
from tradepose_models.strategy.config import StrategyConfig

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_bp(
    name: str,
    direction: TradeDirection = TradeDirection.LONG,
    trend_type: TrendType = TrendType.TREND,
) -> Blueprint:
    return Blueprint(
        name=name,
        direction=direction,
        trend_type=trend_type,
        entry_triggers=[],
        exit_triggers=[],
    )


def _make_config(
    name: str,
    instrument: str,
    direction: TradeDirection = TradeDirection.LONG,
    trend_type: TrendType = TrendType.TREND,
    tz: str = "UTC",
    base_freq: str = "1h",
    adv_blueprints: list[Blueprint] | None = None,
) -> StrategyConfig:
    return StrategyConfig(
        name=name,
        base_instrument=instrument,
        base_freq=base_freq,
        tz=tz,
        base_blueprint=_make_bp(f"{name}_base", direction, trend_type),
        advanced_blueprints=adv_blueprints or [],
        indicators=[],
    )


# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------


@pytest.fixture
def registry(tmp_path) -> PlaybookRegistry:
    """Registry pre-loaded with a small set of strategies for testing."""
    reg = PlaybookRegistry(root=tmp_path / "playbook")

    # FTMO LONG Trend — 2 adv blueprints (both LONG/Trend), freq=15min
    reg.register_strategy(
        _make_config(
            "ftmo_long_trend",
            "FTMO_NAS100",
            direction=TradeDirection.LONG,
            trend_type=TrendType.TREND,
            tz="Europe/Athens",
            base_freq="15min",
            adv_blueprints=[
                _make_bp("adv_long_trend_a", TradeDirection.LONG, TrendType.TREND),
                _make_bp("adv_long_trend_b", TradeDirection.LONG, TrendType.TREND),
            ],
        )
    )

    # FTMO SHORT Trend, freq=15min
    reg.register_strategy(
        _make_config(
            "ftmo_short_trend",
            "FTMO_NAS100",
            direction=TradeDirection.SHORT,
            trend_type=TrendType.TREND,
            tz="Europe/Athens",
            base_freq="15min",
            adv_blueprints=[
                _make_bp("adv_short_trend", TradeDirection.SHORT, TrendType.TREND),
            ],
        )
    )

    # PEPPER LONG Range, freq=1D
    reg.register_strategy(
        _make_config(
            "pepper_long_range",
            "PEPPER_JP225",
            direction=TradeDirection.LONG,
            trend_type=TrendType.RANGE,
            tz="Asia/Tokyo",
            base_freq="1D",
            adv_blueprints=[
                _make_bp("adv_long_range", TradeDirection.LONG, TrendType.RANGE),
            ],
        )
    )

    # Mixed strategy: base LONG/Trend, adv has both Trend and Range blueprints, freq=1h
    reg.register_strategy(
        _make_config(
            "ftmo_long_mixed",
            "FTMO_US30",
            direction=TradeDirection.LONG,
            trend_type=TrendType.TREND,
            tz="Europe/Athens",
            base_freq="1h",
            adv_blueprints=[
                _make_bp("adv_mixed_trend", TradeDirection.LONG, TrendType.TREND),
                _make_bp("adv_mixed_range", TradeDirection.LONG, TrendType.RANGE),
            ],
        )
    )

    return reg


# ---------------------------------------------------------------------------
# keyword-only enforcement
# ---------------------------------------------------------------------------


class TestKeywordOnly:
    """search() must enforce keyword-only arguments."""

    def test_positional_arg_raises(self, registry: PlaybookRegistry) -> None:
        with pytest.raises(TypeError):
            registry.search("FTMO")  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# instrument filter
# ---------------------------------------------------------------------------


class TestInstrumentFilter:
    def test_single_string_partial_match(self, registry: PlaybookRegistry) -> None:
        results = registry.search(instrument="FTMO")
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "ftmo_short_trend" in names
        assert "ftmo_long_mixed" in names
        assert "pepper_long_range" not in names

    def test_single_string_no_match(self, registry: PlaybookRegistry) -> None:
        results = registry.search(instrument="UNKNOWN")
        assert results == []

    def test_list_union_match(self, registry: PlaybookRegistry) -> None:
        """list → both FTMO and PEPPER strategies are returned."""
        results = registry.search(instrument=["FTMO", "PEPPER"])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "pepper_long_range" in names

    def test_list_partial_match(self, registry: PlaybookRegistry) -> None:
        """Partial match works within list items."""
        results = registry.search(instrument=["NAS100"])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "ftmo_short_trend" in names
        assert "pepper_long_range" not in names


# ---------------------------------------------------------------------------
# tz filter
# ---------------------------------------------------------------------------


class TestTzFilter:
    def test_single_tz_exact_match(self, registry: PlaybookRegistry) -> None:
        results = registry.search(tz="Asia/Tokyo")
        assert len(results) == 1
        assert results[0].name == "pepper_long_range"

    def test_tz_list_union(self, registry: PlaybookRegistry) -> None:
        results = registry.search(tz=["Europe/Athens", "Asia/Tokyo"])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "pepper_long_range" in names

    def test_tz_no_match(self, registry: PlaybookRegistry) -> None:
        results = registry.search(tz="America/New_York")
        assert results == []


# ---------------------------------------------------------------------------
# direction filter (blueprint-level)
# ---------------------------------------------------------------------------


class TestDirectionFilter:
    def test_long_excludes_short_strategy(self, registry: PlaybookRegistry) -> None:
        """direction=LONG must exclude strategies whose base_blueprint is SHORT."""
        results = registry.search(direction="Long")
        names = {c.name for c in results}
        assert "ftmo_short_trend" not in names

    def test_long_keeps_long_strategies(self, registry: PlaybookRegistry) -> None:
        results = registry.search(direction=TradeDirection.LONG)
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "pepper_long_range" in names

    def test_short_returns_only_short(self, registry: PlaybookRegistry) -> None:
        results = registry.search(direction="Short")
        names = {c.name for c in results}
        assert names == {"ftmo_short_trend"}

    def test_direction_filters_advanced_blueprints(self, registry: PlaybookRegistry) -> None:
        """Advanced blueprints that don't match direction are stripped out."""
        # ftmo_long_trend has 2 adv bps, both LONG — should keep both
        results = registry.search(direction="Long")
        ftmo = next(c for c in results if c.name == "ftmo_long_trend")
        assert len(ftmo.advanced_blueprints) == 2

    def test_direction_case_insensitive(self, registry: PlaybookRegistry) -> None:
        results_lower = registry.search(direction="long")
        results_upper = registry.search(direction="Long")
        assert {c.name for c in results_lower} == {c.name for c in results_upper}


# ---------------------------------------------------------------------------
# trend_type filter (blueprint-level)
# ---------------------------------------------------------------------------


class TestTrendTypeFilter:
    def test_single_trend_type(self, registry: PlaybookRegistry) -> None:
        results = registry.search(trend_type="Range")
        names = {c.name for c in results}
        assert "pepper_long_range" in names
        # ftmo_long_trend base is Trend → excluded
        assert "ftmo_long_trend" not in names

    def test_trend_type_list_union(self, registry: PlaybookRegistry) -> None:
        """list → both Trend and Range strategies are returned."""
        results = registry.search(trend_type=["Trend", "Range"])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "pepper_long_range" in names

    def test_trend_type_filters_adv_blueprints(self, registry: PlaybookRegistry) -> None:
        """Mixed adv bps: only Trend ones are kept when trend_type='Trend'."""
        results = registry.search(trend_type=TrendType.TREND)
        mixed = next((c for c in results if c.name == "ftmo_long_mixed"), None)
        assert mixed is not None
        adv_names = {bp.name for bp in mixed.advanced_blueprints}
        assert "adv_mixed_trend" in adv_names
        assert "adv_mixed_range" not in adv_names

    def test_trend_type_list_keeps_both_in_mixed(self, registry: PlaybookRegistry) -> None:
        """When searching Trend+Range, mixed strategy keeps all adv bps."""
        results = registry.search(trend_type=["Trend", "Range"])
        mixed = next((c for c in results if c.name == "ftmo_long_mixed"), None)
        assert mixed is not None
        assert len(mixed.advanced_blueprints) == 2


# ---------------------------------------------------------------------------
# combined filters
# ---------------------------------------------------------------------------


class TestCombinedFilters:
    def test_instrument_and_direction(self, registry: PlaybookRegistry) -> None:
        """instrument='FTMO' + direction='Long' → only LONG FTMO strategies."""
        results = registry.search(instrument="FTMO", direction="Long")
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "ftmo_long_mixed" in names
        assert "ftmo_short_trend" not in names
        assert "pepper_long_range" not in names

    def test_instrument_list_and_direction(self, registry: PlaybookRegistry) -> None:
        results = registry.search(instrument=["FTMO", "PEPPER"], direction="Long")
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "pepper_long_range" in names
        assert "ftmo_short_trend" not in names

    def test_all_none_returns_all(self, registry: PlaybookRegistry) -> None:
        results = registry.search()
        assert len(results) == 4

    def test_tz_and_trend_type(self, registry: PlaybookRegistry) -> None:
        results = registry.search(tz="Europe/Athens", trend_type="Trend")
        names = {c.name for c in results}
        # ftmo_long_trend (Trend) and ftmo_long_mixed (base Trend) in Athens
        # ftmo_short_trend (Trend base) also in Athens
        assert "ftmo_long_trend" in names
        assert "ftmo_short_trend" in names
        assert "pepper_long_range" not in names


# ---------------------------------------------------------------------------
# base_freq filter
# ---------------------------------------------------------------------------


class TestBaseFreqFilter:
    def test_single_freq_string(self, registry: PlaybookRegistry) -> None:
        results = registry.search(base_freq="1D")
        names = {c.name for c in results}
        assert names == {"pepper_long_range"}

    def test_single_freq_enum(self, registry: PlaybookRegistry) -> None:
        results = registry.search(base_freq=Freq.DAY_1)
        names = {c.name for c in results}
        assert names == {"pepper_long_range"}

    def test_freq_list_union(self, registry: PlaybookRegistry) -> None:
        """[MIN_15, DAY_1] → both 15min and 1D strategies are returned."""
        results = registry.search(base_freq=[Freq.MIN_15, Freq.DAY_1])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names
        assert "ftmo_short_trend" in names
        assert "pepper_long_range" in names
        assert "ftmo_long_mixed" not in names  # freq=1h

    def test_freq_list_string(self, registry: PlaybookRegistry) -> None:
        results = registry.search(base_freq=["15min", "1h"])
        names = {c.name for c in results}
        assert "ftmo_long_trend" in names  # 15min
        assert "ftmo_long_mixed" in names  # 1h
        assert "pepper_long_range" not in names  # 1D

    def test_freq_no_match(self, registry: PlaybookRegistry) -> None:
        results = registry.search(base_freq="4h")
        assert results == []

    def test_freq_combined_with_direction(self, registry: PlaybookRegistry) -> None:
        """15min + LONG → only LONG 15min strategies."""
        results = registry.search(base_freq="15min", direction="Long")
        names = {c.name for c in results}
        assert names == {"ftmo_long_trend"}
        assert "ftmo_short_trend" not in names
