from __future__ import annotations

import json
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace

import pytest

from backend.bootstrap.settings import PROJECT_ROOT
from backend.data.contracts import (
    DataSource,
    DataUsage,
    MarketEntity,
    ObservationMetadata,
    ProviderResult,
    SnapshotState,
)
from backend.data.heaven import realtime_payload
from backend.data.repository import MarketRepository
from backend.database import MIGRATIONS, Database, MigrationRunner
from backend.features.accounts.models import MembershipRecord, Principal, UserRecord
from backend.features.heaven import fortune, trend
from backend.features.heaven.hexagram import from_lines
from backend.features.heaven.prompt import messages
from backend.features.heaven.repository import HeavenRepository
from backend.features.heaven.service import HeavenService

ICHING = PROJECT_ROOT / "config" / "heaven" / "iching_zh.json"


def _payload() -> dict:
    return {
        "trade_date": "2026-07-22",
        "mode": "historical",
        "stock": {
            "identifier": "601318.SH",
            "code": "601318",
            "name": "中国平安",
            "trade_date": "2026-07-22",
            "quote_kind": "daily",
            "change": 2.5,
            "amount_percentile": 88,
            "turnover_rate": 1.2,
            "seal_amount_million": 0,
            "open_times": 0,
            "streak": 0,
            "status": "普通",
        },
        "sector": {
            "name": "保险Ⅱ",
            "code": "801194.SI",
            "taxonomy": "申万二级",
            "trade_date": "2026-07-22",
            "quote_kind": "daily",
            "change": 2.2,
            "up_count": 5,
            "down_count": 0,
            "member_count": 5,
            "quoted_count": 5,
            "coverage": 1,
            "member_equal_change": 1.8,
            "leader": "新华保险",
            "leading_pct": 4.5,
        },
        "market": {
            "trade_date": "2026-07-22",
            "quote_kind": "daily",
            "sentiment_score": 42,
            "seal_rate": 73.9,
            "amount_billion": 11800,
            "average_amount_billion": 10500,
            "up_count": 3180,
            "down_count": 1730,
            "limit_up_count": 68,
            "limit_down_count": 6,
        },
        "indices": [
            {
                "identifier": "000001.SH",
                "trade_date": "2026-07-22",
                "quote_kind": "daily",
                "change": 0.6,
            },
            {
                "identifier": "399001.SZ",
                "trade_date": "2026-07-22",
                "quote_kind": "daily",
                "change": 1.1,
            },
            {
                "identifier": "399006.SZ",
                "trade_date": "2026-07-22",
                "quote_kind": "daily",
                "change": 1.4,
            },
        ],
    }


def _intraday_payload() -> dict:
    payload = _payload()
    payload["mode"] = "intraday"
    payload["stock"].update(
        quote_kind="realtime",
        turnover_relative=1.2,
        volume_activity_ratio=1.15,
    )
    payload["sector"].update(quote_kind="realtime", relative_turnover=1.1)
    payload["market"]["quote_kind"] = "realtime"
    for row in payload["indices"]:
        row["quote_kind"] = "realtime"
    return payload


def _database(tmp_path: Path) -> Database:
    database = Database(tmp_path / "heaven.db")
    MigrationRunner(database).upgrade(MIGRATIONS)
    with database.transaction() as connection:
        for user_id in (1, 2):
            connection.execute(
                """
                INSERT INTO users (
                    id, username, username_key, password_hash, is_admin,
                    status, created_at, updated_at
                ) VALUES (?, ?, ?, 'hash', 0, 'active', 'now', 'now')
                """,
                (user_id, f"user{user_id}", f"user{user_id}"),
            )
    return database


def _principal() -> Principal:
    now = datetime.now(UTC)
    return Principal(
        "token",
        "csrf",
        UserRecord(1, "user1", "user1", "hash", False, "active", now, now),
        MembershipRecord(1, "active", None, True, 50, now, 1),
    )


def _result(rows: list[dict]) -> ProviderResult:
    return ProviderResult(
        tuple(rows),
        ObservationMetadata(
            source=DataSource.TUSHARE,
            observed_at=datetime(2026, 7, 30, 10, tzinfo=UTC),
            unit="mixed",
            adjustment="not_applicable",
            freshness_seconds=0,
            coverage=1,
            state=SnapshotState.REALTIME,
            usage=DataUsage.CALCULATION,
        ),
    )


