from __future__ import annotations

import json
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

import httpx
import pytest

from backend.bootstrap.application import create_application
from backend.bootstrap.settings import Settings
from backend.data.contracts import (
    DataSource,
    DataUsage,
    ObservationMetadata,
    ProviderResult,
    SnapshotState,
)
from backend.data.repository import MarketRepository
from backend.database.connection import Database
from backend.database.migrations import MIGRATIONS, MigrationRunner
from backend.features.market.events import MarketEventService, apply_event_revisions
from backend.features.market.snapshot import build_realtime_inputs, build_snapshot
from backend.http.errors import AppError
from backend.jobs.repository import JobRepository
from backend.jobs.service import JobAlreadyRunning, JobService
from tests.support import run_scenario
from tests.test_accounts import (
    ADMIN_PASSWORD,
    USER_PASSWORD,
    register,
    use_session,
)

SHANGHAI = ZoneInfo("Asia/Shanghai")


def test_job_service_records_success_failure_attempts_and_exclusion(tmp_path) -> None:
    database = Database(tmp_path / "jobs.db")
    MigrationRunner(database).upgrade(MIGRATIONS)
    repository = JobRepository()
    jobs = JobService(database, repository)

    result = jobs.execute(
        kind="market.refresh",
        run_key="2026-07-30:manual",
        requested_date="2026-07-30",
        trigger="administrator",
        operation=lambda: {
            "coverage": 0.99,
            "source_set": ["tushare", "local"],
            "output_version": "market-summary-v1",
        },
        stale_after_seconds=120,
    )
    assert result["coverage"] == 0.99

    with pytest.raises(RuntimeError, match="network unavailable"):
        jobs.execute(
            kind="market.refresh",
            run_key="2026-07-30:retry",
            requested_date="2026-07-30",
            trigger="after-close",
            operation=lambda: (_ for _ in ()).throw(RuntimeError("network unavailable")),
            stale_after_seconds=120,
        )
    latest = jobs.latest()
    assert [row["status"] for row in latest[:2]] == ["failed", "completed"]
    assert latest[0]["error_message"] == "network unavailable"
    assert latest[1]["source_set"] == ["tushare", "local"]
    failure_finished = datetime.fromisoformat(latest[0]["finished_at"])
    assert not jobs.ready_for_schedule(
        "market.refresh",
        now=failure_finished + timedelta(seconds=59),
        completed_after_seconds=8,
        failed_after_seconds=60,
    )
    assert jobs.ready_for_schedule(
        "market.refresh",
        now=failure_finished + timedelta(seconds=60),
        completed_after_seconds=8,
        failed_after_seconds=60,
    )

    with database.transaction() as connection:
        repository.begin(
            connection,
            kind="auction.collect",
            run_key="active",
            requested_date="2026-07-30",
            trigger="auction-poll",
            started_at=datetime.now(SHANGHAI),
            stale_after_seconds=60,
        )
    with pytest.raises(JobAlreadyRunning):
        jobs.execute(
            kind="auction.collect",
            run_key="second",
            requested_date="2026-07-30",
            trigger="auction-poll",
            operation=lambda: {},
            stale_after_seconds=60,
        )
    assert not jobs.ready_for_schedule(
        "auction.collect",
        now=datetime.now(SHANGHAI) + timedelta(hours=1),
        completed_after_seconds=8,
        failed_after_seconds=60,
    )


def test_job_service_exposes_app_error_message_without_internal_tuple(tmp_path) -> None:
    database = Database(tmp_path / "jobs.db")
    MigrationRunner(database).upgrade(MIGRATIONS)
    jobs = JobService(database, JobRepository())

    with pytest.raises(AppError):
        jobs.execute(
            kind="market.refresh",
            run_key="2026-07-30:failed",
            requested_date="2026-07-30",
            trigger="administrator",
            operation=lambda: (_ for _ in ()).throw(
                AppError("market_data_unavailable", "收盘行情读取失败，已保留原有快照", 503)
            ),
            stale_after_seconds=120,
        )

    assert jobs.latest()[0]["error_message"] == "收盘行情读取失败，已保留原有快照"

    with database.transaction() as connection:
        connection.execute(
            "UPDATE job_runs SET error_message = ? WHERE id = ?",
            (
                "('market_data_unavailable', '收盘行情读取失败，已保留原有快照', 503)",
                jobs.latest()[0]["id"],
            ),
        )
    assert jobs.latest()[0]["error_message"] == "收盘行情读取失败，已保留原有快照"


class EventGateway:
    reason = "银行板块走强"

    def event_reasons(self, trade_date: str) -> ProviderResult:
        return ProviderResult(
            (
                {
                    "event_type": "limit_up",
                    "identifier": "000001.SZ",
                    "reason": self.reason,
                    "first_time": "09:35",
                    "last_time": "14:20",
                    "open_times": 1,
                },
            ),
            metadata(SnapshotState.FINAL),
        )


