"""Job Use Cases 单元测试"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from packages.application.jobs import (
    CancelJobUseCase,
    CompleteJobCommand,
    CompleteJobUseCase,
    CreateJobCommand,
    CreateJobUseCase,
    FailJobCommand,
    FailJobUseCase,
    GetJobStatisticsUseCase,
    GetJobUseCase,
    ListJobsUseCase,
    RetryJobUseCase,
    SubmitJobUseCase,
    UpdateJobProgressCommand,
    UpdateJobProgressUseCase,
)
from packages.domain.job import Job, JobStatus, JobType


@pytest.fixture
def mock_repo():
    return MagicMock()


@pytest.fixture
def sample_job():
    return Job.create(
        project_id="proj_001",
        job_type=JobType.VIDEO_COMPOSE,
        payload={"template_id": "tpl_001"},
        source_id="src_001",
        created_by_user_id="user_001",
        max_retries=3,
    )


class TestCreateJobCommand:
    """CreateJobCommand 测试"""

    def test_default_values(self):
        cmd = CreateJobCommand(project_id="p1", job_type=JobType.VIDEO_COMPOSE)
        assert cmd.project_id == "p1"
        assert cmd.payload == {}
        assert cmd.source_id == ""
        assert cmd.created_by_user_id == ""
        assert cmd.max_retries == 3


class TestCreateJobUseCase:
    """CreateJobUseCase 测试"""

    def test_create_success(self, mock_repo, sample_job):
        mock_repo.create.return_value = sample_job
        use_case = CreateJobUseCase(mock_repo)
        cmd = CreateJobCommand(
            project_id="proj_001",
            job_type=JobType.VIDEO_COMPOSE,
            payload={"template_id": "tpl_001"},
            source_id="src_001",
            created_by_user_id="user_001",
            max_retries=3,
        )
        result = use_case.execute(cmd)
        assert result.status == JobStatus.PENDING
        assert result.project_id == "proj_001"
        mock_repo.create.assert_called_once()

    def test_create_with_string_job_type(self, mock_repo):
        mock_repo.create.side_effect = lambda x: x
        use_case = CreateJobUseCase(mock_repo)
        cmd = CreateJobCommand(project_id="p1", job_type="video_compose")
        result = use_case.execute(cmd)
        assert result.job_type == JobType.VIDEO_COMPOSE


class TestSubmitJobUseCase:
    """SubmitJobUseCase 测试"""

    def test_submit_success(self, mock_repo, sample_job):
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = SubmitJobUseCase(mock_repo)
        result = use_case.execute(sample_job.id, celery_task_id="celery_123")
        assert result.status == JobStatus.RUNNING
        assert result.celery_task_id == "celery_123"

    def test_submit_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = SubmitJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute("nonexistent")

    def test_submit_wrong_status(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        use_case = SubmitJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="只有 pending"):
            use_case.execute(sample_job.id)


class TestUpdateJobProgressUseCase:
    """UpdateJobProgressUseCase 测试"""

    def test_update_progress_success(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = UpdateJobProgressUseCase(mock_repo)
        cmd = UpdateJobProgressCommand(job_id=sample_job.id, progress=50.0, current_stage="渲染中")
        result = use_case.execute(cmd)
        assert result.progress == 50.0
        assert result.current_stage == "渲染中"

    def test_update_progress_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = UpdateJobProgressUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute(UpdateJobProgressCommand(job_id="x", progress=10))

    def test_update_progress_wrong_status(self, mock_repo, sample_job):
        sample_job.status = JobStatus.PENDING
        mock_repo.get.return_value = sample_job
        use_case = UpdateJobProgressUseCase(mock_repo)
        with pytest.raises(ValueError, match="只有 running"):
            use_case.execute(UpdateJobProgressCommand(job_id=sample_job.id, progress=10))


class TestCompleteJobUseCase:
    """CompleteJobUseCase 测试"""

    def test_complete_from_running(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = CompleteJobUseCase(mock_repo)
        cmd = CompleteJobCommand(job_id=sample_job.id, result={"url": "http://..."})
        result = use_case.execute(cmd)
        assert result.status == JobStatus.SUCCESS
        assert result.result["url"] == "http://..."

    def test_complete_from_pending(self, mock_repo, sample_job):
        sample_job.status = JobStatus.PENDING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = CompleteJobUseCase(mock_repo)
        result = use_case.execute(CompleteJobCommand(job_id=sample_job.id))
        assert result.status == JobStatus.SUCCESS

    def test_complete_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = CompleteJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute(CompleteJobCommand(job_id="x"))

    def test_complete_failed_status_raises(self, mock_repo, sample_job):
        sample_job.status = JobStatus.FAILED
        mock_repo.get.return_value = sample_job
        use_case = CompleteJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="只有 running/pending"):
            use_case.execute(CompleteJobCommand(job_id=sample_job.id))


class TestFailJobUseCase:
    """FailJobUseCase 测试"""

    def test_fail_success(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = FailJobUseCase(mock_repo)
        cmd = FailJobCommand(job_id=sample_job.id, error_message="渲染失败")
        result = use_case.execute(cmd)
        assert result.status == JobStatus.FAILED
        assert "渲染失败" in result.error_message

    def test_fail_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = FailJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute(FailJobCommand(job_id="x", error_message="err"))

    def test_fail_updates_error_message(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = FailJobUseCase(mock_repo)
        result = use_case.execute(FailJobCommand(job_id=sample_job.id, error_message="连接超时"))
        assert result.error_message == "连接超时"


class TestRetryJobUseCase:
    """RetryJobUseCase 测试"""

    def test_retry_success(self, mock_repo, sample_job):
        sample_job.status = JobStatus.FAILED
        sample_job.retry_count = 1
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = RetryJobUseCase(mock_repo)
        result = use_case.execute(sample_job.id)
        assert result.status == JobStatus.PENDING
        assert result.retry_count == 2

    def test_retry_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = RetryJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute("nonexistent")


class TestCancelJobUseCase:
    """CancelJobUseCase 测试"""

    def test_cancel_pending(self, mock_repo, sample_job):
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = CancelJobUseCase(mock_repo)
        result = use_case.execute(sample_job.id)
        assert result.status == JobStatus.CANCELLED

    def test_cancel_running(self, mock_repo, sample_job):
        sample_job.status = JobStatus.RUNNING
        mock_repo.get.return_value = sample_job
        mock_repo.update.side_effect = lambda x: x
        use_case = CancelJobUseCase(mock_repo)
        result = use_case.execute(sample_job.id)
        assert result.status == JobStatus.CANCELLED

    def test_cancel_terminal_raises(self, mock_repo, sample_job):
        sample_job.status = JobStatus.SUCCESS
        mock_repo.get.return_value = sample_job
        use_case = CancelJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="终态"):
            use_case.execute(sample_job.id)

    def test_cancel_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = CancelJobUseCase(mock_repo)
        with pytest.raises(ValueError, match="任务不存在"):
            use_case.execute("nonexistent")


class TestGetJobUseCase:
    """GetJobUseCase 测试"""

    def test_get_exists(self, mock_repo, sample_job):
        mock_repo.get.return_value = sample_job
        use_case = GetJobUseCase(mock_repo)
        result = use_case.execute(sample_job.id)
        assert result.id == sample_job.id

    def test_get_not_found(self, mock_repo):
        mock_repo.get.return_value = None
        use_case = GetJobUseCase(mock_repo)
        result = use_case.execute("nonexistent")
        assert result is None


class TestListJobsUseCase:
    """ListJobsUseCase 测试"""

    def test_list_by_project(self, mock_repo, sample_job):
        mock_repo.list_by_project.return_value = [sample_job]
        use_case = ListJobsUseCase(mock_repo)
        result = use_case.execute(project_id="proj_001")
        assert len(result) == 1
        mock_repo.list_by_project.assert_called_once()

    def test_list_by_user(self, mock_repo, sample_job):
        mock_repo.list_by_user.return_value = [sample_job]
        use_case = ListJobsUseCase(mock_repo)
        result = use_case.execute(user_id="user_001")
        assert len(result) == 1
        mock_repo.list_by_user.assert_called_once()

    def test_list_no_filter_raises(self, mock_repo):
        use_case = ListJobsUseCase(mock_repo)
        with pytest.raises(ValueError, match="必须指定"):
            use_case.execute()

    def test_list_with_filters(self, mock_repo, sample_job):
        mock_repo.list_by_project.return_value = [sample_job]
        use_case = ListJobsUseCase(mock_repo)
        use_case.execute(
            project_id="p1",
            job_type=JobType.VIDEO_COMPOSE,
            status=JobStatus.RUNNING,
            limit=20,
            offset=10,
        )
        mock_repo.list_by_project.assert_called_once_with(
            "p1", job_type=JobType.VIDEO_COMPOSE, status=JobStatus.RUNNING, limit=20, offset=10
        )


class TestGetJobStatisticsUseCase:
    """GetJobStatisticsUseCase 测试"""

    def test_statistics(self, mock_repo):
        mock_repo.count_by_project.side_effect = [10, 2, 3, 4, 1]
        use_case = GetJobStatisticsUseCase(mock_repo)
        stats = use_case.execute("proj_001")
        assert stats["project_id"] == "proj_001"
        assert stats["total"] == 10
        assert stats["pending"] == 2
        assert stats["running"] == 3
        assert stats["success"] == 4
        assert stats["failed"] == 1
