"""
分类任务 API 集成测试。

覆盖端点：
- POST /classification-jobs — 提交分类任务
- GET  /classification-jobs/{job_id} — 获取分类任务详情

使用 FastAPI TestClient + dependency_overrides 模式，
导入真实路由模块，mock Celery 和 repository。
"""

from __future__ import annotations

import os
import sys
from unittest.mock import MagicMock

# ── 环境变量 & sys.path（必须在导入 app.* 之前设置） ──────────────────────────
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "api"))

# mock celery_app 以避免实际发送任务
import app.api.routes.classification_jobs as classification_routes
from app.api.routes.classification_jobs import router
from app.dependencies import get_classification_job_repository

from packages.adapters.in_memory import InMemoryClassificationJobRepository
from packages.domain import ClassificationJob, ClassificationJobStatus

classification_routes.celery_app = MagicMock()


# ---------------------------------------------------------------------------
# 1. 辅助函数
# ---------------------------------------------------------------------------


def _make_job(
    project_id: str = "proj-1",
    asset_id: str = "asset-1",
    status: ClassificationJobStatus = ClassificationJobStatus.PENDING,
) -> ClassificationJob:
    job = ClassificationJob.create(project_id=project_id, asset_id=asset_id)
    if status == ClassificationJobStatus.PROCESSING:
        job.status = ClassificationJobStatus.PROCESSING
    elif status == ClassificationJobStatus.COMPLETED:
        job.status = ClassificationJobStatus.COMPLETED
        job.classification = "scenic"
        job.confidence = 0.92
    elif status == ClassificationJobStatus.FAILED:
        job.status = ClassificationJobStatus.FAILED
        job.error_message = "AI 服务不可用"
    return job


# ---------------------------------------------------------------------------
# 2. Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def repo():
    return InMemoryClassificationJobRepository()


@pytest.fixture
def client(repo):
    """创建带有依赖覆盖的 TestClient。"""
    test_app = FastAPI()
    test_app.include_router(router, prefix="/classification-jobs")

    def _override_repo():
        return repo

    test_app.dependency_overrides[get_classification_job_repository] = _override_repo

    yield TestClient(test_app)

    test_app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# 3. POST / — 提交分类任务
# ---------------------------------------------------------------------------


