# source: https://raw.githubusercontent.com/shatianming5/Agent_market/389cd928020b2247909d47cecf41a78974d62755/tests/test_strategy_miner.py
"""Smoke tests for strategy_miner module."""
from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest


# ---------------------------------------------------------------------------
# dtypes
# ---------------------------------------------------------------------------


def test_miner_config_from_dict():
    from agent_market.strategy_miner.dtypes import MinerConfig

    cfg = MinerConfig.from_dict({"model": "gpt-4o", "max_iterations": 3, "unknown_key": 99})
    assert cfg.model == "gpt-4o"
    assert cfg.max_iterations == 3
    assert cfg.max_turns == 30  # default


def test_miner_config_nested_sections():
    from agent_market.strategy_miner.dtypes import MinerConfig

    cfg = MinerConfig.from_dict(
        {
            "budget": {
                "provider": "opencode",
                "max_iterations": 3,
                "max_turns": 9,
                "max_retries": 4,
                "repair_attempts": 2,
            },
            "tools": {
                "tool_allowlist": ["file"],
                "bash_allow": False,
                "bash_timeout": 12,
                "bash_allowlist": ["echo ", "python3 "],
            },
            "evaluation": {
                "min_trades": 25,
                "max_abs_drawdown": 12.5,
                "min_winrate": 0.55,
                "benchmark_suite": "benchmark_pack/default",
            },
            "portfolio": {
                "portfolio_enabled": True,
                "portfolio_top_k": 4,
                "portfolio_min_candidates": 2,
                "portfolio_correlation_threshold": 0.8,
                "portfolio_max_weight": 0.5,
            },
        }
    )

    assert cfg.provider == "opencode"
    assert cfg.max_iterations == 3
    assert cfg.max_turns == 9
    assert cfg.max_retries == 4
    assert cfg.repair_attempts == 2

    assert cfg.tool_allowlist == ["file"]
    assert cfg.bash_allow is False
    assert cfg.bash_timeout == 12
    assert cfg.bash_allowlist == ["echo ", "python3 "]

    assert cfg.min_trades == 25
    assert cfg.max_abs_drawdown == 12.5
    assert cfg.min_winrate == 0.55
    assert cfg.benchmark_suite == "benchmark_pack/default"
    assert cfg.portfolio_enabled is True
    assert cfg.portfolio_top_k == 4
    assert cfg.portfolio_correlation_threshold == 0.8
    assert cfg.portfolio_max_weight == 0.5



def test_miner_config_defaults():
    from agent_market.strategy_miner.dtypes import MinerConfig

    cfg = MinerConfig()
    assert cfg.max_retries == 2
    assert cfg.max_parallel_roles == 1
    assert cfg.max_drawdown_pct == 0.0


def test_miner_config_max_retries_override():
    from agent_market.strategy_miner.dtypes import MinerConfig

    cfg = MinerConfig.from_dict({"max_retries": 5})
    assert cfg.max_retries == 5


def test_miner_state_roundtrip():
    from agent_market.strategy_miner.dtypes import MinerState, Phase, StrategyCandidate

    state = MinerState()
    state.phase = Phase.BACKTEST
    state.iteration = 2
    candidate = StrategyCandidate(
        name="TestStrat", code="class X: pass", strategy_path=Path("/tmp/t.py")
    )
    state.candidates.append(candidate)
    state.best_candidate = candidate
    state.best_score = 0.42

    d = state.to_dict()
    j = json.dumps(d)
    state2 = MinerState.from_dict(json.loads(j))
    assert state2.run_id == state.run_id
    assert state2.phase == Phase.BACKTEST
    assert state2.iteration == 2
    assert state2.best_score == 0.42
    assert state2.best_candidate.name == "TestStrat"
    assert len(state2.candidates) == 1


def test_miner_state_backward_compat_best_reward():
    """Old checkpoints with 'best_reward' should load into best_score."""
    from agent_market.strategy_miner.dtypes import MinerState, Phase

    old_data = {
        "run_id": "abc123",
        "phase": "strategy_gen",
        "iteration": 1,
        "best_reward": 0.75,
        "candidates": [],
        "history": [],
    }
    state = MinerState.from_dict(old_data)
    assert state.best_score == 0.75


