from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import Mock

import pytest

from scripts import benchmark_metainfo_rust as benchmark


def test_build_inputs_separates_video_and_music_domains():
    """影视与音乐输入应独立扩展，并按 repeat 稳定重复。"""
    video_once = benchmark.build_video_inputs(1)
    music_once = benchmark.build_music_inputs(1)

    assert benchmark.build_video_inputs(2) == video_once * 2
    assert benchmark.build_music_inputs(2) == music_once * 2
    assert {kind for kind, _value, _subtitle in music_once} == {
        "music_query",
        "title",
        "path",
    }
    assert all(not value.lower().endswith(benchmark.get_audio_extensions())
               for kind, value, _subtitle in music_once if kind == "music_query")


def test_parse_input_uses_public_production_entries(monkeypatch):
    """输入分发应调用 MetaInfo、MetaInfoPath 和 MetaMusic.parse_query 公开入口。"""
    title_result = object()
    path_result = object()
    music_result = object()
    title_parser = Mock(return_value=title_result)
    path_parser = Mock(return_value=path_result)
    music_parser = Mock(return_value=music_result)
    monkeypatch.setattr(benchmark, "MetaInfo", title_parser)
    monkeypatch.setattr(benchmark, "MetaInfoPath", path_parser)
    monkeypatch.setattr(benchmark.MetaMusic, "parse_query", music_parser)

    assert benchmark.parse_input(("title", "Movie 2026", "subtitle")) is title_result
    assert benchmark.parse_input(("path", "/media/Movie 2026/movie.mkv", None)) is path_result
    assert benchmark.parse_input(("music_query", "Artist - Track", None)) is music_result
    title_parser.assert_called_once_with(
        title="Movie 2026",
        subtitle="subtitle",
        custom_words=["#"],
    )
    path_parser.assert_called_once_with(benchmark.Path("/media/Movie 2026/movie.mkv"))
    music_parser.assert_called_once_with("Artist - Track")


def test_selected_meta_parser_disables_and_restores_all_fast_paths(monkeypatch):
    """Python 对照上下文应屏蔽影视和音乐 Rust 入口，并完整恢复原函数。"""
    rust_accel = benchmark.rust_accel
    parser_names = (
        "parse_metainfo",
        "parse_metainfo_path",
        "find_metainfo",
        "parse_metamusic",
    )
    originals = {}
    for name in parser_names:
        parser = Mock(name=name)
        monkeypatch.setattr(rust_accel, name, parser, raising=False)
        originals[name] = parser

    with benchmark.selected_meta_parser(use_rust=False):
        for name in parser_names:
            assert getattr(rust_accel, name)("sample") is None

    for name, parser in originals.items():
        assert getattr(rust_accel, name) is parser


def test_measure_switches_once_and_warms_up_outside_samples(monkeypatch):
    """一次测量只应切换一次解析器，并额外执行一轮不计时预热。"""
    context_calls = []
    parse_calls = []

    @contextmanager
    def fake_selected_meta_parser(use_rust: bool):
        """记录测试中的解析器上下文进入次数。"""
        context_calls.append(use_rust)
        yield

    def fake_parse_all(inputs):
        """记录测试中的每轮解析调用。"""
        parse_calls.append(inputs)
        return [object()] * len(inputs)

    monkeypatch.setattr(benchmark, "selected_meta_parser", fake_selected_meta_parser)
    monkeypatch.setattr(benchmark, "parse_all", fake_parse_all)

    elapsed, parsed_count = benchmark.measure(
        [("title", "Movie", None)],
        use_rust=False,
        loops=2,
        repeats=3,
    )

    assert context_calls == [False]
    assert len(parse_calls) == 7
    assert parsed_count == 1
    assert elapsed >= 0


def test_assert_projected_results_equal_reports_first_field_difference():
    """等价校验失败时应报告首个输入及稳定字段差异。"""
    inputs = [("music_query", "Artist - Track", None)]
    rust_result = SimpleNamespace(title="Track", artists=["Artist"])
    python_result = SimpleNamespace(title="Other", artists=["Artist"])

    with pytest.raises(AssertionError) as error:
        benchmark.assert_projected_results_equal(
            inputs,
            [rust_result],
            [python_result],
            lambda result: {
                "title": result.title,
                "artists": list(result.artists),
            },
        )

    message = str(error.value)
    assert "Artist - Track" in message
    assert "title" in message
    assert "Track" in message
    assert "Other" in message


