"""Tests for statistics router (/api/statistics).""" import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from models.user import User from models.measurement import Measurement from tests.conftest import auth_headers, create_test_recipe async def _seed_measurements( client: AsyncClient, tec_user: User, recipe_id: int, values: list[float], ) -> tuple[int, int]: """Create measurements and return (subtask_id, version_id).""" tasks_resp = await client.get( f"/api/recipes/{recipe_id}/tasks", headers=auth_headers(tec_user) ) task = tasks_resp.json()[0] subtask_id = task["subtasks"][0]["id"] version_id = task["version_id"] for val in values: await client.post( "/api/measurements/", headers=auth_headers(tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": val, }, ) return subtask_id, version_id class TestSummary: """GET /api/statistics/summary tests.""" async def test_get_statistics_by_recipe( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Metrologist can get summary statistics.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) await _seed_measurements( client, measurement_tec_user, recipe.id, [10.0, 10.1, 10.4, 10.6, 9.4], # pass, pass, warning, fail, fail ) resp = await client.get( "/api/statistics/summary", headers=auth_headers(metrologist_user), params={"recipe_id": recipe.id}, ) assert resp.status_code == 200 data = resp.json() assert data["total"] == 5 assert data["pass_count"] >= 0 assert data["warning_count"] >= 0 assert data["fail_count"] >= 0 assert data["pass_rate"] >= 0 # Total rates should sum to ~100 total_rate = data["pass_rate"] + data["warning_rate"] + data["fail_rate"] assert abs(total_rate - 100.0) < 0.1 async def test_statistics_require_metrologist( self, client: AsyncClient, maker_user: User, db_session: AsyncSession, ): """Non-Metrologist cannot access statistics.""" recipe = await create_test_recipe(db_session, maker_user.id) resp = await client.get( "/api/statistics/summary", headers=auth_headers(maker_user), params={"recipe_id": recipe.id}, ) assert resp.status_code == 403 async def test_statistics_empty( self, client: AsyncClient, metrologist_user: User, db_session: AsyncSession, ): """Statistics on recipe with no measurements returns zeros.""" recipe = await create_test_recipe( db_session, metrologist_user.id ) resp = await client.get( "/api/statistics/summary", headers=auth_headers(metrologist_user), params={"recipe_id": recipe.id}, ) assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert data["pass_count"] == 0 class TestCapability: """GET /api/statistics/capability tests.""" async def test_capability_indices( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Capability endpoint returns Cp/Cpk/Pp/Ppk indices.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, _ = await _seed_measurements( client, measurement_tec_user, recipe.id, [9.9, 10.0, 10.1, 9.8, 10.2, 10.0, 9.95, 10.05, 10.1, 9.9], ) resp = await client.get( "/api/statistics/capability", headers=auth_headers(metrologist_user), params={ "recipe_id": recipe.id, "subtask_id": subtask_id, }, ) assert resp.status_code == 200 data = resp.json() assert data["n"] == 10 assert data["mean"] is not None assert data["std_dev"] is not None # With UTL=10.5 and LTL=9.5, spread=1.0, Cp should be > 0 if data["cp"] is not None: assert data["cp"] > 0 class TestSPCData: """GET /api/statistics/control-chart tests.""" async def test_spc_data( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Control chart returns values, mean, UCL, LCL.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, _ = await _seed_measurements( client, measurement_tec_user, recipe.id, [10.0, 10.1, 9.9, 10.05, 9.95], ) resp = await client.get( "/api/statistics/control-chart", headers=auth_headers(metrologist_user), params={ "recipe_id": recipe.id, "subtask_id": subtask_id, }, ) assert resp.status_code == 200 data = resp.json() assert len(data["values"]) == 5 assert data["mean"] is not None assert data["ucl"] is not None assert data["lcl"] is not None assert data["ucl"] > data["mean"] assert data["lcl"] < data["mean"] class TestDateFilter: """Date filter tests.""" async def test_statistics_date_filter( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Date filter parameters are accepted without error.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) await _seed_measurements( client, measurement_tec_user, recipe.id, [10.0, 10.1], ) resp = await client.get( "/api/statistics/summary", headers=auth_headers(metrologist_user), params={ "recipe_id": recipe.id, "date_from": "2020-01-01T00:00:00", "date_to": "2099-12-31T23:59:59", }, ) assert resp.status_code == 200 # All measurements should be within this wide range assert resp.json()["total"] >= 2