def test_small_sector_with_five_of_five_quotes_passes_all_gates() -> None:
    result = trend.calculate(_payload(), ICHING)

    assert len(result["hexagram"]["lines"]) == 6
    assert all(item["passed"] for item in result["checks"])
    assert result["checks"][2]["message"].endswith("5/5")


def test_missing_one_formal_index_fails_closed() -> None:
    payload = _payload()
    payload["indices"].pop()

    with pytest.raises(trend.TrendDataError) as captured:
        trend.calculate(payload, ICHING)

    assert captured.value.checks[5]["passed"] is False
    assert "三大指数" in captured.value.checks[5]["message"]


def test_intraday_payload_requires_real_activity_inputs() -> None:
    result = trend.calculate(_intraday_payload(), ICHING)
    assert all(item["passed"] for item in result["checks"])
    assert result["hexagram"]["lines"][0]["evidence"][1].startswith("相对换手")

    missing = _intraday_payload()
    missing["stock"]["volume_activity_ratio"] = None
    with pytest.raises(trend.TrendDataError) as captured:
        trend.calculate(missing, ICHING)
    assert captured.value.checks[0]["passed"] is False


def test_realtime_inputs_use_one_trade_date_and_official_sector_quote(tmp_path) -> None:
    database = _database(tmp_path)
    repository = MarketRepository()
    with database.transaction() as connection:
        repository.save_summary(
            connection,
            trade_date="2026-07-29",
            observed_at="2026-07-29T15:00:00+08:00",
            state="final",
            source="tushare",
            coverage=1,
            payload={
                "overview": {
                    "up_count": 2500,
                    "down_count": 2000,
                    "limit_up": 40,
                    "limit_down": 5,
                    "broken": 10,
                    "seal_rate": 80,
                    "amount": 1_000_000_000_000,
                },
                "sentiment": {"score": 50},
                "limits": [],
                "yesterday_limits": [],
            },
        )
    stock_codes = ("601318.SH", "601319.SH", "601336.SH", "601601.SH")
    realtime = [
        {
            "ts_code": code,
            "name": f"保险{index}",
            "trade_time": "2026-07-30 10:00:00",
            "close": 10 + index,
            "pre_close": 10,
            "high": 10 + index,
            "low": 9.8,
            "open": 10,
            "vol": 1_000_000 + index * 100_000,
            "amount": 100_000_000 + index * 10_000_000,
        }
        for index, code in enumerate(stock_codes)
    ]
    realtime.extend(
        {
            "ts_code": code,
            "name": code,
            "trade_time": "2026-07-30 10:00:00",
            "close": 101,
            "pre_close": 100,
            "high": 101,
            "low": 99,
            "open": 100,
            "vol": 1,
            "amount": 1,
        }
        for code in ("000001.SH", "399001.SZ", "399006.SZ")
    )
    raw = {
        "realtime": _result(realtime),
        "members": _result(
            [
                {
                    "sector_code": "801194.SI",
                    "sector_name": "保险Ⅱ",
                    "ts_code": code,
                    "name": f"保险{index}",
                }
                for index, code in enumerate(stock_codes)
            ]
        ),
        "capital": _result([{"ts_code": code, "float_share": 100_000} for code in stock_codes]),
        "stock_history": _result(
            [
                {"ts_code": "601318.SH", "trade_date": f"2026072{day}", "vol": 10_000}
                for day in range(5, 10)
            ]
        ),
        "price_limits": _result(
            [{"ts_code": code, "up_limit": 20, "down_limit": 5} for code in stock_codes]
        ),
        "suspensions": _result([]),
        "sector_realtime": _result(
            [
                {
                    "ts_code": "801194.SI",
                    "name": "保险Ⅱ",
                    "trade_time": "2026-07-30 10:00:00",
                    "close": 102,
                    "pre_close": 100,
                    "pct_change": 2,
                }
            ]
        ),
    }
    payload = realtime_payload(
        database,
        repository,
        MarketEntity("stock", "601318.SH", "601318", "中国平安"),
        "2026-07-30",
        "2026-07-29",
        raw,
        datetime(2026, 7, 30, 10, tzinfo=UTC),
    )

    assert payload["mode"] == "intraday"
    assert payload["stock"]["trade_date"] == "2026-07-30"
    assert payload["sector"]["change"] == 2
    assert payload["sector"]["quoted_count"] == 4
    assert len([row for row in payload["indices"] if row["change"] is not None]) == 3