# ---------------------------------------------------------------------------
# grading
# ---------------------------------------------------------------------------


def test_compute_factor_score_none_inputs():
    from agent_market.strategy_miner.grading import compute_factor_score

    assert compute_factor_score() is None
    assert compute_factor_score(features_parquet=None, expression="x") is None


# ---------------------------------------------------------------------------
# sandbox validation
# ---------------------------------------------------------------------------


def test_validate_strategy_code_pass():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy
import pandas_ta as ta

class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    timeframe = "5m"
    def populate_indicators(self, dataframe, metadata):
        return dataframe
    def populate_entry_trend(self, dataframe, metadata):
        return dataframe
    def populate_exit_trend(self, dataframe, metadata):
        return dataframe
"""
    ok, msg = validate_strategy_code(code)
    assert ok, f"Expected pass: {msg}"


def test_validate_strategy_code_forbidden_import():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
import os
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, df, m): return df
    def populate_entry_trend(self, df, m): return df
    def populate_exit_trend(self, df, m): return df
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "os" in msg


def test_validate_strategy_code_missing_method():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    pass
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "Missing" in msg


def test_validate_strategy_code_no_istrategy():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
class NotAStrategy:
    def populate_indicators(self, df, m): return df
    def populate_entry_trend(self, df, m): return df
    def populate_exit_trend(self, df, m): return df
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "IStrategy" in msg


def test_validate_strategy_code_forbidden_call():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, df, m):
        eval("1+1")
        return df
    def populate_entry_trend(self, df, m): return df
    def populate_exit_trend(self, df, m): return df
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "eval" in msg


def test_validate_strategy_code_blocks_negative_shift():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, df, m):
        df['future'] = df['close'].shift(-1)
        return df
    def populate_entry_trend(self, df, m): return df
    def populate_exit_trend(self, df, m): return df
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "look-ahead" in msg


def test_validate_strategy_code_blocks_centered_rolling():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645Rolling(IStrategy):
    def populate_indicators(self, df, m):
        df['ma'] = df['close'].rolling(10, center=True).mean()
        return df
    def populate_entry_trend(self, df, m): return df
    def populate_exit_trend(self, df, m): return df
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "look-ahead" in msg


def test_validate_strategy_code_rejects_date_set_index_before_informative_merge():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
import pandas as pd
from freqtrade.strategy import IStrategy, merge_informative_pair
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645Merge(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        if not isinstance(dataframe.index, pd.DatetimeIndex):
            dataframe.set_index("date", inplace=True)
        inf = dataframe.copy()
        dataframe = merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True)
        return dataframe
    def populate_entry_trend(self, dataframe, metadata): return dataframe
    def populate_exit_trend(self, dataframe, metadata): return dataframe
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "merge_informative_pair requires a preserved 'date' column" in msg


def test_auto_fix_strategy_code_preserves_date_column_for_informative_merge():
    from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code

    code = """
import pandas as pd
from freqtrade.strategy import IStrategy, merge_informative_pair
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        if not isinstance(dataframe.index, pd.DatetimeIndex):
            dataframe.set_index("date", inplace=True)
        inf = dataframe.copy()
        if not isinstance(inf.index, pd.DatetimeIndex):
            inf.set_index("date", inplace=True)
        dataframe = merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True)
        return dataframe
    def populate_entry_trend(self, dataframe, metadata): return dataframe
    def populate_exit_trend(self, dataframe, metadata): return dataframe
"""
    fixed, fixes = auto_fix_strategy_code(code)
    assert "preserve_date_column_for_informative_merge" in fixes
    assert '.set_index("date", inplace=True)' not in fixed
    assert 'dataframe.index = pd.DatetimeIndex(dataframe["date"])' in fixed
    assert 'inf.index = pd.DatetimeIndex(inf["date"])' in fixed
    ok, msg = validate_strategy_code(fixed)
    assert ok, msg


def test_auto_fix_strategy_code_rewrites_self_merge_informative_pair_usage():
    from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code

    code = """
import pandas as pd
from freqtrade.strategy import IStrategy
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        inf = dataframe.copy()
        dataframe = self.merge_informative_pair(dataframe, inf, "5m", "15m", ffill=True)
        return dataframe
    def populate_entry_trend(self, dataframe, metadata): return dataframe
    def populate_exit_trend(self, dataframe, metadata): return dataframe
