from __future__ import annotations

import ast
import unittest
from datetime import datetime, timedelta
from pathlib import Path

from backend.data import (
    DataPolicyError,
    DataQualityError,
    DataSourcePolicy,
    QualityEvidence,
    build_data_gateway,
)
from backend.data.quality import market_timezone


class DataGatewayTests(unittest.TestCase):
    def test_policy_allows_registered_calculation_source(self) -> None:
        policy = DataSourcePolicy.load()
        contract = policy.assert_allowed(
            "market.stock_daily", "tushare", "calculation"
        )
        self.assertEqual(contract.primary, "tushare")

    def test_policy_rejects_public_web_source_for_calculation(self) -> None:
        policy = DataSourcePolicy.load()
        with self.assertRaises(DataPolicyError):
            policy.assert_allowed(
                "observation.realtime_indices", "eastmoney", "calculation"
            )

    def test_policy_rejects_blocked_dataset(self) -> None:
        policy = DataSourcePolicy.load()
        with self.assertRaises(DataPolicyError):
            policy.assert_allowed("market.level2", "unresolved", "display")

    def test_gateway_uses_live_token_supplier_and_shared_ifind(self) -> None:
        token = {"value": "first"}
        gateway = build_data_gateway(
            {"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
            lambda: token["value"],
        )
        self.assertEqual(gateway.tushare().token, "first")
        token["value"] = "second"
        self.assertEqual(gateway.tushare().token, "second")
        self.assertIs(gateway.chart_data.ifind, gateway.ifind)

    def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
        source = (
            Path(__file__).resolve().parents[1]
            / "backend"
            / "features"
            / "market"
            / "service.py"
        ).read_text(encoding="utf-8")
        self.assertEqual(source.count("TushareClient(self.token)"), 1)
        self.assertIn("return gateway.tushare()", source)

    def test_provider_construction_has_unique_declared_owners(self) -> None:
        root = Path(__file__).resolve().parents[1]
        owners = {
            "EastmoneyChartClient": {"backend/data/gateway.py"},
            "IfindHttpClient": {"backend/data/gateway.py"},
            "IfindProvider": {"backend/data/gateway.py"},
            "MarketChartClient": {"backend/data/gateway.py"},
            "TushareClient": {"backend/features/market/service.py"},
            "TushareProvider": {"backend/data/gateway.py"},
            "WebRealtimeAggregator": {"backend/data/gateway.py"},
        }
        found = {name: set() for name in owners}
        for path in (root / "backend").rglob("*.py"):
            relative = path.relative_to(root).as_posix()
            tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
            for node in ast.walk(tree):
                if not isinstance(node, ast.Call):
                    continue
                name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
                if name in found:
                    found[name].add(relative)
        self.assertEqual(found, owners)
        provider_source = (root / "backend/data/providers/tushare.py").read_text(
            encoding="utf-8"
        )
        self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_source)

    def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
        timezone = market_timezone()
        now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
        gateway = build_data_gateway({})
        report = gateway.require_quality(
            QualityEvidence(
                dataset_id="market.stock_daily",
                provider_id="tushare",
                data_time="2026-07-29",
                observed_at=now,
                actual_count=5000,
                expected_count=5000,
                adjustment="current-unadjusted",
                units={
                    "open": "CNY/share", "high": "CNY/share", "low": "CNY/share",
                    "close": "CNY/share", "pct_chg": "percent",
                    "volume_shares": "share", "amount_yuan": "CNY",
                },
            ),
            "calculation",
            now,
        )
        self.assertTrue(report.accepted)
        self.assertEqual(report.coverage_ratio, 1.0)

    def test_quality_gate_rejects_stale_dynamic_auction(self) -> None:
        timezone = market_timezone()
        now = datetime(2026, 7, 29, 9, 24, tzinfo=timezone)
        gateway = build_data_gateway({})
        with self.assertRaises(DataQualityError):
            gateway.require_quality(
                QualityEvidence(
                    dataset_id="market.auction_dynamic",
                    provider_id="ifind",
                    data_time=now - timedelta(seconds=30),
                    observed_at=now - timedelta(seconds=29),
                    units={
                        "price": "CNY/share", "volume_shares": "share",
                        "amount_yuan": "CNY", "pre_close": "CNY/share",
                        "turnover_rate_pct": "percent", "volume_ratio": "ratio",
                        "float_share": "share",
                    },
                ),
                "calculation",
                now,
            )

    def test_quality_gate_rejects_low_coverage_and_wrong_adjustment(self) -> None:
        timezone = market_timezone()
        now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
        gateway = build_data_gateway({})
        report = gateway.quality.evaluate(
            QualityEvidence(
                dataset_id="market.stock_daily",
                provider_id="tushare",
                data_time="2026-07-29",
                observed_at=now,
                actual_count=4000,
                expected_count=5000,
                adjustment="forward1",
            ),
            "calculation",
            now,
        )
        self.assertFalse(report.accepted)
        self.assertTrue(any("Coverage" in issue for issue in report.issues))
        self.assertTrue(any("Adjustment" in issue for issue in report.issues))

    def test_quality_gate_enforces_financial_point_in_time(self) -> None:
        timezone = market_timezone()
        now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
        gateway = build_data_gateway({})
        report = gateway.quality.evaluate(
            QualityEvidence(
                dataset_id="market.fundamentals",
                provider_id="tushare",
                data_time="2026-06-30",
                observed_at=now,
                available_at="2026-08-15",
            ),
            "calculation",
            now,
        )
        self.assertFalse(report.accepted)
        self.assertTrue(any("not available" in issue for issue in report.issues))

    def test_provider_chain_never_silently_promotes_display_fallback(self) -> None:
        gateway = build_data_gateway({})
        self.assertEqual(
            gateway.provider_chain("chart.intraday", "display"),
            ("ifind", "eastmoney"),
        )
        with self.assertRaises(RuntimeError):
            gateway.provider_chain("chart.intraday", "calculation")


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