from __future__ import annotations from datetime import UTC, datetime, timedelta from uuid import uuid4 from fastapi.testclient import TestClient from app.api.tasks import get_task_repository from app.main import app from app.storage import JsonFileTaskRepository, StoredTask def make_repo(tmp_path) -> JsonFileTaskRepository: return JsonFileTaskRepository(tmp_path / "tasks.json") def make_task( *, title: str = "Task", started: bool = False, done: bool = False, ) -> StoredTask: now = datetime.now(UTC) started_at = now - timedelta(minutes=10) if started else None done_at = now - timedelta(minutes=1) if done else None return StoredTask( id=uuid4(), title=title, created_at=now - timedelta(hours=1), started_at=started_at, done_at=done_at, ) def make_client(tmp_path) -> tuple[TestClient, JsonFileTaskRepository]: repo = make_repo(tmp_path) app.dependency_overrides[get_task_repository] = lambda: repo client = TestClient(app) return client, repo def teardown_function() -> None: app.dependency_overrides.clear() def test_stats_returns_html_page_for_empty_board(tmp_path): client, _repo = make_client(tmp_path) response = client.get("/stats") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] assert "TaskFlow Stats" in response.text assert "No task selected" in response.text assert "Backlog" in response.text assert "In Progress" in response.text assert "Done" in response.text assert response.text.count("No tasks") == 3 def test_stats_renders_tasks_in_all_kanban_columns(tmp_path): client, repo = make_client(tmp_path) backlog = make_task(title="Backlog title", started=False, done=False) in_progress = make_task(title="In progress title", started=True, done=False) done = make_task(title="Done title", started=True, done=True) repo.create_task(backlog) repo.create_task(in_progress) repo.create_task(done) response = client.get("/stats") assert response.status_code == 200 assert "Backlog title" in response.text assert "In progress title" in response.text assert "Done title" in response.text assert "Backlog" in response.text assert "In Progress" in response.text assert "Done" in response.text def test_stats_preselects_first_task_in_details_block(tmp_path): client, repo = make_client(tmp_path) first = make_task(title="First task", started=False, done=False) second = make_task(title="Second task", started=True, done=False) repo.create_task(first) repo.create_task(second) response = client.get("/stats") assert response.status_code == 200 assert 'id="selected-title"' in response.text assert 'id="selected-created-at"' in response.text assert "First task" in response.text assert first.created_at.isoformat() in response.text def test_stats_shows_placeholders_for_missing_dates(tmp_path): client, repo = make_client(tmp_path) task = make_task(title="Backlog only", started=False, done=False) repo.create_task(task) response = client.get("/stats") assert response.status_code == 200 assert 'id="selected-started-at"' in response.text assert 'id="selected-done-at"' in response.text assert 'id="selected-cycle-time"' in response.text assert ">—<" in response.text def test_stats_renders_cycle_time_for_completed_task(tmp_path): client, repo = make_client(tmp_path) task = make_task(title="Completed task", started=True, done=True) repo.create_task(task) response = client.get("/stats") assert response.status_code == 200 assert 'id="selected-cycle-time"' in response.text assert "0h 9m" in response.text assert ">—<" not in response.text.split('id="selected-cycle-time"', 1)[1].split("", 2)[1] def test_stats_embeds_task_payload_for_client_side_switching(tmp_path): client, repo = make_client(tmp_path) task = make_task(title="Payload task", started=True, done=True) repo.create_task(task) response = client.get("/stats") assert response.status_code == 200 assert "data-task='" in response.text assert str(task.id) in response.text assert '"title": "Payload task"' in response.text assert '"status": "done"' in response.text assert '"created_at":' in response.text assert '"started_at":' in response.text assert '"done_at":' in response.text assert '"cycle_time":' in response.text def test_stats_includes_js_hooks_for_dynamic_selected_task_update(tmp_path): client, repo = make_client(tmp_path) repo.create_task(make_task(title="Clickable")) response = client.get("/stats") assert response.status_code == 200 assert 'id="selected-title"' in response.text assert 'id="selected-status"' in response.text assert 'id="selected-started-at"' in response.text assert 'id="selected-done-at"' in response.text assert 'id="selected-cycle-time"' in response.text assert 'id="selected-created-at"' in response.text assert 'document.querySelectorAll(".task-card")' in response.text assert 'card.addEventListener("click"' in response.text assert "JSON.parse(card.dataset.task)" in response.text def test_stats_handles_utf8_titles(tmp_path): client, repo = make_client(tmp_path) repo.create_task(make_task(title="Задача пример")) response = client.get("/stats") assert response.status_code == 200 assert "Задача пример" in response.text def test_stats_escapes_html_in_task_title(tmp_path): client, repo = make_client(tmp_path) repo.create_task(make_task(title="")) response = client.get("/stats") assert response.status_code == 200 assert "" not in response.text assert "<script>alert(1)</script>" in response.text def test_stats_rejects_unsupported_method(tmp_path): client, _repo = make_client(tmp_path) response = client.post("/stats") assert response.status_code == 405 def test_stats_still_works_after_recovery_from_corrupted_file(tmp_path): file_path = tmp_path / "tasks.json" file_path.write_text("{broken", encoding="utf-8") repo = JsonFileTaskRepository(file_path) app.dependency_overrides[get_task_repository] = lambda: repo client = TestClient(app) response = client.get("/stats") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] assert "TaskFlow Stats" in response.text assert "No task selected" in response.text def test_stats_renders_created_started_and_done_values_for_completed_task(tmp_path): client, repo = make_client(tmp_path) task = make_task(title="Detailed task", started=True, done=True) repo.create_task(task) response = client.get("/stats") assert response.status_code == 200 assert task.created_at.isoformat() in response.text assert task.started_at.isoformat() in response.text assert task.done_at.isoformat() in response.text