from pathlib import Path from types import SimpleNamespace import pytest from hockey_data.auth_bridge import HockeyUser from hockey_data.mapping_context import MappingDataService from hockey_data.models import Game, Player, PlayerTournament, Tournament from tests.support import LocalTestDatabase def _seed(database: LocalTestDatabase) -> None: with database.session() as session: session.add(Tournament(external_id="1437", level="khl", name_ru="Предсезонные матчи", season="2026/27")) session.add(Game( external_id="902918", tournament_external_id="1437", home_team_external_id="23", away_team_external_id="17", home_team_name_ru="СКА", home_team_short_ru="СКА", home_team_city_ru="Санкт-Петербург", home_score=2, away_team_name_ru="Динамо М", away_team_short_ru="ДИН", away_team_city_ru="Москва", away_score=1, status="live", arena_ru="Ледовый дворец", start_time="19:30", )) player = Player(external_id="18451", first_name_ru="Иван", last_name_ru="Иванов", full_name_ru="Иванов Иван", position_ru="Нападающий", photo_url="ivanov.png") session.add(player); session.flush() session.add(PlayerTournament(player_id=player.id, tournament_external_id="1437", club_external_id="23", team_name_ru="СКА", jersey_number="17")) def test_context_variables_and_sql_catalog_are_dynamic(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-context.sqlite3") database.create_all(); _seed(database) service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") variables = service.list_variables()["variables"] assert any(row["key"] == "game_id" and row["is_system"] for row in variables) assert any(row["key"] == "highlighted_game_id" for row in variables) custom = service.create_variable(SimpleNamespace( key="my_special_player_id", label="Мой выделенный игрок", category="Пользовательские", description="", value_type="id", entity_type="player", scope="match", source_type="manual", default_value="", enabled=True, sort_order=1200, ), admin) assert custom["key"] == "my_special_player_id" set_value = service.set_context_value("selected_player_id", "18451", admin, {"game_id": "902918", "session_token": "S1"}) assert set_value["value"] == "18451" catalog = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1"}) values = {item["key"]: item["value"] for item in catalog["items"]} assert values["game.home.name"] == "СКА" assert values["game.away.score"] == 1 assert values["selected_player.full_name"] == "Иванов Иван" assert catalog["context"]["team1_id"] == "23" assert catalog["context"]["team2_id"] == "17" source = service.create_source(SimpleNamespace( code="custom_demo", name="Свои данные", category="Тест", description="", sql_text='SELECT :game_id AS "game_ref", :my_special_player_id AS "special_player"', field_metadata={"game_ref": {"label": "Матч"}, "special_player": {"label": "Игрок"}}, enabled=True, sort_order=1500, ), admin) assert source["code"] == "custom_demo" service.set_context_value("my_special_player_id", "18451", admin, {"game_id": "902918", "session_token": "S1"}) catalog2 = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1"}) values2 = {item["key"]: item["value"] for item in catalog2["items"]} assert values2["custom_demo.game_ref"] == "902918" assert values2["custom_demo.special_player"] == "18451" def test_sql_sources_reject_writes(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-context-danger.sqlite3") database.create_all() service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") with pytest.raises(Exception): service.create_source(SimpleNamespace( code="bad", name="bad", category="", description="", sql_text="DELETE FROM hockey_games", field_metadata={}, enabled=True, sort_order=1, ), admin) def test_sql_table_source_exposes_every_cell_without_changing_query(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-table.sqlite3") database.create_all(); _seed(database) service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") service.create_source(SimpleNamespace( code="score_rows", name="Таблица строк", category="Таблица", description="", sql_text='SELECT 1 AS "place", \'СКА\' AS "team", 10 AS "points" UNION ALL SELECT 2, \'Динамо\', 8 UNION ALL SELECT 3, \'Локомотив\', 7', field_metadata={"place": {"label": "Место"}, "team": {"label": "Команда"}, "points": {"label": "Очки"}}, enabled=True, sort_order=1600, ), admin) catalog = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1"}) values = {item["key"]: item["value"] for item in catalog["items"]} # Backwards-compatible source.column means the first row. assert values["score_rows.team"] == "СКА" # New addressable cell keys let Mapping pick any row/column from one SQL table. assert values["score_rows.row.2.team"] == "Динамо" assert values["score_rows.row.3.points"] == 7 table = next(item for item in catalog["tables"] if item["code"] == "score_rows") assert table["row_count"] == 3 assert [column["key"] for column in table["columns"]] == ["place", "team", "points"] assert table["rows"][1]["values"]["team"] == "Динамо" def test_language_suffix_pairs_create_virtual_auto_key(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-language.sqlite3") database.create_all(); _seed(database) service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") service.create_source(SimpleNamespace( code="bilingual", name="Два языка", category="Тест", description="", sql_text='SELECT \'17 августа 2026\' AS "date_RUS", \'August 17, 2026\' AS "date_ENG", \'СКА\' AS "team_RUS", \'SKA\' AS "team_ENG"', field_metadata={}, enabled=True, sort_order=1700, ), admin) ru = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1", "ui_language": "ru"}) ru_values = {item["key"]: item["value"] for item in ru["items"]} assert ru_values["bilingual.date"] == "17 августа 2026" assert ru_values["bilingual.team"] == "СКА" ru_item = next(item for item in ru["items"] if item["key"] == "bilingual.date") assert ru_item["localized"] is True assert ru_item["resolved_column"] == "date_RUS" en = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1", "ui_language": "en"}) en_values = {item["key"]: item["value"] for item in en["items"]} assert en_values["bilingual.date"] == "August 17, 2026" assert en_values["bilingual.team"] == "SKA" en_table = next(item for item in en["tables"] if item["code"] == "bilingual") assert [column["key"] for column in en_table["columns"]] == ["date", "team"] assert all(column["localized"] for column in en_table["columns"]) def test_language_suffix_pairs_work_for_specific_table_rows(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-language-table.sqlite3") database.create_all(); _seed(database) service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") service.create_source(SimpleNamespace( code="clubs_lang", name="Клубы", category="Тест", description="", sql_text='SELECT 1 AS "id", \'СКА\' AS "name_RUS", \'SKA\' AS "name_ENG" UNION ALL SELECT 2, \'Динамо\', \'Dynamo\'', field_metadata={}, enabled=True, sort_order=1701, ), admin) catalog = service.data_catalog(admin, {"game_id": "902918", "session_token": "S1", "ui_language": "en"}) values = {item["key"]: item["value"] for item in catalog["items"]} assert values["clubs_lang.row.1.name"] == "SKA" assert values["clubs_lang.row.2.name"] == "Dynamo" def test_sql_allows_comments_and_one_trailing_semicolon_but_not_two_statements(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-sql-normalize.sqlite3") database.create_all(); _seed(database) service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") source = service.create_source(SimpleNamespace( code="commented", name="SQL comments", category="Тест", description="", sql_text='-- дата текущего матча\nSELECT :game_id AS "game_id" /* безопасный комментарий */;', field_metadata={}, enabled=True, sort_order=1800, ), admin) assert source["code"] == "commented" preview = service.preview_source(source["id"], admin, {"game_id": "902918"}) assert preview["rows"][0]["game_id"] == "902918" with pytest.raises(Exception): service.create_source(SimpleNamespace( code="two_queries", name="bad", category="", description="", sql_text='SELECT 1; SELECT 2', field_metadata={}, enabled=True, sort_order=1, ), admin) def test_sql_context_parser_ignores_postgresql_time_formats_and_casts(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-sql-params.sqlite3") database.create_all() service = MappingDataService(database) # type: ignore[arg-type] params = service.validate_sql( "SELECT TO_CHAR(NOW(), 'HH24:MI:SS') AS time_text, value::text AS value_text " "FROM hockey_games WHERE external_id = :game_id" ) assert params == {"game_id"} def test_sql_source_stores_auto_refresh_settings(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "mapping-sql-auto.sqlite3") database.create_all() service = MappingDataService(database) # type: ignore[arg-type] admin = HockeyUser(id="1", login="admin", display_name="Admin", is_admin=True, role="admin") source = service.create_source(SimpleNamespace( code="clock", name="Часы", category="Система", description="", sql_text="SELECT 1 AS clock_value", field_metadata={}, enabled=True, auto_refresh_enabled=True, refresh_interval_ms=1000, sort_order=10, ), admin) assert source["auto_refresh_enabled"] is True assert source["refresh_interval_ms"] == 1000 listed = next(row for row in service.list_sources()["sources"] if row["code"] == "clock") assert listed["auto_refresh_enabled"] is True assert listed["refresh_interval_ms"] == 1000