def test_admin_event_revision_wins_and_history_is_preserved(tmp_path) -> None:
    database = Database(tmp_path / "events.db")
    MigrationRunner(database).upgrade(MIGRATIONS)
    repository = MarketRepository()
    payload = {
        "limits": [{"identifier": "000001.SZ", "code": "000001", "reason": ""}],
        "broken": [],
        "down_limits": [],
    }
    with database.transaction() as connection:
        connection.execute(
            """
            INSERT INTO users (
                username, username_key, password_hash, is_admin, created_at, updated_at
            ) VALUES ('admin', 'admin', 'hash', 1, ?, ?)
            """,
            ("2026-07-30T09:00:00+08:00", "2026-07-30T09:00:00+08:00"),
        )
        connection.execute(
            """
            INSERT INTO market_summaries (
                trade_date, observed_at, state, source, coverage, payload_json, created_at
            ) VALUES ('2026-07-30', '2026-07-30T15:00:00+08:00', 'final',
                      'tushare', 1, ?, '2026-07-30T15:05:00+08:00')
            """,
            (json.dumps(payload, ensure_ascii=False),),
        )
    gateway = EventGateway()
    events = MarketEventService(database, repository, gateway)
    assert events.supplement("2026-07-30")["updated"] == 1
    events.revise(
        trade_date="2026-07-30",
        identifier="000001.SZ",
        event_type="limit_up",
        reason="管理员核验原因",
        first_time="09:36",
        last_time="14:21",
        open_times=2,
        user_id=1,
    )
    gateway.reason = "后续自动结果"
    assert events.supplement("2026-07-30")["updated"] == 0
    with database.read() as connection:
        revisions = repository.event_revisions(connection, "2026-07-30")
    resolved = apply_event_revisions(payload, revisions)
    assert resolved["limits"][0]["reason"] == "管理员核验原因"
    assert resolved["limits"][0]["reason_source"] == "admin"
    history = events.history("2026-07-30", "000001.SZ")
    assert [row["source"] for row in history] == ["admin", "ifind"]


def test_realtime_snapshot_uses_official_limits_and_yuan_amounts() -> None:
    inputs = {
        "daily": ProviderResult(
            (
                {
                    "ts_code": "000001.SZ",
                    "open": 10.2,
                    "high": 11,
                    "low": 10.1,
                    "close": 11,
                    "pre_close": 10,
                    "pct_chg": 10,
                    "amount": 100_000_000,
                    "amount_unit": "yuan",
                },
                {
                    "ts_code": "000002.SZ",
                    "open": 10,
                    "high": 11,
                    "low": 9.9,
                    "close": 10.5,
                    "pre_close": 10,
                    "pct_chg": 5,
                    "amount": 200_000_000,
                    "amount_unit": "yuan",
                },
            ),
            metadata(SnapshotState.REALTIME),
        ),
        "price_limits": ProviderResult(
            (
                {"ts_code": "000001.SZ", "up_limit": 11, "down_limit": 9},
                {"ts_code": "000002.SZ", "up_limit": 11, "down_limit": 9},
            ),
            metadata(SnapshotState.REALTIME),
        ),
        "previous_limit_up": ProviderResult(
            ({"ts_code": "000001.SZ", "limit_times": 2},),
            metadata(SnapshotState.FINAL),
        ),
    }
    directory = {
        "000001.SZ": {"name": "ST样本", "sector": "银行"},
        "000002.SZ": {"name": "炸板样本", "sector": "银行"},
    }
    snapshot = build_snapshot(
        "2026-07-30",
        "2026-07-29",
        build_realtime_inputs(inputs, directory),
    )
    assert snapshot["overview"]["amount"] == 300_000_000
    assert snapshot["limits"][0]["streak"] == 3
    assert snapshot["limits"][0]["amount"] == 100_000_000
    assert snapshot["broken"][0]["code"] == "000002"


def test_operations_status_and_admin_audit_permissions(tmp_path) -> None:
    application = create_application(Settings.for_test(tmp_path))

    async def scenario(client: httpx.AsyncClient) -> None:
        _, admin = await register(client, "operations-admin", ADMIN_PASSWORD)
        client.cookies.clear()
        _, regular = await register(client, "operations-user", USER_PASSWORD)
        status = await client.get("/api/operations/status")
        assert status.status_code == 200
        assert status.json()["state"] == "degraded"
        assert (await client.get("/api/admin/operations/jobs")).status_code == 403
        use_session(client, admin)
        jobs = await client.get("/api/admin/operations/jobs")
        assert jobs.status_code == 200
        assert jobs.json() == []

    run_scenario(application, scenario)


def metadata(state: SnapshotState) -> ObservationMetadata:
    return ObservationMetadata(
        source=DataSource.TUSHARE,
        observed_at=datetime(2026, 7, 30, 10, 0, tzinfo=SHANGHAI),
        unit="mixed",
        adjustment="not_applicable",
        freshness_seconds=0,
        coverage=1,
        state=state,
        usage=DataUsage.CALCULATION,
    )
