from __future__ import annotations

import asyncio
from pathlib import Path

from fastapi import HTTPException

from hockey_data.agent_bridge import MAPPING_BATCH_MAX_COMMANDS, VmixAgentHub
from tests.support import LocalTestDatabase


def _entry(index: int, input_ref: str, *, value_size: int = 4) -> dict:
    return {
        "data_key": f"source.row.{index}",
        "command": {
            "Function": "SetText",
            "Input": input_ref,
            "SelectedName": f"Field{index}.Text",
            "Value": "X" * value_size,
        },
    }


def test_mapping_chunk_planner_groups_by_input_and_caps_packets(tmp_path: Path) -> None:
    database = LocalTestDatabase(tmp_path / "mapping-chunks.sqlite3")
    database.create_all()
    hub = VmixAgentHub(database)  # type: ignore[arg-type]

    entries = [*[_entry(i, "INPUT-A") for i in range(95)], *[_entry(100 + i, "INPUT-B") for i in range(17)]]
    chunks = hub._mapping_batch_chunks(entries)

    assert len(chunks) == 4  # A: 40 + 40 + 15, B: 17
    assert all(len(chunk) <= MAPPING_BATCH_MAX_COMMANDS for chunk in chunks)
    assert all(len({item[1]["command"]["Input"] for item in chunk}) == 1 for chunk in chunks)
    # ACK positions can still be restored to original Mapping order.
    assert [index for chunk in chunks for index, _entry_item in chunk] == list(range(len(entries)))


def test_mapping_chunk_transport_sends_300_plus_links_in_small_batches(tmp_path: Path) -> None:
    database = LocalTestDatabase(tmp_path / "mapping-300.sqlite3")
    database.create_all()
    hub = VmixAgentHub(database)  # type: ignore[arg-type]
    packets: list[list[dict]] = []

    async def fake_batch(device_id: str, *, assignment_id: str, match_id: str, commands: list[dict], timeout: float = 8.0) -> dict:
        assert device_id == "GFX-MAP-300"
        packets.append(commands)
        return {"ok": True, "results": [{"ok": True} for _ in commands]}

    async def scenario() -> None:
        hub.send_vmix_batch = fake_batch  # type: ignore[method-assign]
        entries: list[dict] = []
        # 12 Inputs x 31 commands = 372 Mapping commands.
        for input_index in range(12):
            entries.extend(_entry(input_index * 1000 + row, f"INPUT-{input_index:02d}") for row in range(31))
        result = await hub._send_mapping_entries_resilient(
            "GFX-MAP-300",
            assignment_id="assignment-300",
            match_id="game-300",
            entries=entries,
            use_batch=True,
        )
        assert all(item["ok"] for item in result["results"])
        assert result["input_groups"] == 12
        assert result["chunks_total"] == 12
        assert result["chunks_ok"] == 12
        assert result["chunks_failed"] == 0
        assert result["fallback_commands"] == 0
        assert sum(len(packet) for packet in packets) == 372
        assert max(len(packet) for packet in packets) <= MAPPING_BATCH_MAX_COMMANDS
        assert all(len({command["Input"] for command in packet}) == 1 for packet in packets)

    asyncio.run(scenario())


def test_mapping_transport_splits_timeout_batch_and_keeps_going(tmp_path: Path) -> None:
    database = LocalTestDatabase(tmp_path / "mapping-retry.sqlite3")
    database.create_all()
    hub = VmixAgentHub(database)  # type: ignore[arg-type]
    packet_sizes: list[int] = []

    async def flaky_batch(device_id: str, *, assignment_id: str, match_id: str, commands: list[dict], timeout: float = 8.0) -> dict:
        packet_sizes.append(len(commands))
        if len(commands) > 10:
            raise HTTPException(status_code=504, detail="simulated batch timeout")
        return {"ok": True, "results": [{"ok": True} for _ in commands]}

    async def scenario() -> None:
        hub.send_vmix_batch = flaky_batch  # type: ignore[method-assign]
        entries = [_entry(i, "ONE-BIG-INPUT") for i in range(35)]
        result = await hub._send_mapping_entries_resilient(
            "GFX-MAP-RETRY",
            assignment_id="assignment-retry",
            match_id="game-retry",
            entries=entries,
            use_batch=True,
        )
        assert all(item["ok"] for item in result["results"])
        assert result["chunks_total"] == 1
        assert result["chunks_ok"] == 1
        assert result["chunks_failed"] == 0
        assert result["retries"] > 0
        assert packet_sizes[0] == 35
        assert any(size <= 10 for size in packet_sizes[1:])

    asyncio.run(scenario())


def test_mapping_transport_retries_isolated_failed_command(tmp_path: Path) -> None:
    database = LocalTestDatabase(tmp_path / "mapping-single-fallback.sqlite3")
    database.create_all()
    hub = VmixAgentHub(database)  # type: ignore[arg-type]
    singles: list[dict] = []

    async def partial_batch(device_id: str, *, assignment_id: str, match_id: str, commands: list[dict], timeout: float = 8.0) -> dict:
        results = [{"ok": True} for _ in commands]
        results[3] = {"ok": False, "reason": "temporary failure"}
        return {"ok": False, "results": results}

    async def single_ok(device_id: str, *, assignment_id: str, match_id: str, command: dict, timeout: float = 5.0) -> dict:
        singles.append(command)
        return {"ok": True}

    async def scenario() -> None:
        hub.send_vmix_batch = partial_batch  # type: ignore[method-assign]
        hub.send_vmix_command = single_ok  # type: ignore[method-assign]
        entries = [_entry(i, "INPUT-FALLBACK") for i in range(8)]
        result = await hub._send_mapping_entries_resilient(
            "GFX-MAP-FALLBACK",
            assignment_id="assignment-fallback",
            match_id="game-fallback",
            entries=entries,
            use_batch=True,
        )
        assert all(item["ok"] for item in result["results"])
        assert result["fallback_commands"] == 1
        assert result["retries"] == 1
        assert len(singles) == 1
        assert singles[0]["SelectedName"] == "Field3.Text"

    asyncio.run(scenario())


def test_build97_runtime_version_and_mapping_diagnostics() -> None:
    root = Path(__file__).resolve().parents[1]
    app = (root / "app.py").read_text(encoding="utf-8")
    bridge = (root / "hockey_data" / "agent_bridge.py").read_text(encoding="utf-8")
    admin = (root / "hockey_data" / "static" / "admin-directories.js").read_text(encoding="utf-8")

    assert 'BUILD_VERSION = "2026.08.21.1"' in app
    assert 'result["transport"] = "chunked_batch" if use_batch else "legacy"' in bridge
    assert 'MAPPING_BATCH_MAX_COMMANDS = 40' in bridge
    assert 'mappingApplyTransportSummary' in admin
    assert 'пакетов ${chunksApplied}/${chunksTotal}' in admin
