"""MediaKit 重试逻辑 + 错误日志增强测试.

覆盖范围：
1. _is_retryable_error — 可重试错误判断
2. extract_frames 重试 — signal: killed / InternalError 自动重试一次
3. analyze_videos 重试 — 同上
4. 不可重试错误不触发重试
5. 默认抽帧策略为 TimeInterval
"""

from __future__ import annotations

from unittest.mock import MagicMock, Mock, patch

import pytest

from packages.shared.mediakit_client import (
    MediaKitClient,
    _is_retryable_error,
)

# ── _is_retryable_error ──────────────────────────────────────────────────────


class TestIsRetryableError:
    """可重试错误判断."""

    def test_signal_killed(self):
        """signal: killed 可重试."""
        assert _is_retryable_error("signal: killed") is True

    def test_internal_error(self):
        """InternalError 可重试."""
        assert _is_retryable_error("InternalError: something went wrong") is True

    def test_oom(self):
        """OOM 可重试."""
        assert _is_retryable_error("ExtractFrames OOM") is True

    def test_out_of_memory(self):
        """out of memory 可重试."""
        assert _is_retryable_error("process out of memory") is True

    def test_model_timeout_not_retryable(self):
        """model timeout 不可重试."""
        assert _is_retryable_error("model timeout") is False

    def test_network_error_not_retryable(self):
        """普通网络错误不可重试."""
        assert _is_retryable_error("Connection refused") is False

    def test_empty_string(self):
        """空字符串不可重试."""
        assert _is_retryable_error("") is False

    def test_case_insensitive(self):
        """大小写不敏感."""
        assert _is_retryable_error("SIGNAL: KILLED") is True
        assert _is_retryable_error("internalerror") is True


# ── extract_frames 重试逻辑 ──────────────────────────────────────────────────


class TestExtractFramesRetry:
    """extract_frames 重试逻辑测试."""

    def _make_client(self, api_key: str = "test-key") -> MediaKitClient:
        with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
            mock_settings.return_value = MagicMock(
                mediakit_api_key=api_key,
                mediakit_base_url="https://mock.mediakit.com/api/v1",
                mediakit_timeout=30,
            )
            return MediaKitClient()

    @patch("packages.shared.mediakit_client.time.sleep")
    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_retry_on_signal_killed_then_success(self, mock_post, mock_get, mock_sleep):
        """signal: killed 后重试成功."""
        # 第一次提交成功
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-retry-1"},
            raise_for_status=MagicMock(),
        )

        # 第一次轮询返回 failed(signal: killed)，第二次返回 success
        call_count = [0]

        def get_side_effect(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                # 第一次轮询：失败
                return MagicMock(
                    status_code=200,
                    json=lambda: {"status": "failed", "error": "signal: killed"},
                    raise_for_status=MagicMock(),
                )
            else:
                # 第二次轮询（重试后）：成功
                return MagicMock(
                    status_code=200,
                    json=lambda: {
                        "status": "success",
                        "result": {"snapshots": [{"image_url": "https://example.com/frame.jpg", "timestamp": 1.0}]},
                    },
                    raise_for_status=MagicMock(),
                )

        mock_get.side_effect = get_side_effect

        client = self._make_client()
        frames = client.extract_frames(
            video_url="https://example.com/video.mp4",
            strategy="TimeInterval",
            max_frames=5,
            poll_interval=0.01,
            max_retries=1,
        )

        assert frames is not None
        assert len(frames) == 1
        assert frames[0]["image_url"] == "https://example.com/frame.jpg"

        # 验证 sleep 被调用（重试间隔）
        mock_sleep.assert_called()

    @patch("packages.shared.mediakit_client.time.sleep")
    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_no_retry_on_non_retryable_error(self, mock_post, mock_get, mock_sleep):
        """不可重试错误直接返回 None，不重试."""
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-fail"},
            raise_for_status=MagicMock(),
        )

        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "failed", "error": "model timeout"},
            raise_for_status=MagicMock(),
        )

        client = self._make_client()
        frames = client.extract_frames(
            video_url="https://example.com/video.mp4",
            strategy="TimeInterval",
            max_frames=5,
            poll_interval=0.01,
            max_retries=1,
        )

        assert frames is None
        # 不可重试错误不应该调用 sleep（不重试）
        mock_sleep.assert_not_called()

    @patch("packages.shared.mediakit_client.time.sleep")
    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_retry_exhausted_returns_none(self, mock_post, mock_get, mock_sleep):
        """重试次数用尽后返回 None."""
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-fail-retry"},
            raise_for_status=MagicMock(),
        )

        # 每次都返回 signal: killed
        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "failed", "error": "signal: killed"},
            raise_for_status=MagicMock(),
        )

        client = self._make_client()
        frames = client.extract_frames(
            video_url="https://example.com/video.mp4",
            strategy="TimeInterval",
            max_frames=5,
            poll_interval=0.01,
            max_retries=1,  # 最多重试 1 次
        )

        assert frames is None
        # post 被调用 2 次（原始 + 1 次重试）
        assert mock_post.call_count == 2

    def test_default_strategy_is_time_interval(self):
        """默认抽帧策略为 TimeInterval（不是 SceneChange）."""
        with patch("packages.shared.mediakit_client.httpx.post") as mock_post:
            with patch("packages.shared.mediakit_client.httpx.get") as mock_get:
                mock_post.return_value = MagicMock(
                    status_code=200,
                    json=lambda: {"task_id": "task-default"},
                    raise_for_status=MagicMock(),
                )
                mock_get.return_value = MagicMock(
                    status_code=200,
                    json=lambda: {
                        "status": "success",
                        "result": {"snapshots": [{"image_url": "url", "timestamp": 0.0}]},
                    },
                    raise_for_status=MagicMock(),
                )

                client = self._make_client()
                # 不传 strategy，使用默认值
                client.extract_frames(
                    video_url="https://example.com/video.mp4",
                    poll_interval=0.01,
                )

                # 验证提交时使用了 TimeInterval
                call_args = mock_post.call_args
                payload = call_args.kwargs["json"]
                assert payload["strategy"] == "TimeInterval"