def test_video_projection_ignores_python_parser_internal_state():
    """影视等价投影不应纳入 Python 解析器的临时私有字段。"""
    rust_result = benchmark.MetaInfo("Marty Supreme 2025 2160p WEB-DL")
    python_result = benchmark.MetaInfo("Marty Supreme 2025 2160p WEB-DL")
    rust_result._index = 1
    python_result._index = 99
    rust_result._effect = []
    python_result._effect = ["temporary"]

    assert benchmark.project_video_result(rust_result) == benchmark.project_video_result(
        python_result
    )


def test_validate_rust_runtime_rejects_disabled_and_old_extensions(monkeypatch):
    """运行前检查应拒绝关闭的 Rust 和缺少音乐入口的旧扩展。"""
    rust_accel = benchmark.rust_accel
    monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=False))

    with pytest.raises(RuntimeError, match="未启用"):
        benchmark.validate_rust_runtime()

    monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=True))
    monkeypatch.setattr(rust_accel, "parse_metamusic", Mock(return_value={}), raising=False)
    monkeypatch.setattr(rust_accel, "_moviepilot_rust", SimpleNamespace())

    with pytest.raises(RuntimeError, match="版本过旧"):
        benchmark.validate_rust_runtime()


def test_validate_rust_runtime_requires_successful_music_probe(monkeypatch):
    """音乐 Rust 入口存在但实际回退 Python 时也必须拒绝执行基准。"""
    rust_accel = benchmark.rust_accel
    extension = SimpleNamespace(parse_metamusic_fast=Mock())
    monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=True))
    monkeypatch.setattr(rust_accel, "_moviepilot_rust", extension)
    monkeypatch.setattr(rust_accel, "parse_metamusic", Mock(return_value=None), raising=False)

    with pytest.raises(RuntimeError, match="探针"):
        benchmark.validate_rust_runtime()


def test_main_outputs_independent_video_and_music_metrics(monkeypatch, capsys):
    """主程序应分别输出影视和音乐等价状态、耗时及性能提升。"""
    monkeypatch.setattr(benchmark, "validate_rust_runtime", Mock())
    monkeypatch.setattr(benchmark, "build_video_inputs", Mock(return_value=[("title", "V", None)]))
    monkeypatch.setattr(
        benchmark,
        "build_music_inputs",
        Mock(return_value=[("music_query", "M", None), ("title", "M.flac", None)]),
    )
    results = [
        {
            "rust_ms": 1.0,
            "python_ms": 2.0,
            "rust_count": 1,
            "python_count": 1,
            "speedup": 2.0,
        },
        {
            "rust_ms": 2.0,
            "python_ms": 6.0,
            "rust_count": 2,
            "python_count": 2,
            "speedup": 3.0,
        },
    ]
    monkeypatch.setattr(benchmark, "benchmark_suite", Mock(side_effect=results))
    monkeypatch.setattr(
        benchmark.sys,
        "argv",
        ["benchmark_metainfo_rust.py", "--repeat-inputs", "1", "--loops", "1", "--repeats", "1"],
    )

    assert benchmark.main() == 0
    output = capsys.readouterr().out
    assert "video_equivalent=true" in output
    assert "video_speedup=2.00x" in output
    assert "music_equivalent=true" in output
    assert "music_speedup=3.00x" in output
    assert "video_rust_us_per_item=1000.000" in output
    assert "music_rust_us_per_item=1000.000" in output


def test_main_reports_runtime_failure_with_nonzero_exit(monkeypatch, capsys):
    """Rust 未就绪时主程序应明确报错并返回非零状态。"""
    monkeypatch.setattr(
        benchmark,
        "validate_rust_runtime",
        Mock(side_effect=RuntimeError("old extension")),
    )
    monkeypatch.setattr(benchmark.sys, "argv", ["benchmark_metainfo_rust.py"])

    assert benchmark.main() == 2
    assert "benchmark_error=old extension" in capsys.readouterr().err
