import sqlite3
import tempfile
import unittest
from datetime import datetime, timedelta
from pathlib import Path

from database import ReviewDatabase
from backend.features.screener.engine import (
    ADVANCED_CURATED_STRATEGIES,
    CURATED_STRATEGIES,
    FACTOR_FIELDS,
    FACTOR_GROUPS,
    ScreenerEngine,
    _broken_reversal_metrics,
    _earnings_event_rows,
    _popularity_factor_rows,
    _risk_flags,
    _rsi,
    _quarter_periods,
)
from server import DashboardService, automatic_screener_jobs


class CuratedScreenerTests(unittest.TestCase):
    def test_curated_library_contains_original_and_advanced_strategies(self):
        self.assertEqual(19, len(ADVANCED_CURATED_STRATEGIES))
        self.assertEqual(29, len(CURATED_STRATEGIES))
        self.assertEqual(29, len({item["name"] for item in CURATED_STRATEGIES}))
        self.assertTrue(
            {"行业动量轮动", "主力资金行业流入"}.issubset(
                {item["name"] for item in CURATED_STRATEGIES}
            )
        )
        self.assertTrue(
            all(item["formula"]["meta"]["library"] == "curated" for item in CURATED_STRATEGIES)
        )
        self.assertTrue(
            {
                "景气-趋势-拥挤三维行业打分",
                "大小盘/成长价值风格切换(元策略)",
                "业绩超预期漂移(SUE/PEAD)",
                "多因子综合打分(IC动态加权)",
                "热度突增潜伏(另类数据)",
                "机构榜溢价",
            }.issubset({item["name"] for item in CURATED_STRATEGIES})
        )

    def test_every_curated_strategy_explains_environment_and_failure_risk(self):
        for strategy in CURATED_STRATEGIES:
            meta = strategy["formula"]["meta"]
            self.assertTrue(meta.get("suitable_environment"), strategy["name"])
            self.assertTrue(meta.get("failure_risk"), strategy["name"])
            self.assertNotIn("emotion_gate", meta, strategy["name"])

    def test_automatic_curated_jobs_are_not_filtered_by_market_regime(self):
        strategies = [
            {
                "name": "阶段策略",
                "regimes": ["retreat"],
                "formula": {"meta": {"library": "stage"}},
            },
            *CURATED_STRATEGIES,
        ]
        for regime in ("ice", "repair", "fermentation", "climax", "divergence", "retreat"):
            jobs = automatic_screener_jobs(strategies, regime)
            curated_names = {
                job["strategy"]["name"] for job in jobs if job["mode"] == "curated"
            }
            self.assertEqual(
                {strategy["name"] for strategy in CURATED_STRATEGIES},
                curated_names,
                regime,
            )

    def test_curated_risk_flags_do_not_reintroduce_regime_gating(self):
        row = {
            "pct_chg": 0,
            "return_10d": 0,
            "volatility_10d": 0,
            "amount_billion": 5,
        }
        self.assertIn("市场处于退潮阶段，策略可能选择空仓", _risk_flags(row, "retreat"))
        self.assertNotIn(
            "市场处于退潮阶段，策略可能选择空仓",
            _risk_flags(row, "retreat", include_regime_risk=False),
        )

    def test_every_curated_formula_uses_supported_factors(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            engine = ScreenerEngine(database)
            for strategy in CURATED_STRATEGIES:
                formula = engine.validate_formula(strategy["formula"])
                fields = {
                    item["field"]
                    for item in formula["filters"] + formula["score"]
                }
                self.assertTrue(fields.issubset(FACTOR_FIELDS), strategy["name"])

    def test_server_gate_blocks_specialized_strategies_until_sources_are_ready(self):
        factor_dates = [f"2026{index + 1:04d}" for index in range(260)]
        health = {
            "market": True,
            "auction": True,
            "benchmark": True,
            "valuation": True,
            "fundamental": True,
            "dividend_history": True,
            "moneyflow_history": True,
            "earnings_events": False,
            "popularity": False,
            "institutions": False,
        }
        expected = {
            "业绩超预期漂移(SUE/PEAD)": "业绩预告与快报",
            "热度突增潜伏(另类数据)": "当日人气榜",
            "机构榜溢价": "龙虎榜机构席位",
        }
        by_name = {strategy["name"]: strategy for strategy in CURATED_STRATEGIES}

        for name, missing_label in expected.items():
            self.assertEqual(
                [missing_label],
                DashboardService._strategy_missing_data(
                    by_name[name], factor_dates, health
                ),
                name,
            )

        ready_health = {
            **health,
            "earnings_events": True,
            "popularity": True,
            "institutions": True,
        }
        for name in expected:
            self.assertEqual(
                [],
                DashboardService._strategy_missing_data(
                    by_name[name], factor_dates, ready_health
                ),
                name,
            )

    def test_factor_groups_cover_every_quant_factor(self):
        grouped = [field for fields in FACTOR_GROUPS.values() for field in fields]
        self.assertEqual(set(FACTOR_FIELDS), set(grouped))
        self.assertEqual(len(grouped), len(set(grouped)))

    def test_database_migrates_valuation_and_fundamental_columns(self):
        with tempfile.TemporaryDirectory() as root:
            path = Path(root) / "review.db"
            ReviewDatabase(path)
            connection = sqlite3.connect(path)
            try:
                indicator_columns = {
                    row[1] for row in connection.execute("PRAGMA table_info(daily_indicators)")
                }
                tables = {
                    row[0] for row in connection.execute(
                        "SELECT name FROM sqlite_master WHERE type='table'"
                    )
                }
            finally:
                connection.close()
            self.assertTrue({"pe_ttm", "pb", "ps_ttm", "dv_ttm"}.issubset(indicator_columns))
            self.assertIn("fundamental_indicators", tables)
            self.assertIn("benchmark_bars", tables)
            self.assertIn("earnings_events", tables)
            self.assertIn("popularity_factors", tables)
            self.assertIn("lhb_institution_daily", tables)

    def test_advanced_strategies_declare_history_and_backtest_contracts(self):
        for strategy in ADVANCED_CURATED_STRATEGIES:
            meta = strategy["formula"]["meta"]
            self.assertGreaterEqual(meta["history_days"], 80, strategy["name"])
            self.assertGreaterEqual(meta["backtest_days"], 1, strategy["name"])
            self.assertGreater(meta["take_profit"], 0, strategy["name"])
            self.assertLess(meta["stop_loss"], 0, strategy["name"])

    def test_quarter_periods_stop_at_selected_date(self):
        periods = _quarter_periods("20260722", 5)
        self.assertEqual(
            ["20250630", "20250930", "20251231", "20260331", "20260630"],
            periods,
        )

    def test_factor_health_summary_uses_availability_counts(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            with database.connect() as connection:
                connection.execute(
                    "INSERT INTO daily_bars (trade_date, ts_code) VALUES (?, ?)",
                    ("20260722", "600000.SH"),
                )
                connection.execute(
                    """
                    INSERT INTO daily_indicators
                        (trade_date, ts_code, pe_ttm)
                    VALUES (?, ?, ?)
                    """,
                    ("20260722", "600000.SH", 8.5),
                )
                connection.executemany(
                    "INSERT INTO daily_indicators (trade_date, ts_code) VALUES (?, ?)",
                    [(f"{year}1231", f"{year % 100:02d}0000.SZ") for year in range(2022, 2026)],
                )
                connection.execute(
                    "INSERT INTO auction_factors (trade_date, ts_code) VALUES (?, ?)",
                    ("20260722", "600000.SH"),
                )
                connection.executemany(
                    """
                    INSERT INTO fundamental_indicators (end_date, ann_date, ts_code, roe)
                    VALUES (?, ?, ?, ?)
                    """,
                    [
                        ("20251231", "20260430", f"{index:06d}.SZ", 10.0)
                        for index in range(100)
                    ],
                )
                connection.executemany(
                    "INSERT INTO benchmark_bars (trade_date, ts_code, close) VALUES (?, ?, ?)",
                    [(f"2026{index + 1:04d}", "000300.SH", 4000 + index) for index in range(60)],
                )

            health = database.factor_health_summary("20260722")
            self.assertTrue(health["market"])
            self.assertTrue(health["auction"])
            self.assertTrue(health["valuation"])
            self.assertTrue(health["fundamental"])
            self.assertTrue(health["dividend_history"])
            self.assertTrue(health["benchmark"])
            self.assertEqual(health["valuation_rows"], 1)
            self.assertEqual(health["fundamental_rows"], 100)
            self.assertEqual(health["dividend_years"], 5)

    def test_moneyflow_health_requires_the_latest_five_market_dates(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            dates = [f"202607{day:02d}" for day in range(20, 25)]
            database.upsert_daily_bars([
                {
                    "trade_date": trade_date, "ts_code": "600000.SH",
                    "open": 10, "high": 10.2, "low": 9.8, "close": 10,
                    "pct_chg": 0, "vol": 1000, "amount": 100000,
                }
                for trade_date in dates
            ])
            database.upsert_moneyflow([
                {"trade_date": "20260105", "ts_code": "600000.SH", "net_mf_amount": 10}
            ] * 5)
            self.assertFalse(database.factor_health_summary(dates[-1])["moneyflow_history"])
            database.upsert_moneyflow([
                {"trade_date": trade_date, "ts_code": "600000.SH", "net_mf_amount": 10}
                for trade_date in dates
            ])
            health = database.factor_health_summary(dates[-1])
            self.assertTrue(health["moneyflow_history"])
            self.assertEqual(health["moneyflow_dates"], 5)

    def test_technical_helpers_detect_rsi_and_daily_reversal_path(self):
        self.assertLess(_rsi([10, 9, 8, 7, 6, 5, 4], 6), 1)
        rows = [
            {"close": 10, "high": 10, "vol": 100},
            {"close": 11, "high": 11, "vol": 120},
            {"close": 12, "high": 12, "vol": 130},
            {"close": 11.2, "high": 11.8, "vol": 100},
            {"close": 12.5, "high": 12.5, "vol": 140},
        ]
        metrics = _broken_reversal_metrics(
            rows, [False, True, True, False, True], "600000", "示例"
        )
        self.assertEqual(metrics["signal"], 1)
        self.assertEqual(metrics["days"], 1)

    def test_factor_builder_generates_long_window_and_benchmark_factors(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            database.upsert_stock_master([
                {
                    "ts_code": "600000.SH", "name": "趋势样本", "industry": "银行",
                    "market": "主板", "list_date": "20000101",
                }
            ])
            dates = []
            cursor = datetime(2025, 6, 1)
            while len(dates) < 260:
                if cursor.weekday() < 5:
                    dates.append(cursor.strftime("%Y%m%d"))
                cursor += timedelta(days=1)
            bars = []
            benchmarks = []
            indicators = []
            for index, trade_date in enumerate(dates):
                close = 10 + index * 0.05
                bars.append({
                    "trade_date": trade_date, "ts_code": "600000.SH",
                    "open": close - 0.02, "high": close + 0.08, "low": close - 0.08,
                    "close": close, "pct_chg": 0.25, "vol": 1000 + index,
                    "amount": 200000,
                })
                benchmarks.append({
                    "trade_date": trade_date, "ts_code": "000300.SH",
                    "close": 4000 + index, "pct_chg": 0.02,
                })
                if index >= 250:
                    indicators.append({
                        "trade_date": trade_date, "ts_code": "600000.SH",
                        "turnover_rate": 2, "volume_ratio": 1,
                    })
            database.upsert_daily_bars(bars)
            database.upsert_benchmark_bars(benchmarks)
            database.upsert_daily_indicators(indicators)

            factors, actual_date = ScreenerEngine(database).build_factors(
                dates[-1], history_days=260
            )

            self.assertEqual(actual_date, dates[-1])
            self.assertEqual(len(factors), 1)
            factor = factors[0]
            self.assertEqual(factor["ma_bull_alignment"], 1)
            self.assertEqual(factor["rs_high_120"], 1)
            self.assertGreater(factor["momentum_60_5"], 0)
            self.assertEqual(factor["momentum_60_5_rank"], 0)

    def test_factor_builder_generates_sector_momentum_and_five_day_flow(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            stocks = [
                ("600001.SH", "动量样本", "电子", 0.16, 180),
                ("600002.SH", "对照样本", "银行", 0.02, -40),
            ]
            database.upsert_stock_master([
                {
                    "ts_code": code, "name": name, "industry": industry,
                    "market": "主板", "list_date": "20000101",
                }
                for code, name, industry, _, _ in stocks
            ])
            dates = []
            cursor = datetime(2026, 4, 1)
            while len(dates) < 80:
                if cursor.weekday() < 5:
                    dates.append(cursor.strftime("%Y%m%d"))
                cursor += timedelta(days=1)
            bars = []
            for index, trade_date in enumerate(dates):
                for code, _, _, slope, _ in stocks:
                    close = 10 + index * slope
                    bars.append({
                        "trade_date": trade_date, "ts_code": code,
                        "open": close - 0.03, "high": close + 0.08,
                        "low": close - 0.08, "close": close,
                        "pct_chg": slope, "vol": 1000 + index,
                        "amount": 300000,
                    })
            database.upsert_daily_bars(bars)
            database.upsert_daily_indicators([
                {
                    "trade_date": dates[-1], "ts_code": code,
                    "turnover_rate": 2, "volume_ratio": 1,
                    "circ_mv": 1000000, "total_mv": 1500000,
                }
                for code, *_ in stocks
            ])
            database.upsert_moneyflow([
                {
                    "trade_date": trade_date, "ts_code": code,
                    "net_mf_amount": daily_flow,
                }
                for trade_date in dates[-5:]
                for code, _, _, _, daily_flow in stocks
            ])

            factors, _ = ScreenerEngine(database).build_factors(
                dates[-1], history_days=80
            )
            by_code = {item["ts_code"]: item for item in factors}
            leader = by_code["600001.SH"]
            laggard = by_code["600002.SH"]
            self.assertGreater(leader["return_20d"], laggard["return_20d"])
            self.assertEqual(leader["sector_momentum_rank"], 1)
            self.assertEqual(laggard["sector_momentum_rank"], 0)
            self.assertGreater(leader["net_flow_5d_million"], 0)
            self.assertLess(laggard["net_flow_5d_million"], 0)
            self.assertEqual(leader["sector_flow_rank"], 1)

    def test_stage_three_event_and_composite_factors_are_date_scoped(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            stocks = [
                ("600001.SH", "成长样本", "电子", 0.08),
                ("600002.SH", "价值样本", "银行", 0.02),
            ]
            database.upsert_stock_master([
                {
                    "ts_code": code, "name": name, "industry": industry,
                    "market": "主板", "list_date": "20000101",
                }
                for code, name, industry, _ in stocks
            ])
            dates = []
            cursor = datetime(2026, 3, 1)
            while len(dates) < 80:
                if cursor.weekday() < 5:
                    dates.append(cursor.strftime("%Y%m%d"))
                cursor += timedelta(days=1)
            database.upsert_daily_bars([
                {
                    "trade_date": trade_date, "ts_code": code,
                    "open": 10 + index * slope - 0.02,
                    "high": 10 + index * slope + 0.08,
                    "low": 10 + index * slope - 0.08,
                    "close": 10 + index * slope,
                    "pct_chg": slope, "vol": 1000 + index, "amount": 300000,
                }
                for index, trade_date in enumerate(dates)
                for code, _, _, slope in stocks
            ])
            database.upsert_daily_indicators([
                {
                    "trade_date": dates[-1], "ts_code": "600001.SH",
                    "turnover_rate": 3, "volume_ratio": 1.4, "total_mv": 900000,
                    "circ_mv": 700000, "pe_ttm": 25, "pb": 3, "ps_ttm": 4,
                },
                {
                    "trade_date": dates[-1], "ts_code": "600002.SH",
                    "turnover_rate": 1, "volume_ratio": 0.9, "total_mv": 5000000,
                    "circ_mv": 4000000, "pe_ttm": 8, "pb": 0.8, "ps_ttm": 1,
                },
            ])
            database.upsert_fundamental_indicators([
                {
                    "end_date": "20260331", "ann_date": dates[-10],
                    "ts_code": "600001.SH", "roe": 16, "roic": 13,
                    "grossprofit_margin": 35, "netprofit_yoy": 45, "or_yoy": 30,
                },
                {
                    "end_date": "20260331", "ann_date": dates[-10],
                    "ts_code": "600002.SH", "roe": 9, "roic": 7,
                    "grossprofit_margin": 18, "netprofit_yoy": 5, "or_yoy": 3,
                },
            ])
            database.upsert_earnings_events([{
                "end_date": "20260331", "ann_date": dates[-3],
                "ts_code": "600001.SH", "forecast_profit": 100,
                "actual_profit": 125, "surprise_pct": 25,
                "revenue_yoy": 30, "netprofit_yoy": 45,
                "source": "forecast+express",
            }])
            database.upsert_popularity_factors([{
                "trade_date": dates[-1], "ts_code": "600001.SH",
                "ths_rank": 5, "dc_rank": 8, "combined_score": 75,
                "rank_change": 12, "dual_source": True,
            }])
            database.upsert_lhb_institutions([{
                "trade_date": dates[-1], "ts_code": "600001.SH",
                "exalter": "机构专用", "buy": 80_000_000,
                "sell": 20_000_000, "net_buy": 60_000_000,
            }])

            factors, actual_date = ScreenerEngine(database).build_factors(
                dates[-1], history_days=80
            )
            by_code = {item["ts_code"]: item for item in factors}
            factor = by_code["600001.SH"]
            self.assertEqual(actual_date, dates[-1])
            self.assertEqual(factor["earnings_days_since_announce"], 2)
            self.assertEqual(factor["earnings_surprise_pct"], 25)
            self.assertEqual(factor["popularity_score"], 75)
            self.assertEqual(factor["popularity_dual_source"], 1)
            self.assertEqual(factor["institution_net_buy_million"], 60)
            self.assertEqual(factor["institution_seat_count"], 1)
            self.assertIsNotNone(factor["sector_composite_score"])
            self.assertIsNotNone(factor["style_fit_score"])
            self.assertIsNotNone(factor["multi_factor_composite"])
            health = database.factor_health_summary(dates[-1])
            self.assertTrue(health["earnings_events"])
            self.assertTrue(health["popularity"])
            self.assertTrue(health["institutions"])

    def test_stage_three_sources_normalize_units_and_rank_changes(self):
        earnings = _earnings_event_rows(
            [{
                "ts_code": "600001.SH", "ann_date": "20260401",
                "end_date": "20260331", "net_profit_min": 10000,
                "net_profit_max": 12000,
            }],
            [{
                "ts_code": "600001.SH", "ann_date": "20260420",
                "end_date": "20260331", "n_income": 132_000_000,
                "yoy_net_profit": 30, "yoy_sales": 18,
            }],
            "20260420",
        )
        self.assertEqual(len(earnings), 1)
        self.assertEqual(round(earnings[0]["actual_profit"]), 13200)
        self.assertEqual(round(earnings[0]["surprise_pct"]), 20)

        popularity = _popularity_factor_rows(
            "20260420",
            [{"data_type": "热股", "ts_code": "600001.SH", "rank": 5}],
            [{"data_type": "A股市场", "ts_code": "600001.SH", "rank": 8}],
            [{"data_type": "热股", "ts_code": "600001.SH", "rank": 20}],
            [{"data_type": "A股市场", "ts_code": "600001.SH", "rank": 30}],
        )
        self.assertEqual(len(popularity), 1)
        self.assertEqual(popularity[0]["rank_change"], 15)
        self.assertTrue(popularity[0]["dual_source"])

    def test_screen_reports_signal_health(self):
        with tempfile.TemporaryDirectory() as root:
            database = ReviewDatabase(Path(root) / "review.db")
            engine = ScreenerEngine(database)
            formula = {
                "universe": {"exclude_st": True, "listed_days_min": 0},
                "filters": [{"field": "pct_chg", "op": ">", "value": 0}],
                "score": [{"field": "amount_billion", "weight": 1, "direction": "desc"}],
                "limit": 5,
                "min_score": 0,
            }
            result = engine.screen(
                0, "20260724", formula, "repair", "健康检查", False,
                mode="curated",
                prepared_factors=[{
                    "ts_code": "600000.SH", "code": "600000", "name": "浦发银行",
                    "sector": "银行", "listed_days": 1000, "pct_chg": 1,
                    "amount_billion": 5, "price": 10, "return_5d": 1,
                    "volume_ratio_5d": 1, "sector_strength": 50,
                }],
                prepared_date="20260724",
            )
            health = result["meta"]["health"]
            self.assertEqual(health["status"], "normal")
            self.assertEqual(health["signal_count"], 1)
            self.assertEqual(health["coverage"], 100)


if __name__ == "__main__":
    unittest.main()