# ── analyze_videos 重试逻辑 ──────────────────────────────────────────────────


class TestAnalyzeVideosRetry:
    """analyze_videos 重试逻辑测试."""

    def _make_client(self, api_key: str = "test-key") -> MediaKitClient:
        with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
            mock_settings.return_value = MagicMock(
                mediakit_api_key=api_key,
                mediakit_base_url="https://mock.mediakit.com/api/v1",
                mediakit_timeout=30,
            )
            return MediaKitClient()

    @patch("packages.shared.mediakit_client.time.sleep")
    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_retry_on_internal_error_then_success(self, mock_post, mock_get, mock_sleep):
        """InternalError 后重试成功."""
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-retry-vu"},
            raise_for_status=MagicMock(),
        )

        call_count = [0]

        def get_side_effect(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                return MagicMock(
                    status_code=200,
                    json=lambda: {"status": "failed", "error": "InternalError: service busy"},
                    raise_for_status=MagicMock(),
                )
            else:
                return MagicMock(
                    status_code=200,
                    json=lambda: {
                        "status": "completed",
                        "result": {"contents": ["视频展示了一只猫在沙发上睡觉"]},
                    },
                    raise_for_status=MagicMock(),
                )

        mock_get.side_effect = get_side_effect

        client = self._make_client()
        result = client.analyze_videos(
            video_urls=["https://example.com/cat.mp4"],
            prompt="描述视频内容",
            poll_interval=0.01,
            max_retries=1,
        )

        assert result is not None
        assert len(result) == 1
        assert "猫" in result[0]
        mock_sleep.assert_called()

    @patch("packages.shared.mediakit_client.time.sleep")
    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_no_retry_on_unknown_error(self, mock_post, mock_get, mock_sleep):
        """未知错误不重试."""
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-unknown-fail"},
            raise_for_status=MagicMock(),
        )

        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "failed", "error": "unknown error"},
            raise_for_status=MagicMock(),
        )

        client = self._make_client()
        result = client.analyze_videos(
            video_urls=["https://example.com/v.mp4"],
            prompt="describe",
            poll_interval=0.01,
            max_retries=1,
        )

        assert result is None
        mock_sleep.assert_not_called()
        # post 只调用 1 次（不重试）
        assert mock_post.call_count == 1


# ── 错误日志增强 ─────────────────────────────────────────────────────────────


class TestErrorLogging:
    """错误日志包含完整信息."""

    def _make_client(self, api_key: str = "test-key") -> MediaKitClient:
        with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
            mock_settings.return_value = MagicMock(
                mediakit_api_key=api_key,
                mediakit_base_url="https://mock.mediakit.com/api/v1",
                mediakit_timeout=30,
            )
            return MediaKitClient()

    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_extract_frames_error_includes_task_id(self, mock_post, mock_get, caplog):
        """抽帧失败日志包含 task_id 和完整 error."""
        import logging

        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-log-test"},
            raise_for_status=MagicMock(),
        )

        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {
                "status": "failed",
                "error": "signal: killed",
                "error_node": "ExtractFrames",
            },
            raise_for_status=MagicMock(),
        )

        client = self._make_client()

        with caplog.at_level(logging.ERROR):
            client.extract_frames(
                video_url="https://example.com/video.mp4",
                strategy="TimeInterval",
                max_frames=5,
                poll_interval=0.01,
                max_retries=0,  # 不重试，直接失败
            )

        # 验证日志中包含 task_id 和 error 信息
        error_logs = [r.message for r in caplog.records if r.levelno >= logging.ERROR]
        assert any("task-log-test" in msg for msg in error_logs)
        assert any("signal: killed" in msg for msg in error_logs)

    @patch("packages.shared.mediakit_client.httpx.get")
    @patch("packages.shared.mediakit_client.httpx.post")
    def test_analyze_videos_error_includes_full_error(self, mock_post, mock_get, caplog):
        """视频理解失败日志包含完整 error 和 error_node."""
        import logging

        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"task_id": "task-vu-log"},
            raise_for_status=MagicMock(),
        )

        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {
                "status": "failed",
                "error": "signal: killed in ExtractFrames",
                "error_node": "ExtractFrames",
            },
            raise_for_status=MagicMock(),
        )

        client = self._make_client()

        with caplog.at_level(logging.ERROR):
            client.analyze_videos(
                video_urls=["https://example.com/v.mp4"],
                prompt="describe",
                poll_interval=0.01,
                max_retries=0,
            )

        error_logs = [r.message for r in caplog.records if r.levelno >= logging.ERROR]
        assert any("task-vu-log" in msg for msg in error_logs)
        assert any("ExtractFrames" in msg for msg in error_logs)