"""
    fixed, fixes = auto_fix_strategy_code(code)
    assert "rewrite_self_merge_informative_pair" in fixes
    assert "ensure_merge_informative_pair_import" in fixes
    assert "self.merge_informative_pair" not in fixed
    assert "merge_informative_pair(dataframe, inf, '5m', '15m', ffill=True)" in fixed
    ok, msg = validate_strategy_code(fixed)
    assert ok, msg


def test_auto_fix_strategy_code_forces_append_timeframe_false_when_suffix_is_used():
    from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy, merge_informative_pair
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        inf = dataframe.copy()
        dataframe = merge_informative_pair(
            dataframe,
            inf,
            "5m",
            "1h",
            suffix="1h",
            ffill=True,
        )
        return dataframe
    def populate_entry_trend(self, dataframe, metadata): return dataframe
    def populate_exit_trend(self, dataframe, metadata): return dataframe
"""
    fixed, fixes = auto_fix_strategy_code(code)
    assert "force_append_timeframe_false_for_suffix" in fixes
    assert "append_timeframe=False" in fixed
    ok, msg = validate_strategy_code(fixed)
    assert ok, msg


def test_auto_fix_strategy_code_rewrites_parameter_default_to_value():
    from agent_market.strategy_miner.sandbox import auto_fix_strategy_code, validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy, DecimalParameter

class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645ParamDefault(IStrategy):
    p = DecimalParameter(0.5, 3.0, default=1.0, space="buy")

    def populate_indicators(self, dataframe, metadata):
        x = float(self.p.default)
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        return dataframe
"""
    fixed, fixes = auto_fix_strategy_code(code)
    assert "rewrite_parameter_default_to_value" in fixes
    assert "self.p.default" not in fixed
    assert "self.p.value" in fixed
    ok, msg = validate_strategy_code(fixed)
    assert ok, msg


def test_validate_strategy_code_rejects_suffix_without_append_timeframe_false():
    from agent_market.strategy_miner.sandbox import validate_strategy_code

    code = """
