"""Headless-тесты TUI (Textual pilot).""" from __future__ import annotations import datetime as dt from decimal import Decimal from textual.widgets import DataTable, Input, Static from selectel_ml_tui.config import Credentials, Settings from selectel_ml_tui.models import ( ApiKeyUsage, BudgetInfo, Consumption, HistoryPoint, Metrics, ModelMetrics, TokenUsage, ) from selectel_ml_tui.services import DashboardData from selectel_ml_tui.storage import Storage from selectel_ml_tui.ui.app import SelectelMLTUI class FakeService: """Сервис-заглушка: возвращает готовые данные.""" def __init__(self, data: DashboardData) -> None: self.data = data self.refresh_count = 0 async def refresh(self) -> DashboardData: self.refresh_count += 1 return self.data def _dashboard_data() -> DashboardData: return DashboardData( month_consumption=Consumption.model_validate( { "period": "2026-08-01T00:00:00", "amount_rub": "854.83", "requests": 1034, "tokens_total": 451607518, "input_tokens": 100, "output_tokens": 200, "cache_read_tokens": 30, "cache_write_tokens": 40, } ), metrics=Metrics.model_validate( { "availability_pct": 99.9, "avg_ttft_ms": 0.4, "avg_latency_ms": 1.2, "requests": 1035, "input_tokens": 5, } ), models=[ ModelMetrics.model_validate( { "model_id": "gpt-4o", "model_name": "GPT-4o", "requests": 69, "availability_pct": 100.0, "avg_ttft_ms": 3.35, } ) ], keys=[ ApiKeyUsage( name="Mei", prefix="sk-sl-v1-9dd61ec", is_active=True, spend_limit_rub=Decimal("1300"), spend_rub=Decimal("1082.95"), remaining_rub=Decimal("217.05"), ) ], budget=BudgetInfo( spend_limit_rub=Decimal("2000"), spend_rub=Decimal("1077.87"), remaining_rub=Decimal("922.13"), ), ) def _make_app(tmp_path, data: DashboardData | None = None) -> SelectelMLTUI: settings = Settings(refresh_interval=3600) creds = Credentials(static_token="tok") storage = Storage(tmp_path / "cache.db") service = FakeService(data or _dashboard_data()) return SelectelMLTUI( settings=settings, credentials=creds, storage=storage, service=service, ) async def test_dashboard_renders_data(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.pause() month = app.screen.query_one("#month-panel", Static) rendered = str(month.render()) assert "854,83" in rendered assert "Потребление за месяц" in rendered metrics = app.screen.query_one("#metrics-panel", Static) assert "99,9%" in str(metrics.render()) table = app.screen.query_one("#models-table", DataTable) assert table.row_count == 1 assert "GPT-4o" in table.get_row_at(0) async def test_refresh_hotkey_reruns_service(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.pause() service = app._service assert service.refresh_count == 1 await pilot.press("r") await pilot.pause() assert service.refresh_count == 2 async def test_navigate_to_history_and_settings(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.pause() # history await pilot.press("h") await pilot.pause() assert app.screen.__class__.__name__ == "HistoryScreen" # назад await pilot.press("escape") await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen" # settings await pilot.press("s") await pilot.pause() assert app.screen.__class__.__name__ == "SettingsScreen" info = app.screen.query_one("#settings-info", Static).render() assert "API base URL" in str(info) assert "tok" in str(info) await pilot.press("escape") await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen" async def test_history_screen_shows_points(tmp_path) -> None: storage = Storage(tmp_path / "cache.db") storage.save_history( HistoryPoint( timestamp=dt.datetime(2026, 8, 18, 0, 0), amount_rub=Decimal("854.83"), tokens=TokenUsage(input_tokens=100), ) ) app = _make_app(tmp_path) app._storage = storage async with app.run_test() as pilot: await pilot.press("h") await pilot.pause() table = app.screen.query_one("#history-table", DataTable) assert table.row_count == 1 cells = [str(cell) for cell in table.get_row_at(0)] assert any("854,83" in cell for cell in cells) bars = app.screen.query_one("#history-bars") assert bars.render() != "Нет данных для графика" async def test_history_period_switch(tmp_path) -> None: storage = Storage(tmp_path / "cache.db") for day in range(20): storage.save_history( HistoryPoint( timestamp=dt.datetime(2026, 8, day + 1, 0, 0), amount_rub=Decimal(str(day + 1)), tokens=TokenUsage(input_tokens=day + 1), ) ) app = _make_app(tmp_path) app._storage = storage async with app.run_test() as pilot: await pilot.press("h") await pilot.pause() title = app.screen.query_one("#history-title", Static) assert "30 дней" in str(title.render()) table = app.screen.query_one("#history-table", DataTable) rows_30 = table.row_count await pilot.click("#period-7") await pilot.pause() title = app.screen.query_one("#history-title", Static) assert "7 дней" in str(title.render()) table = app.screen.query_one("#history-table", DataTable) assert table.row_count < rows_30 await pilot.click("#period-90") await pilot.pause() assert "90 дней" in str(app.screen.query_one("#history-title", Static).render()) assert app.screen.query_one("#history-table", DataTable).row_count >= rows_30 async def test_model_detail_on_enter(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.pause() await pilot.press("enter") await pilot.pause() assert app.screen.__class__.__name__ == "ModelDetailScreen" detail = app.screen.query_one("#model-detail", Static) rendered = str(detail.render()) assert "GPT-4o" in rendered assert "Запросы" in rendered await pilot.press("escape") await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen" async def test_settings_save_interval(tmp_path, monkeypatch) -> None: saved = {} def fake_save(settings) -> None: saved["interval"] = settings.refresh_interval monkeypatch.setattr("selectel_ml_tui.ui.app.save_settings", fake_save) app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.press("s") await pilot.pause() inp = app.screen.query_one("#interval-input", Input) inp.value = "120" await pilot.click("#save-settings") await pilot.pause() assert app._settings.refresh_interval == 120 assert saved["interval"] == 120 async def test_settings_clear_cache(tmp_path) -> None: storage = Storage(tmp_path / "cache.db") storage.save_history( HistoryPoint( timestamp=dt.datetime(2026, 8, 18, 0, 0), amount_rub=Decimal("854.83"), tokens=TokenUsage(input_tokens=100), ) ) app = _make_app(tmp_path) app._storage = storage async with app.run_test() as pilot: await pilot.press("s") await pilot.pause() await pilot.click("#clear-cache") await pilot.pause() assert storage.get_history(days=30) == [] async def test_offline_status_in_subtitle(tmp_path) -> None: data = _dashboard_data() data.offline = True app = _make_app(tmp_path, data) async with app.run_test() as pilot: await pilot.pause() assert "offline" in app.sub_title async def test_window_resize(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen" await pilot.resize_terminal(80, 24) await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen" month = app.screen.query_one("#month-panel", Static) assert "854,83" in str(month.render()) async def test_keys_screen(tmp_path) -> None: app = _make_app(tmp_path) async with app.run_test() as pilot: await pilot.pause() await pilot.press("k") await pilot.pause() assert app.screen.__class__.__name__ == "KeysScreen" table = app.screen.query_one("#keys-table", DataTable) assert table.row_count == 1 cells = [str(cell) for cell in table.get_row_at(0)] assert any("Mei" in cell for cell in cells) assert any("1 300" in cell for cell in cells) budget = app.screen.query_one("#keys-budget", Static) assert "Бюджет роутера" in str(budget.render()) await pilot.press("escape") await pilot.pause() assert app.screen.__class__.__name__ == "DashboardScreen"