Newer
Older
gnexus-tasks / backend / tests / test_predict.py
"""Тесты M4: бюджет/оценка затрат, режим «3 варианта», глобальные настройки."""

from typing import Any

from fastapi.testclient import TestClient

from app.services.options import pick_options
from tests.conftest import _test_session_factory


def test_session() -> Any:
    return _test_session_factory()


def test_budget_and_actual_fields_manual(client: TestClient) -> None:
    tid = client.post("/api/tasks", json={"title": "Задача с бюджетом"}).json()["id"]
    updated = client.patch(
        f"/api/tasks/{tid}",
        json={"budget_money": 5000, "cost_estimate_money": 3500, "actual_minutes": 90},
    ).json()
    assert updated["budget_money"] == 5000
    assert updated["cost_estimate_money"] == 3500
    assert updated["actual_minutes"] == 90


def test_pick_options_fit_time_and_diversity(client: TestClient) -> None:
    p1 = client.post("/api/projects", json={"name": "Дом"}).json()["id"]
    p2 = client.post("/api/projects", json={"name": "Работа"}).json()["id"]

    def approve(title: str, minutes: int | None, project_id: int | None) -> None:
        tid = client.post("/api/tasks", json={"title": title}).json()["id"]
        patch: dict[str, Any] = {}
        if minutes is not None:
            patch["estimated_minutes"] = minutes
        if project_id is not None:
            patch["project_id"] = project_id
        client.patch(f"/api/tasks/{tid}", json=patch)
        client.post(f"/api/tasks/{tid}/approve")

    approve("Мелочь 30м", 30, p1)
    approve("Средняя 60м", 60, p2)
    approve("Большая 300м", 300, None)
    approve("Чуть больше 135м", 135, None)  # ровно верхняя граница окна 90±50%
    approve("Без оценки", None, p1)
    # приостановленный проект исключается
    paused = client.post("/api/projects", json={"name": "Заморожено"}).json()["id"]
    client.patch(f"/api/projects/{paused}", json={"relevance_status": "paused"})
    approve("Из замороженного", 15, paused)

    options = pick_options(test_session(), 90)
    titles = [t.title for t in options]
    assert len(options) <= 3
    assert "Мелочь 30м" in titles  # короче доступного времени — всегда подходит
    assert "Средняя 60м" in titles
    assert "Чуть больше 135м" in titles  # верхняя граница ±50% допускается
    assert "Большая 300м" not in titles  # сильно выходит за окно 90 минут
    assert "Из замороженного" not in titles  # проект неактивен
    # разнообразие: не более одной задачи на проект
    projects_used = [t.project_id for t in options]
    assert len(projects_used) == len(set(projects_used))


def test_pick_options_no_estimate_fallback(client: TestClient) -> None:
    def approve(title: str, minutes: int | None) -> None:
        tid = client.post("/api/tasks", json={"title": title}).json()["id"]
        if minutes is not None:
            client.patch(f"/api/tasks/{tid}", json={"estimated_minutes": minutes})
        client.post(f"/api/tasks/{tid}/approve")

    approve("Не влезает 500м", 500)
    approve("Без оценки", None)

    options = pick_options(test_session(), 60)
    titles = [t.title for t in options]
    assert "Без оценки" in titles  # без оценки — учитываем как фолбэк
    assert "Не влезает 500м" not in titles


def test_pick_options_skips_done_and_cancelled(client: TestClient) -> None:
    tid = client.post("/api/tasks", json={"title": "Завершённая"}).json()["id"]
    client.patch(f"/api/tasks/{tid}", json={"status": "done"})
    client.post(f"/api/tasks/{tid}/approve")

    options = pick_options(test_session(), 600)
    assert all(t.id != tid for t in options)


def test_suggest_endpoint(client: TestClient) -> None:
    resp = client.post("/api/tasks/suggest", json={"available_minutes": 120})
    assert resp.status_code == 200
    assert len(resp.json()) <= 3
    # валидация времени
    assert client.post("/api/tasks/suggest", json={"available_minutes": 0}).status_code == 422


def test_proposal_estimate_applied(client: TestClient, monkeypatch: Any) -> None:
    def fake_propose(
        self: Any, title: str, description: str, tag_names: list[str], project_names: list[str]
    ) -> dict[str, Any]:
        return {
            "tags": [],
            "project": None,
            "new_project": False,
            "priority": 7,
            "estimated_minutes": 45,
        }

    monkeypatch.setattr("app.services.detailing.DetailingService.propose", fake_propose)
    tid = client.post("/api/tasks", json={"title": "Что-то на 45 минут"}).json()["id"]
    approved = client.post(f"/api/tasks/{tid}/approve", json={"apply_proposal": True}).json()
    assert approved["estimated_minutes"] == 45


def test_global_currency_settings(client: TestClient) -> None:
    # смена валюты частичным PUT
    assert client.put("/api/settings", json={"currency": "EUR"}).json()["currency"] == "EUR"
    assert client.get("/api/settings").json()["currency"] == "EUR"
    # недопустимая валюта — 422
    assert client.put("/api/settings", json={"currency": "RUB"}).status_code == 422