"""
Fáze 0 – golden replay gate plánovače (bez DB).

Pro každou fixture v tests/golden/fixtures/ (kompletní vstupy solveru zmrazené
z reálné DB skriptem scripts/harness/extract_fixtures.py) spustí
solve_dispatch_two_pass a porovná normalizovaný výstup s golden snapshotem
v tests/golden/snapshots/.

Účel: regresní brána pro dekompozici planning_engine.py — identity refactor
musí držet výstupy bit-perfektně (floaty zaokrouhleny na 4 d.m.).

Regenerace snapshotů (vědomá změna chování):
  GOLDEN_UPDATE=1 python3 -m pytest tests/test_golden_replay.py -q

Replay jde STEJNOU cestou jako produkce: _load_site_context + _load_slots nad
fixture stubem DB → žádná duplikace mapování DB → objekty.
"""

from __future__ import annotations

import asyncio
import json
import os
import unittest
from datetime import datetime
from pathlib import Path

from services import planning_engine as pe

GOLDEN_DIR = Path(__file__).resolve().parent / "golden"
FIXTURES_DIR = GOLDEN_DIR / "fixtures"
SNAPSHOTS_DIR = GOLDEN_DIR / "snapshots"

_DT_SLOT_KEYS = ("interval_start", "charge_acquisition_cutoff_at")


class _FixtureDB:
    """Stub asyncpg connection: vrací zmrazený context a sloty z fixture."""

    def __init__(self, fixture: dict):
        self._fixture = fixture

    async def fetchval(self, query: str, *args):
        assert "fn_planning_site_context" in query, f"Nečekaný fetchval: {query!r}"
        return json.dumps(self._fixture["context_json"])

    async def fetch(self, query: str, *args):
        assert "fn_load_planning_slots_full" in query, f"Nečekaný fetch: {query!r}"
        rows: list[dict] = []
        for raw in self._fixture["slot_rows"]:
            d = dict(raw)
            for key in _DT_SLOT_KEYS:
                if d.get(key):
                    d[key] = datetime.fromisoformat(d[key])
            rows.append(d)
        return rows


def _round(val: float, places: int = 4) -> float:
    out = round(float(val), places)
    return 0.0 if out == 0.0 else out  # normalizace -0.0


def _normalize_results(results: list) -> dict:
    rows = []
    for r in results:
        rows.append(
            {
                "interval_start": r.interval_start.isoformat(),
                "battery_setpoint_w": int(r.battery_setpoint_w),
                "battery_soc_target": _round(r.battery_soc_target, 2),
                "grid_setpoint_w": int(r.grid_setpoint_w),
                "export_limit_w": int(r.export_limit_w),
                "export_mode": r.export_mode,
                "deye_physical_mode": r.deye_physical_mode,
                "deye_gen_cutoff_enabled": r.deye_gen_cutoff_enabled,
                "ev1_setpoint_w": r.ev1_setpoint_w,
                "ev2_setpoint_w": r.ev2_setpoint_w,
                "ev1_via_bat_w": int(r.ev1_via_bat_w),
                "ev2_via_bat_w": int(r.ev2_via_bat_w),
                "heat_pump_enabled": bool(r.heat_pump_enabled),
                "heat_pump_setpoint_w": int(r.heat_pump_setpoint_w),
                "pv_a_curtailed_w": int(r.pv_a_curtailed_w),
                "expected_cost_czk": _round(r.expected_cost_czk),
                "cashflow_czk": _round(r.cashflow_czk),
                "battery_arbitrage_czk": _round(r.battery_arbitrage_czk),
                "penalty_czk": _round(r.penalty_czk),
                "green_bonus_czk": _round(r.green_bonus_czk),
            }
        )
    totals = {
        "slots": len(rows),
        "expected_cost_czk": _round(sum(r["expected_cost_czk"] for r in rows), 3),
        "cashflow_czk": _round(sum(r["cashflow_czk"] for r in rows), 3),
        "penalty_czk": _round(sum(r["penalty_czk"] for r in rows), 3),
        "grid_import_slots": sum(1 for r in rows if r["grid_setpoint_w"] > 0),
        "grid_export_slots": sum(1 for r in rows if r["grid_setpoint_w"] < 0),
        "curtail_slots": sum(1 for r in rows if r["pv_a_curtailed_w"] > 0),
    }
    return {"totals": totals, "slots": rows}


def _replay_fixture(fixture: dict) -> dict:
    async def _run() -> dict:
        db = _FixtureDB(fixture)
        meta = fixture["meta"]
        (
            battery,
            heat_pump,
            grid,
            vehicles,
            ev_sessions,
            soc_wh,
            tuv_temp,
            operating_mode,
            tuv_stats,
        ) = await pe._load_site_context(int(meta["site_id"]), db)
        slots = await pe._load_slots(
            int(meta["site_id"]),
            datetime.fromisoformat(meta["window_from"]),
            datetime.fromisoformat(meta["window_to"]),
            db,
            soc_wh=soc_wh,
        )
        try:
            results, _ms, _snap = pe.solve_dispatch_two_pass(
                slots,
                battery,
                heat_pump,
                grid,
                ev_sessions,
                vehicles,
                soc_wh,
                tuv_temp,
                tuv_delta_stats=tuv_stats,
                operating_mode=operating_mode or "AUTO",
                planner_version=pe._planner_engine_version(),
            )
        except pe.PlannerSolverError as exc:
            # Selhání solveru je taky chování k zafixování (např. home-01 2026-05-01:
            # Infeasible po celém relax řetězci). Až ho Fáze 2/3 opraví, golden diff
            # to zviditelní a snapshot se vědomě zregeneruje.
            return {
                "solver_error": exc.solver_status,
                "relax_chain": list(exc.relax_chain),
            }
        return _normalize_results(results)

    return asyncio.run(_run())


def _fixture_paths() -> list[Path]:
    return sorted(FIXTURES_DIR.glob("*.json"))


class GoldenReplayTests(unittest.TestCase):
    maxDiff = None

    def test_fixtures_exist(self) -> None:
        self.assertTrue(
            _fixture_paths(),
            f"Žádné fixtures v {FIXTURES_DIR} – spusť scripts/harness/extract_fixtures.py",
        )


def _make_test(path: Path):
    def test(self: GoldenReplayTests) -> None:
        fixture = json.loads(path.read_text(encoding="utf-8"))
        actual = _replay_fixture(fixture)
        snap_path = SNAPSHOTS_DIR / path.name
        if os.environ.get("GOLDEN_UPDATE") == "1":
            SNAPSHOTS_DIR.mkdir(parents=True, exist_ok=True)
            snap_path.write_text(
                json.dumps(actual, ensure_ascii=False, indent=1) + "\n", encoding="utf-8"
            )
            return
        self.assertTrue(
            snap_path.exists(),
            f"Chybí snapshot {snap_path.name} – vygeneruj přes GOLDEN_UPDATE=1",
        )
        expected = json.loads(snap_path.read_text(encoding="utf-8"))
        if "solver_error" in expected or "solver_error" in actual:
            self.assertEqual(expected, actual, f"{path.name}: změna výsledku/selhání solveru")
            return
        self.assertEqual(
            expected["totals"],
            actual["totals"],
            f"{path.name}: změna agregátů plánu (totals)",
        )
        self.assertEqual(
            expected["slots"],
            actual["slots"],
            f"{path.name}: změna plánu per slot",
        )

    return test


for _path in _fixture_paths():
    _name = "test_golden_" + _path.stem.replace("-", "_").replace(".", "_")
    setattr(GoldenReplayTests, _name, _make_test(_path))


if __name__ == "__main__":
    unittest.main()