class TestSubmitClassificationJob:
    """提交分类任务端点测试。"""

    def test_submit_with_valid_data(self, client):
        """使用有效数据提交分类任务应成功。"""
        resp = client.post(
            "/classification-jobs",
            json={
                "project_id": "proj-123",
                "asset_id": "asset-456",
            },
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["project_id"] == "proj-123"
        assert data["asset_id"] == "asset-456"
        assert data["status"] == "pending"
        assert data["classification"] == ""
        assert data["confidence"] == 0.0
        assert data["error_message"] == ""
        assert "id" in data
        assert len(data["id"]) > 0

    def test_submit_generates_unique_id(self, client):
        """每次提交应生成不同的任务 ID。"""
        resp1 = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a1"})
        resp2 = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a2"})
        assert resp1.json()["id"] != resp2.json()["id"]

    def test_submit_missing_project_id_returns_422(self, client):
        """缺少 project_id 应返回 422。"""
        resp = client.post("/classification-jobs", json={"asset_id": "asset-1"})
        assert resp.status_code == 422

    def test_submit_missing_asset_id_returns_422(self, client):
        """缺少 asset_id 应返回 422。"""
        resp = client.post("/classification-jobs", json={"project_id": "proj-1"})
        assert resp.status_code == 422

    def test_submit_empty_project_id_returns_422(self, client):
        """空 project_id 应返回 422。"""
        resp = client.post("/classification-jobs", json={"project_id": "", "asset_id": "asset-1"})
        assert resp.status_code == 422

    def test_submit_empty_asset_id_returns_422(self, client):
        """空 asset_id 应返回 422。"""
        resp = client.post("/classification-jobs", json={"project_id": "proj-1", "asset_id": ""})
        assert resp.status_code == 422

    def test_submit_sends_celery_task(self, client):
        """提交任务后应触发 Celery 异步任务。"""
        classification_routes.celery_app.send_task.reset_mock()

        resp = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a1"})
        assert resp.status_code == 200

        job_id = resp.json()["id"]
        classification_routes.celery_app.send_task.assert_called_once_with(
            "worker.classify_asset",
            args=[job_id],
        )

    def test_submit_persists_to_repository(self, client, repo):
        """提交后任务应保存到 repository。"""
        resp = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a1"})
        job_id = resp.json()["id"]

        saved = repo.get(job_id)
        assert saved is not None
        assert saved.project_id == "p1"
        assert saved.asset_id == "a1"
        assert saved.status == ClassificationJobStatus.PENDING


# ---------------------------------------------------------------------------
# 4. GET /{job_id} — 获取分类任务详情
# ---------------------------------------------------------------------------


class TestGetClassificationJob:
    """获取分类任务详情端点测试。"""

    def test_get_pending_job(self, client, repo):
        """获取 pending 状态的任务。"""
        job = _make_job(status=ClassificationJobStatus.PENDING)
        repo.create(job)

        resp = client.get(f"/classification-jobs/{job.id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == job.id
        assert data["status"] == "pending"
        assert data["classification"] == ""
        assert data["confidence"] == 0.0

    def test_get_processing_job(self, client, repo):
        """获取 processing 状态的任务。"""
        job = _make_job(status=ClassificationJobStatus.PROCESSING)
        repo.create(job)

        resp = client.get(f"/classification-jobs/{job.id}")
        assert resp.status_code == 200
        assert resp.json()["status"] == "processing"

    def test_get_completed_job(self, client, repo):
        """获取已完成的任务应包含分类结果和置信度。"""
        job = _make_job(status=ClassificationJobStatus.COMPLETED)
        repo.create(job)

        resp = client.get(f"/classification-jobs/{job.id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "completed"
        assert data["classification"] == "scenic"
        assert data["confidence"] == 0.92
        assert data["error_message"] == ""

    def test_get_failed_job(self, client, repo):
        """获取失败的任务应包含错误信息。"""
        job = _make_job(status=ClassificationJobStatus.FAILED)
        repo.create(job)

        resp = client.get(f"/classification-jobs/{job.id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "failed"
        assert "AI 服务不可用" in data["error_message"]

    def test_get_nonexistent_job_returns_404(self, client):
        """获取不存在的任务应返回 404。"""
        resp = client.get("/classification-jobs/nonexistent-job-id")
        assert resp.status_code == 404

    def test_response_contains_all_required_fields(self, client, repo):
        """响应应包含所有必需字段。"""
        job = _make_job()
        repo.create(job)

        resp = client.get(f"/classification-jobs/{job.id}")
        data = resp.json()
        for field in ["id", "project_id", "asset_id", "status", "classification", "confidence", "error_message"]:
            assert field in data, f"缺少字段: {field}"


# ---------------------------------------------------------------------------
# 5. 跨端点场景
# ---------------------------------------------------------------------------


class TestClassificationApiScenarios:
    """分类任务 API 跨端点集成场景。"""

    def test_submit_then_get_pending(self, client, repo):
        """提交任务后立即查询应为 pending 状态。"""
        submit_resp = client.post(
            "/classification-jobs",
            json={"project_id": "proj-scenario", "asset_id": "asset-scenario"},
        )
        assert submit_resp.status_code == 200
        job_id = submit_resp.json()["id"]

        get_resp = client.get(f"/classification-jobs/{job_id}")
        assert get_resp.status_code == 200
        assert get_resp.json()["status"] == "pending"
        assert get_resp.json()["project_id"] == "proj-scenario"
        assert get_resp.json()["asset_id"] == "asset-scenario"

    def test_submit_simulate_complete_then_get(self, client, repo):
        """模拟 worker 完成任务后查询应返回结果。"""
        submit_resp = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a1"})
        job_id = submit_resp.json()["id"]

        # 模拟 worker 处理完成
        job = repo.get(job_id)
        assert job is not None
        job.status = ClassificationJobStatus.COMPLETED
        job.classification = "product"
        job.confidence = 0.88
        repo.update(job)

        # 查询结果
        get_resp = client.get(f"/classification-jobs/{job_id}")
        assert get_resp.status_code == 200
        data = get_resp.json()
        assert data["status"] == "completed"
        assert data["classification"] == "product"
        assert data["confidence"] == 0.88

    def test_submit_simulate_failure_then_get(self, client, repo):
        """模拟 worker 失败后查询应返回错误信息。"""
        submit_resp = client.post("/classification-jobs", json={"project_id": "p1", "asset_id": "a1"})
        job_id = submit_resp.json()["id"]

        # 模拟处理失败
        job = repo.get(job_id)
        assert job is not None
        job.status = ClassificationJobStatus.FAILED
        job.error_message = "网络超时"
        repo.update(job)

        get_resp = client.get(f"/classification-jobs/{job_id}")
        assert get_resp.status_code == 200
        data = get_resp.json()
        assert data["status"] == "failed"
        assert "网络超时" in data["error_message"]


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