from freqtrade.strategy import IStrategy, merge_informative_pair
class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645SuffixMerge(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        inf = dataframe.copy()
        dataframe = merge_informative_pair(dataframe, inf, "5m", "1h", suffix="1h", ffill=True)
        return dataframe
    def populate_entry_trend(self, dataframe, metadata): return dataframe
    def populate_exit_trend(self, dataframe, metadata): return dataframe
"""
    ok, msg = validate_strategy_code(code)
    assert not ok
    assert "append_timeframe=False" in msg


def test_ensure_freqtrade_strategy_compliance_adds_ohlcv_suffix_guard_for_merge_asof():
    from agent_market.strategy_miner.sandbox import ensure_freqtrade_strategy_compliance_code, validate_strategy_code

    code = """
import pandas as pd
from freqtrade.strategy import IStrategy

class Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645(IStrategy):
    timeframe = "5m"
    can_short = False
    order_types = {"entry": "market", "exit": "market", "stoploss": "market", "stoploss_on_exchange": False}
    order_time_in_force = {"entry": "GTC", "exit": "GTC"}

    def populate_indicators(self, df, metadata):
        informative = df[["date", "close"]].copy()
        df = pd.merge_asof(
            df.sort_values("date"),
            informative.sort_values("date"),
            on="date",
            direction="backward",
        )
        return df

    def populate_entry_trend(self, df, metadata):
        return df

    def populate_exit_trend(self, df, metadata):
        return df
"""
    fixed, fixes = ensure_freqtrade_strategy_compliance_code(code, timeframe="5m", enforce_can_short_false=True)
    assert "add_ohlcv_suffix_guard" in fixes
    assert "close_x" in fixed
    ok, msg = validate_strategy_code(fixed)
    assert ok, msg


# ---------------------------------------------------------------------------
# knowledge base
# ---------------------------------------------------------------------------


def test_knowledge_base_roundtrip():
    from agent_market.strategy_miner.knowledge_base import KnowledgeBase

    with tempfile.TemporaryDirectory() as td:
        kb = KnowledgeBase(Path(td) / "kb.json")
        kb.add_elite("S1", "code1", 0.8, {"profit_total_pct": 10, "trades": 50}, 0)
        kb.add_elite("S2", "code2", 0.5, {"profit_total_pct": 5, "trades": 30}, 1)
        kb.add_failure("F1", 0, "validation", "Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645 import")

        assert len(kb.elites) == 2
        assert kb.elites[0]["reward"] == 0.8  # sorted desc
        assert len(kb.failures) == 1

        # Reload
        kb2 = KnowledgeBase(Path(td) / "kb.json")
        assert len(kb2.elites) == 2
        assert kb2.to_dict()["top_reward"] == 0.8


def test_knowledge_base_prefers_family_match_in_generation_query():
    from agent_market.strategy_miner.knowledge_base import KnowledgeBase

    with tempfile.TemporaryDirectory() as td:
        kb = KnowledgeBase(Path(td) / "kb.json")
        kb.merge_payload(
            {
                "strategy_cards": [
                    {
                        "run_id": "run_ml",
                        "name": "HugeMlWinner",
                        "iteration": 0,
                        "candidate_type": "ml",
                        "candidate_family": "ml/lightgbm",
                        "timeframe": "5m",
                        "universe": ["BTC/USDT", "ETH/USDT"],
                        "metrics": {"sharpe": 999.0, "profit_pct": 20.0, "trades": 1000},
                    },
                    {
                        "run_id": "run_rule",
                        "name": "BreakoutRuleWinner",
                        "iteration": 0,
                        "candidate_type": "rule",
                        "candidate_family": "rule/breakout",
                        "timeframe": "5m",
                        "universe": ["BTC/USDT", "ETH/USDT"],
                        "metrics": {"sharpe": 1.2, "profit_pct": 4.0, "trades": 80},
                    },
                ]
            }
        )

        results = kb.query_strategy_cards_for_generation(
            top_n=2,
            family="rule/breakout",
            timeframe="5m",
            universe=["BTC/USDT", "ETH/USDT"],
        )

        assert len(results) == 2
        assert results[0]["name"] == "BreakoutRuleWinner"


def test_knowledge_base_retrieve_for_generation_can_include_recent_cards():
    from agent_market.strategy_miner.knowledge_base import KnowledgeBase

    with tempfile.TemporaryDirectory() as td:
        kb = KnowledgeBase(Path(td) / "kb.json")
        kb.merge_payload(
            {
                "strategy_cards": [
                    {
                        "card_id": "run_old:OldBest:0",
                        "created_at": "2026-01-01T00:00:00+00:00",
                        "run_id": "run_old",
                        "name": "OldBest",
                        "iteration": 0,
                        "candidate_type": "ml",
                        "candidate_family": "ml/lightgbm",
                        "timeframe": "5m",
                        "universe": ["BTC/USDT"],
                        "metrics": {"sharpe": 10.0, "profit_pct": 100.0, "trades": 1000},
                    },
                    {
                        "card_id": "run_new:RecentLow:0",
                        "created_at": "2026-01-02T00:00:00+00:00",
                        "run_id": "run_new",
                        "name": "RecentLow",
                        "iteration": 0,
                        "candidate_type": "ml",
                        "candidate_family": "ml/lightgbm",
                        "timeframe": "5m",
                        "universe": ["BTC/USDT"],
                        "metrics": {"sharpe": 0.0, "profit_pct": 0.0, "trades": 0},
                    },
                ]
            }
        )

        res = kb.retrieve_for_generation(
            family="ml/lightgbm",
            timeframe="5m",
            universe=["BTC/USDT"],
            top_n=1,
            recent_n=1,
        )
        names = [c.get("name") for c in res.strategy_cards]
        assert "OldBest" in names
        assert "RecentLow" in names
        assert int((res.query or {}).get("recent_n") or 0) == 1



# ---------------------------------------------------------------------------
# prompts
# ---------------------------------------------------------------------------


def test_build_strategy_gen_prompt():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
    )
    assert "IStrategy" in p
    assert "Iteration 0" in p


def test_build_analysis_prompt():
    from agent_market.strategy_miner.prompts import build_analysis_prompt

    p = build_analysis_prompt(
        strategy_code="class X: pass",
        backtest_summary={"profit_total_pct": 5.0, "trades": 30},
        metrics={"sharpe": 1.2, "sortino": 1.5},
    )
    assert "JSON" in p
    assert "strengths" in p
    assert "verdict" in p


def test_strategy_gen_prompt_openai_compatible_no_tool_tags():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        provider="openai_compatible",
    )
    assert "You MAY use tool-call tags" not in p
    assert "single Python code block" in p


def test_strategy_gen_prompt_opencode_has_tool_tags():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        provider="opencode",
    )
    assert "<write" in p or "tool-call tags" in p


def test_strategy_gen_prompt_market_orders_advice():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
    )
    assert "market" in p.lower()


def test_strategy_gen_prompt_market_profile():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        market_profile="- Trading pairs: BTC/USDT\n- Stake currency: USDT",
    )
    assert "Market Profile" in p
    assert "BTC/USDT" in p


def test_repair_prompt_openai_compatible_no_tool_tags():
    from agent_market.strategy_miner.prompts import build_repair_prompt

    p = build_repair_prompt(
        sandbox_path="/tmp/sandbox",
        strategy_rel_path="user_data/strategies/Foo.py",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        failure="Syntax error",
        attempt=1,
        max_attempts=3,
        provider="openai_compatible",
    )
    assert "Start by reading" not in p
    assert "single Python code block" in p


# ---------------------------------------------------------------------------
# prompts with KB context
# ---------------------------------------------------------------------------


def test_prompt_with_kb_context():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    elites = [
        {"name": "EliteA", "reward": 0.8, "profit_pct": 15, "trades": 80, "winrate": 0.65},
    ]
    failure_summary = "  - iter0 [validation]: Forbidden import: os"
    p = build_strategy_gen_prompt(
        iteration=2,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=0.8,
        elite_summaries=elites,
        failure_summary=failure_summary,
    )
    assert "Elite Strategy Archive" in p
    assert "EliteA" in p
    assert "Failure Patterns" in p
    assert "Forbidden import" in p


def test_prompt_without_kb_context():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        elite_summaries=None,
        failure_summary=None,
    )
    assert "Elite Strategy Archive" not in p
    assert "Failure Patterns" not in p


def test_strategy_prompt_includes_factor_memory_context():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        factor_context="## Factor Memory Retrieval\n- breakout_card [id] tf=5m gate_pass=True",
    )
    assert "Factor Memory Retrieval" in p
    assert "breakout_card" in p


def test_strategy_prompt_includes_strategy_memory_context():
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    p = build_strategy_gen_prompt(
        iteration=0,
        sandbox_path="/tmp/sandbox",
        freqtrade_config="config.json",
        timerange="20250101-20260101",
        history=[],
        best_score=float("-inf"),
        strategy_memory_context="## Strategy Memory Retrieval\n- PriorBreakoutWinner [run:card] family=rule/breakout type=rule sharpe=1.2",
    )
    assert "Strategy Memory Retrieval" in p
    assert "PriorBreakoutWinner" in p


# ---------------------------------------------------------------------------
# KB integration with prompts end-to-end
# ---------------------------------------------------------------------------


def test_kb_feeds_into_prompt():
    """Knowledge base data flows correctly into prompt generation."""
    from agent_market.strategy_miner.knowledge_base import KnowledgeBase
    from agent_market.strategy_miner.prompts import build_strategy_gen_prompt

    with tempfile.TemporaryDirectory() as td:
        kb = KnowledgeBase(Path(td) / "kb.json")
        kb.add_elite("TopStrat", "code...", 0.9, {"profit_total_pct": 20, "trades": 100, "winrate": 0.7, "max_drawdown_abs": -3}, 0)
        kb.add_failure("Github_shatianming5_Agent_market__test_strategy_miner__20260608_143645Strat", 1, "backtest", "Timeout after 300s")

        p = build_strategy_gen_prompt(
            iteration=2,
            sandbox_path="/tmp/sandbox",
            freqtrade_config="cfg.json",
            timerange="20250101-20260101",
            history=[],
            best_score=0.9,
            elite_summaries=kb.elites[:3],
            failure_summary=kb.failure_summary(5),
        )
        assert "TopStrat" in p
        assert "Timeout" in p


# ---------------------------------------------------------------------------
# imports
# ---------------------------------------------------------------------------


def test_all_imports():
    """Ensure the full module tree is importable."""
    from agent_market.strategy_miner import KnowledgeBase, MinerConfig, MinerState, Phase, StrategyCandidate, run_strategy_miner
    from agent_market.strategy_miner.agent_adapter import StrategyAgent
    from agent_market.strategy_miner.grading import compute_factor_score
    from agent_market.strategy_miner.sandbox import validate_strategy_code


# ---------------------------------------------------------------------------
# agent_adapter: max_retries & error handling
# ---------------------------------------------------------------------------


def test_agent_adapter_passes_max_retries():
    """StrategyAgent should forward max_retries to OpenCodeExecutor."""
    from unittest.mock import MagicMock, patch

    with patch("agent_market.strategy_miner.agent_adapter.OpenCodeExecutor") as MockExec:
        mock_instance = MagicMock()
        mock_instance.close.return_value = None
        MockExec.return_value = mock_instance

        from agent_market.strategy_miner.agent_adapter import StrategyAgent

        agent = StrategyAgent(
            workspace=Path("/tmp/test_ws"),
            model="test-model",
            max_retries=5,
        )
        _, kwargs = MockExec.call_args
        assert kwargs["max_retries"] == 5
        agent.close()


def test_agent_adapter_default_max_retries():
    """Default max_retries should be 2."""
    from unittest.mock import MagicMock, patch

    with patch("agent_market.strategy_miner.agent_adapter.OpenCodeExecutor") as MockExec:
        mock_instance = MagicMock()
        mock_instance.close.return_value = None
        MockExec.return_value = mock_instance

        from agent_market.strategy_miner.agent_adapter import StrategyAgent

        agent = StrategyAgent(
            workspace=Path("/tmp/test_ws"),
            model="test-model",
        )
        _, kwargs = MockExec.call_args
        assert kwargs["max_retries"] == 2
        agent.close()


def test_agent_adapter_close_is_idempotent():
    """Calling close() multiple times should not raise."""
    from unittest.mock import MagicMock, patch

    with patch("agent_market.strategy_miner.agent_adapter.OpenCodeExecutor") as MockExec:
        mock_instance = MagicMock()
        mock_instance.close.return_value = None
        MockExec.return_value = mock_instance

        from agent_market.strategy_miner.agent_adapter import StrategyAgent

        agent = StrategyAgent(workspace=Path("/tmp/ws"), provider="opencode", model="test-model")
        agent.close()
        agent.close()  # should not raise


def test_agent_adapter_run_after_close_raises():
    """Calling run() after close() should raise RuntimeError."""
    from unittest.mock import MagicMock, patch

    with patch("agent_market.strategy_miner.agent_adapter.OpenCodeExecutor") as MockExec:
        mock_instance = MagicMock()
        mock_instance.close.return_value = None
        MockExec.return_value = mock_instance

        from agent_market.strategy_miner.agent_adapter import StrategyAgent

        agent = StrategyAgent(workspace=Path("/tmp/ws"), provider="opencode", model="test-model")
        agent.close()
        with pytest.raises(RuntimeError, match="already closed"):
            agent.run("test prompt")


def test_agent_adapter_no_model_errors_in_no_template_mode():
    """Without a configured model/credentials, StrategyAgent should error (no-template enforced)."""
    import os
    from unittest.mock import patch

    from agent_market.strategy_miner.agent_adapter import StrategyAgent

    with patch.dict(os.environ, {}, clear=True):
        with tempfile.TemporaryDirectory() as td:
            with pytest.raises(ValueError, match="model"):
                StrategyAgent(workspace=Path(td))


def test_agent_adapter_opencode_without_model_raises():
    """If provider=opencode is forced, missing model should raise."""
    import os
    from unittest.mock import patch

    from agent_market.strategy_miner.agent_adapter import StrategyAgent

    with patch.dict(os.environ, {}, clear=True):
        with pytest.raises(ValueError, match="model"):
            StrategyAgent(workspace=Path("/tmp/ws"), provider="opencode")
