# ruff: noqa: E501 - compact SQL fixtures intentionally mirror the legacy schema.

from __future__ import annotations

import base64
import hashlib
import json
import sqlite3

from cryptography.fernet import Fernet

from backend.features.market.events import apply_event_revisions
from backend.features.screener.catalog import strategy_by_id
from backend.security.passwords import PasswordHasher
from tools.legacy_migration import LegacyMigrator


def _legacy_database(path, key: str) -> None:
    salt = b"0123456789abcdef"
    digest = hashlib.scrypt(
        b"Password123", salt=salt, n=2**14, r=8, p=1, dklen=32
    )
    encoded_salt = base64.urlsafe_b64encode(salt).decode()
    encoded_hash = base64.urlsafe_b64encode(digest).decode()
    fernet = Fernet(key.encode())
    credentials = {
        "tushare_token": "secret-tushare",
        "ifind_refresh_token": "secret-refresh",
        "member_daily_limit": 61,
        "llm_models": [{
            "id": "main", "name": "Primary", "base_url": "https://model.invalid/v1",
            "model": "model-a", "api_key": "secret-model",
        }],
        "primary_model_id": "main",
    }
    with sqlite3.connect(path) as connection:
        connection.executescript(
            """
            CREATE TABLE users (id INTEGER PRIMARY KEY,username TEXT,password_salt TEXT,
              password_hash TEXT,created_at TEXT,updated_at TEXT,role TEXT,llm_mode TEXT,
              membership_status TEXT,membership_plan TEXT,membership_starts_at TEXT,
              membership_expires_at TEXT);
            CREATE TABLE user_birth_profiles
              (user_id INTEGER PRIMARY KEY,encrypted_payload TEXT,updated_at TEXT);
            CREATE TABLE llm_usage (id INTEGER PRIMARY KEY,user_id INTEGER,feature TEXT,
              source TEXT,model TEXT,status TEXT,latency_ms INTEGER,created_at TEXT,
              role TEXT,prompt_version TEXT,error_code TEXT,input_tokens INTEGER,output_tokens INTEGER);
            CREATE TABLE system_settings
              (setting_key TEXT PRIMARY KEY,encrypted_payload TEXT,updated_at TEXT);
            CREATE TABLE stock_master (ts_code TEXT PRIMARY KEY,code TEXT,name TEXT,
              industry TEXT,market TEXT,list_date TEXT,updated_at TEXT);
            CREATE TABLE daily_bars (trade_date TEXT,ts_code TEXT,open REAL,high REAL,
              low REAL,close REAL,pct_chg REAL,vol REAL,amount REAL);
            CREATE TABLE dashboard_snapshots
              (trade_date TEXT PRIMARY KEY,source TEXT,payload TEXT,record_count INTEGER,updated_at TEXT);
            CREATE TABLE watchlist (user_id INTEGER,code TEXT,name TEXT,sector TEXT,color TEXT,
              created_at TEXT,updated_at TEXT,remark TEXT);
            CREATE TABLE review_notes (id INTEGER PRIMARY KEY,code TEXT,stock_name TEXT,
              trade_date TEXT,content TEXT,plan TEXT,created_at TEXT,updated_at TEXT,
              user_id INTEGER,summary TEXT);
            CREATE TABLE trade_entries (id INTEGER PRIMARY KEY,user_id INTEGER,trade_date TEXT,
              code TEXT,name TEXT,action TEXT,price REAL,quantity INTEGER,position_pct REAL,
              pnl_amount REAL,pnl_pct REAL,thesis TEXT,execution TEXT,emotion TEXT,tags TEXT,
              created_at TEXT,updated_at TEXT);
            CREATE TABLE alerts (id INTEGER PRIMARY KEY,user_id INTEGER,kind TEXT,title TEXT,
              content TEXT,available_date TEXT,code TEXT,dedupe_key TEXT,is_read INTEGER,
              created_at TEXT,updated_at TEXT,read_at TEXT);
            CREATE TABLE assistant_messages (id INTEGER PRIMARY KEY,user_id INTEGER,role TEXT,
              content TEXT,context_date TEXT,created_at TEXT);
            CREATE TABLE mentor_preferences (user_id INTEGER,mentor_id TEXT,pinned INTEGER,
              sort_order INTEGER,updated_at TEXT);
            CREATE TABLE mentor_messages (id INTEGER PRIMARY KEY,user_id INTEGER,mentor_id TEXT,
              trade_date TEXT,role TEXT,content TEXT,meta TEXT,created_at TEXT);
            CREATE TABLE heaven_readings (id INTEGER PRIMARY KEY,user_id INTEGER,mode TEXT,
              context_date TEXT,subject TEXT,subject_detail TEXT,answer TEXT,
              context_snapshot TEXT,dedupe_key TEXT,created_at TEXT);
            CREATE TABLE screener_runs (id INTEGER PRIMARY KEY,trade_date TEXT,regime TEXT,
              strategy_name TEXT,formula TEXT,result TEXT,created_at TEXT,user_id INTEGER,mode TEXT);
            CREATE TABLE screener_strategies (id INTEGER PRIMARY KEY,name TEXT,description TEXT,
              regimes TEXT,formula TEXT,builtin INTEGER,created_at TEXT,updated_at TEXT,user_id INTEGER);
            CREATE TABLE strategy_tracks (id INTEGER PRIMARY KEY,user_id INTEGER,run_id INTEGER,
              selection_date TEXT,strategy_name TEXT,ts_code TEXT,code TEXT,name TEXT,sector TEXT,
              entry_price REAL,created_at TEXT,updated_at TEXT);
            CREATE TABLE data_snapshots (kind TEXT,cache_key TEXT,source TEXT,payload TEXT,
              updated_at TEXT,PRIMARY KEY(kind,cache_key));
            CREATE TABLE reason_overrides (trade_date TEXT,code TEXT,reason TEXT,
              updated_at TEXT,PRIMARY KEY(trade_date,code));
            CREATE TABLE seat_aliases (seat_name TEXT PRIMARY KEY,alias TEXT,updated_at TEXT);
            """
        )
        now = "2026-07-29T16:00:00+08:00"
        connection.execute(
            "INSERT INTO users VALUES (8,'leefer',?,?,?,?,'admin','auto','active','永久',?,NULL)",
            (encoded_salt, encoded_hash, now, now, now),
        )
        profile = fernet.encrypt(
            b'{"birth_datetime":"1990-03-08T08:30:00","gender":"male"}'
        ).decode()
        connection.execute("INSERT INTO user_birth_profiles VALUES (8,?,?)", (profile, now))
        connection.execute(
            "INSERT INTO llm_usage VALUES (1,8,'mentor','model','','success',3,?,'','','',0,0)",
            (now,),
        )
        encrypted = fernet.encrypt(json.dumps(credentials).encode()).decode()
        connection.execute("INSERT INTO system_settings VALUES ('credentials',?,?)", (encrypted, now))
        connection.execute(
            "INSERT INTO stock_master VALUES ('000001.SZ','000001','平安银行','银行','主板','19910403',?)",
            (now,),
        )
        connection.execute(
            "INSERT INTO daily_bars VALUES ('20260729','000001.SZ',10,11,9,10.5,5,100,1000)"
        )
        dashboard = json.dumps(
            {
                "meta": {
                    "requested_date": "2026-07-30",
                    "trade_date": "2026-07-29",
                    "previous_trade_date": "2026-07-28",
                },
                "overview": {
                    "up_count": 1,
                    "down_count": 0,
                    "flat_count": 0,
                    "limit_up_count": 1,
                    "limit_down_count": 0,
                    "broken_count": 0,
                    "amount_billion": 12.5,
                    "seal_rate": 100,
                    "sentiment_score": 64,
                    "sentiment_label": "情绪偏强",
                    "sentiment_phase": "修复",
                    "sentiment_direction": "升温",
                    "sentiment_components": {
                        "breadth": {"label": "市场宽度", "score": 100, "weight": 20}
                    },
                    "sentiment_engine_version": 2,
                },
                "limits": [
                    {
                        "ts_code": "000001.SZ",
                        "code": "000001",
                        "name": "Ping An Bank",
                        "reason": "legacy",
                        "amount_billion": 1.25,
                        "seal_amount_million": 20,
                    }
                ],
                "broken": [],
                "down_limits": [],
            }
        )
        connection.execute(
            "INSERT INTO dashboard_snapshots VALUES ('20260730','tushare',?,1,?)",
            (dashboard, now),
        )
        connection.execute(
            "INSERT INTO watchlist VALUES (8,'000001','平安银行','银行','red',?,?, '长期')",
            (now, now),
        )
        connection.execute(
            "INSERT INTO review_notes VALUES (1,'000001','平安银行','20260729','复盘','计划',?,?,8,'总结')",
            (now, now),
        )
        connection.execute(
            "INSERT INTO trade_entries VALUES (1,8,'20260729','000001','平安银行','buy',10,100,20,NULL,NULL,'逻辑','执行','calm','[]',?,?)",
            (now, now),
        )
        connection.execute(
            "INSERT INTO alerts VALUES (1,8,'manual','提醒','','20260730','','a',0,?,?,NULL)",
            (now, now),
        )
        connection.execute("INSERT INTO assistant_messages VALUES (1,8,'user','问题','20260729',?)", (now,))
        connection.execute("INSERT INTO mentor_preferences VALUES (8,'mentor',1,1,?)", (now,))
        connection.execute(
            "INSERT INTO mentor_messages VALUES (1,8,'mentor','20260729','user','问题','',?)", (now,)
        )
        connection.execute(
            "INSERT INTO heaven_readings VALUES (1,8,'fortune','20260729','','','结果','{}','d',?)", (now,)
        )
        result = json.dumps({"candidates": [{"identifier": "000001.SZ", "close": 10.5}]})
        connection.execute(
            "INSERT INTO screener_runs VALUES (1,'20260729','','策略','{}',?, ?,8,'curated')",
            (result, now),
        )
        connection.execute(
            "INSERT INTO screener_runs VALUES (2,'20260729','','退潮防守观察','{}',?, ?,8,'smart')",
            (result, now),
        )
        connection.execute(
            "INSERT INTO screener_runs VALUES (3,'20260729','','退潮防守观察','{}',?, ?,NULL,'smart')",
            (result, now),
        )
        multi_factor = strategy_by_id("curated-25")
        assert multi_factor is not None
        connection.execute(
            "INSERT INTO screener_runs VALUES (4,'20260729','','多因子综合打分(IC动态加权)',?, ?, ?,NULL,'curated')",
            (json.dumps(multi_factor["formula"]), result, now),
        )
        connection.execute(
            "INSERT INTO screener_strategies VALUES (1,'自定义','','[]','{}',0,?,?,8)",
            (now, now),
        )
        connection.execute(
            "INSERT INTO screener_strategies VALUES (2,'legacy builtin','','[]','{}',1,?,?,NULL)",
            (now, now),
        )
        connection.execute(
            "INSERT INTO strategy_tracks VALUES (1,8,1,'20260729','策略','000001.SZ','000001','平安银行','银行',10.5,?,?)",
            (now, now),
        )
        connection.execute(
            "INSERT INTO strategy_tracks VALUES (2,8,2,'20260729','退潮防守观察','000001.SZ','000001','平安银行','银行',10.5,?,?)",
            (now, now),
        )
        connection.execute(
            "INSERT INTO data_snapshots VALUES ('popularity_v1','20260729','legacy','{}',?)", (now,)
        )
        connection.execute(
            "INSERT INTO reason_overrides VALUES ('20260729','000001','admin reason',?)",
            (now,),
        )
        connection.execute(
            "INSERT INTO seat_aliases VALUES ('seat-a','trader-a',?)", (now,)
        )
        snapshots = {
            "ifind_event_enrichment_v1": (
                "20260729",
                {
                    "trade_date": "20260729",
                    "generated_at": now,
                    "limits": {
                        "000001": {
                            "reason": "ifind reason",
                            "first_time": "09:31:03",
                            "last_time": "14:52:01",
                            "open_times": 1,
                        }
                    },
                    "broken": {},
                    "down_limits": {},
                },
            ),
            "rotation_sector_members_v1": (
                "20260729:Bank",
                {
                    "meta": {"sector_code": "801780.SI"},
                    "rows": [
                        {
                            "ts_code": "000001.SZ",
                            "code": "000001",
                            "name": "Ping An Bank",
                            "quoted": True,
                        }
                    ],
                },
            ),
            "theme_directory_v1": (
                "ths",
                {"items": [{"ts_code": "885001.TI", "name": "Theme A"}]},
            ),
            "theme_library_v1": (
                "20260729",
                {
                    "meta": {"updated_at": now},
                    "summary": {"theme_count": 1},
                    "items": [{"code": "885001.TI", "name": "Theme A"}],
                },
            ),
            "theme_detail_v1": (
                "20260729:885001.TI",
                {
                    "meta": {"updated_at": now},
                    "theme": {"code": "885001.TI", "name": "Theme A"},
                    "series": [
                        {
                            "trade_date": "20260728",
                            "open": 10,
                            "high": 11,
                            "low": 9,
                            "close": 10,
                            "volume": 100,
                        },
                        {
                            "trade_date": "20260729",
                            "open": 10,
                            "high": 12,
                            "low": 10,
                            "close": 11,
                            "volume": 120,
                        },
                    ],
                    "members": [
                        {
                            "ts_code": "000001.SZ",
                            "code": "000001",
                            "name": "Ping An Bank",
                            "price": 10.5,
                            "change": 5,
                            "amount_billion": 1.2,
                            "has_quote": True,
                        }
                    ],
                    "summary": {"member_count": 1},
                },
            ),
            "hot_money_profiles_v1": (
                "directory",
                {
                    "profiles": [
                        {
                            "name": "trader-a",
                            "description": "profile",
                            "organizations": ["seat-a"],
                        }
                    ]
                },
            ),
            "dragon_tiger": (
                "20260729",
                {
                    "meta": {"updated_at": now},
                    "rows": [
                        {
                            "ts_code": "000001.SZ",
                            "name": "Ping An Bank",
                            "change": 5,
                            "reason": "listed",
                            "institutions": [
                                {
                                    "seat_name": "seat-a",
                                    "buy_million": 2,
                                    "sell_million": 1,
                                    "net_buy_million": 1,
                                }
                            ],
                        }
                    ],
                },
            ),
            "hot_money_detail_v3": (
                "20260729",
                {
                    "meta": {"updated_at": now},
                    "traders": [
                        {
                            "name": "trader-a",
                            "operations": [
                                {
                                    "ts_code": "000001.SZ",
                                    "name": "Ping An Bank",
                                    "change": 5,
                                    "seat_name": "seat-a",
                                    "buy_million": 2,
                                    "sell_million": 1,
                                    "net_buy_million": 1,
                                    "reason": "listed",
                                }
                            ],
                        }
                    ],
                },
            ),
        }
        connection.executemany(
            "INSERT INTO data_snapshots VALUES (?,?, 'legacy',?,?)",
            [
                (kind, cache_key, json.dumps(payload), now)
                for kind, (cache_key, payload) in snapshots.items()
            ],
        )