def test_manual_objective_sector_value_recomputes_without_overwriting_valid_data() -> None:
    payload = _payload()
    payload["sector"]["change"] = None
    original_leader_change = payload["sector"]["leading_pct"]

    result = trend.calculate(
        payload,
        ICHING,
        {"sector": {"change": 2.8, "leading_pct": -9.9}},
    )

    assert result["sector"]["change"] == 2.8
    assert result["sector"]["leading_pct"] == original_leader_change
    assert result["checks"][3]["source"] == "manual"


def test_hexagram_is_deterministic_and_contains_only_six_lines() -> None:
    first = from_lines([7, 8, 9, 6, 7, 8], ICHING)
    second = from_lines([7, 8, 9, 6, 7, 8], ICHING)

    assert first == second
    assert len(first["lines"]) == 6
    assert first["moving_lines"] == [3, 4]


def test_fortune_uses_fixed_weight_total_and_composite_phrase() -> None:
    field = fortune.build("2026-07-30")

    assert sum(item["score"] for item in field["balance"]) == 100
    assert "·" in field["phrase"]
    assert [item["label"] for item in field["layers"]] == ["年纲", "客主加临", "日辰触发"]


def test_repository_history_is_account_isolated(tmp_path) -> None:
    database = _database(tmp_path)
    repository = HeavenRepository()
    with database.transaction() as connection:
        repository.add(
            connection,
            user_id=1,
            mode="heart",
            reading_date="2026-07-30",
            subject_key="",
            result={"a": 1},
            created_at="now",
        )
        repository.add(
            connection,
            user_id=2,
            mode="heart",
            reading_date="2026-07-30",
            subject_key="",
            result={"a": 2},
            created_at="now",
        )
    with database.read() as connection:
        first = repository.list(connection, 1, None, None)
        second = repository.list(connection, 2, None, None)

    assert json.loads(first[0]["result_json"]) == {"a": 1}
    assert json.loads(second[0]["result_json"]) == {"a": 2}


def test_daily_fortune_is_created_once_even_before_interpretation(tmp_path) -> None:
    database = _database(tmp_path)
    repository = HeavenRepository()
    with database.transaction() as connection:
        first, first_reused = repository.ensure_fortune(
            connection,
            user_id=1,
            reading_date="2026-07-30",
            result={"phrase": "初次结果"},
            created_at="now",
        )
        second, second_reused = repository.ensure_fortune(
            connection,
            user_id=1,
            reading_date="2026-07-30",
            result={"phrase": "不应覆盖"},
            created_at="later",
        )

    assert first_reused is False
    assert second_reused is True
    assert first["id"] == second["id"]
    assert json.loads(second["result_json"])["phrase"] == "初次结果"


def test_each_heart_cast_appends_exactly_one_line() -> None:
    service = HeavenService(
        SimpleNamespace(),
        SimpleNamespace(),
        SimpleNamespace(),
        SimpleNamespace(),
        SimpleNamespace(),
        ICHING,
    )

    first = service.heart_line(_principal(), "2026-07-30", [])
    second = service.heart_line(_principal(), "2026-07-30", first["values"])

    assert len(first["values"]) == 1
    assert len(second["values"]) == 2
    assert len(first["faces"]) == 3


def test_fortune_prompt_never_contains_raw_birth_fields() -> None:
    result = fortune.build(
        "2026-07-30",
        SimpleNamespace(birth_date="1990-01-02", birth_time="03:04", gender="male"),
    )
    prompt = json.dumps(messages("fortune", result), ensure_ascii=False)

    assert "1990-01-02" not in prompt
    assert "03:04" not in prompt
    assert '"gender"' not in prompt
