"""M7 定时任务测试。

使用 mock subprocess.run,不依赖真实脚本执行。
"""

from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

from scheduler import PipelineResult, StepResult, run_pipeline, run_step

# --------------------------------------------------------------------------- #
# fixtures
# --------------------------------------------------------------------------- #

def _mock_proc(returncode: int = 0, stderr: str = "") -> MagicMock:
    p = MagicMock()
    p.returncode = returncode
    p.stderr = stderr
    p.stdout = "ok"
    return p


# --------------------------------------------------------------------------- #
# StepResult 模型
# --------------------------------------------------------------------------- #

def test_step_result_defaults() -> None:
    sr = StepResult(name="crawler", success=True, elapsed_sec=12.3)
    assert sr.name == "crawler"
    assert sr.success is True
    assert sr.elapsed_sec == 12.3
    assert sr.exit_code is None


def test_pipeline_result_all_success() -> None:
    pr = PipelineResult(steps=[
        StepResult(name="crawler", success=True, elapsed_sec=1),
        StepResult(name="extractor", success=True, elapsed_sec=2),
    ])
    assert pr.all_success is True


def test_pipeline_result_partial_failure() -> None:
    pr = PipelineResult(steps=[
        StepResult(name="crawler", success=True, elapsed_sec=1),
        StepResult(name="extractor", success=False, elapsed_sec=0),
    ])
    assert pr.all_success is False


# --------------------------------------------------------------------------- #
# run_step
# --------------------------------------------------------------------------- #

def test_run_step_success() -> None:
    with patch("subprocess.run", return_value=_mock_proc(returncode=0,
                                                          stderr="INFO | 完成: 成功 20/20")):
        sr = run_step("crawler", "20260616")
    assert sr.success is True
    assert sr.exit_code == 0


def test_run_step_failure() -> None:
    with patch("subprocess.run", return_value=_mock_proc(returncode=1)):
        sr = run_step("extractor", "20260616")
    assert sr.success is False
    assert sr.exit_code == 1
    assert "rc=1" in sr.tail_msg


def test_run_step_timeout() -> None:
    with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd=["uv"], timeout=10)):
        sr = run_step("llm", "20260616")
    assert sr.success is False
    assert "超时" in sr.tail_msg


def test_run_step_exception() -> None:
    with patch("subprocess.run", side_effect=OSError("磁盘满")):
        sr = run_step("embedding", "20260616")
    assert sr.success is False
    assert "磁盘满" in sr.tail_msg


def test_run_step_unknown_name() -> None:
    sr = run_step("nonexistent", "20260616")
    assert sr.success is False
    assert "未知" in sr.tail_msg


# --------------------------------------------------------------------------- #
# run_pipeline
# --------------------------------------------------------------------------- #

def test_run_pipeline_all_success() -> None:
    with (
        patch("subprocess.run", return_value=_mock_proc(returncode=0)),
        patch("scheduler.reporter.generate_report", return_value=Path("/tmp/r.html")),
    ):
        result = run_pipeline("20260616", steps=["crawler", "extractor", "dedup"])
    assert len(result.steps) == 3
    assert result.all_success is True
    assert result.started_at is not None
    assert result.finished_at is not None


def test_run_pipeline_continues_on_failure() -> None:
    """中间步骤失败,后续继续执行(不阻断)。"""
    call_count = {"n": 0}

    def _side_effect(*args, **kwargs):
        call_count["n"] += 1
        if call_count["n"] == 2:  # extractor 失败
            return _mock_proc(returncode=1, stderr="GNE 提取异常")
        return _mock_proc(returncode=0, stderr="ok")

    with patch("subprocess.run", side_effect=_side_effect):
        result = run_pipeline("20260616", steps=["crawler", "extractor", "dedup", "llm"])
    assert len(result.steps) == 4
    # extractor 失败,但后续仍执行
    assert result.steps[1].success is False
    assert result.steps[2].success is True


def test_run_pipeline_custom_steps() -> None:
    with patch("subprocess.run", return_value=_mock_proc(returncode=0, stderr="ok")):
        result = run_pipeline("20260616", steps=["crawler", "extractor"])
    assert len(result.steps) == 2
    assert result.all_success is True