def test_legacy_migration_is_idempotent_and_preserves_login(tmp_path) -> None:
    source = tmp_path / "legacy.db"
    target = tmp_path / "next.db"
    key = Fernet.generate_key().decode()
    _legacy_database(source, key)

    first = LegacyMigrator(source, target, key).run()
    second = LegacyMigrator(source, target, key).run()

    assert first["integrity"] == second["integrity"] == "ok"
    assert first["unmapped_tables"] == []
    assert first["unmapped_snapshot_kinds"] == []
    assert first["source_tables"]["reason_overrides"] == 1
    assert first["intentionally_skipped_rows"]["screener_strategies_builtin"]["count"] == 1
    assert "market_event_revisions" in first["target_counts"]
    rendered = json.dumps(first)
    assert "secret-tushare" not in rendered
    assert "secret-model" not in rendered
    with sqlite3.connect(target) as connection:
        connection.row_factory = sqlite3.Row
        password = connection.execute("SELECT password_hash FROM users WHERE id=8").fetchone()[0]
        assert PasswordHasher().verify("Password123", password)
        assert connection.execute("SELECT count(*) FROM watchlist_entries").fetchone()[0] == 1
        profile_payload = json.loads(
            Fernet(key.encode()).decrypt(
                connection.execute(
                    "SELECT encrypted_payload FROM birth_profiles WHERE user_id=8"
                ).fetchone()[0].encode()
            )
        )
        assert profile_payload == {
            "birth_date": "1990-03-08",
            "birth_time": "08:30",
            "gender": "male",
        }
        assert connection.execute("SELECT count(*) FROM screener_runs").fetchone()[0] == 3
        stage_run = connection.execute(
            """SELECT id,owner_user_id,strategy_id,strategy_version FROM screener_runs
            WHERE mode='stage'"""
        ).fetchone()
        assert dict(stage_run) == {
            "id": 3,
            "owner_user_id": None,
            "strategy_id": "stage-06",
            "strategy_version": 1,
        }
        renamed = connection.execute(
            """SELECT strategy_id,strategy_name FROM screener_runs
            WHERE strategy_id='curated-25'"""
        ).fetchone()
        assert dict(renamed) == {
            "strategy_id": "curated-25",
            "strategy_name": "动态多因子（基础版）",
        }
        assert connection.execute("SELECT count(*) FROM strategy_tracks").fetchone()[0] == 2
        assert connection.execute(
            "SELECT run_id FROM strategy_tracks WHERE id=2"
        ).fetchone()[0] == 3
        assert connection.execute("SELECT daily_llm_limit FROM memberships").fetchone()[0] == 61
        assert connection.execute("SELECT count(*) FROM seat_aliases").fetchone()[0] == 1
        assert connection.execute(
            "SELECT count(*) FROM sector_member_snapshots"
        ).fetchone()[0] == 1
        assert connection.execute(
            "SELECT count(*) FROM chart_series WHERE entity_type='theme'"
        ).fetchone()[0] == 1
        summary = json.loads(
            connection.execute(
                "SELECT payload_json FROM market_summaries WHERE trade_date='2026-07-29'"
            ).fetchone()[0]
        )
        assert summary["limits"][0]["identifier"] == "000001.SZ"
        assert summary["trade_date"] == "2026-07-29"
        assert summary["overview"]["limit_up"] == 1
        assert summary["overview"]["amount"] == 1_250_000_000
        assert summary["limits"][0]["amount"] == 125_000_000
        assert summary["limits"][0]["seal_amount"] == 20_000_000
        assert summary["sentiment"]["score"] == 64
        assert summary["sentiment"]["phase"] == "修复"
        assert summary["sentiment"]["components"][0]["key"] == "breadth"
        all_revisions = tuple(
            connection.execute(
                """SELECT * FROM market_event_revisions WHERE trade_date='2026-07-29'
                ORDER BY priority DESC,id DESC"""
            ).fetchall()
        )
        assert len(all_revisions) == 2
        revisions = all_revisions[:1]
        revised = apply_event_revisions(summary, revisions)
        assert revised["limits"][0]["reason"] == "admin reason"
        raw_dragon = json.loads(
            connection.execute(
                """SELECT payload_json FROM market_insight_snapshots
                WHERE kind='dragon-list' AND trade_date='2026-07-29'"""
            ).fetchone()[0]
        )
        assert raw_dragon["profiles"][0]["desc"] == "profile"
        assert raw_dragon["official"][0]["hm_name"] == "trader-a"
