#!/usr/bin/env bash
# Offline tests for the "api" adapter (scripts/ha/lib/import_api.py, ha_api.py,
# ha_ws.py): normalization of real-shaped HA API responses saved as fixtures,
# idempotency, stale-file cleanup, and the "websocket-client missing" error
# path. No network access, no HA instance needed — REST calls are replaced
# with a fake in-memory client, and the WebSocket client is replaced via
# sys.modules injection (see PYEOF below).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HA_LIB_DIR="$SCRIPT_DIR/../lib"
FIXTURES_DIR="$SCRIPT_DIR/fixtures"

python3 - "$HA_LIB_DIR" "$FIXTURES_DIR" <<'PYEOF'
import json
import os
import sys
import tempfile

lib_dir, fixtures_dir = sys.argv[1:3]
sys.path.insert(0, lib_dir)

import import_api  # noqa: E402
from normalize import parse_yaml_text  # noqa: E402

fail = False


def check(condition, message):
    global fail
    if condition:
        print(f"PASS: {message}")
    else:
        print(f"FAIL: {message}", file=sys.stderr)
        fail = True


def load_fixture(name):
    with open(os.path.join(fixtures_dir, name), encoding="utf-8") as f:
        return json.load(f)


class FakeClient:
    """Stands in for ha_api.Client: canned responses keyed by exact path."""

    def __init__(self, responses):
        self._responses = responses

    def get(self, path):
        return self._responses.get(path)


# --- automations / scripts / scenes: REST-shaped fixtures, no network ---

states = load_fixture("api_states.json")
responses = {
    "/api/config/automation/config/111": load_fixture("api_automation_config_111.json"),
    "/api/config/script/config/demo_script": load_fixture("api_script_config_demo_script.json"),
    "/api/config/scene/config/222": load_fixture("api_scene_config_222.json"),
}
client = FakeClient(responses)

with tempfile.TemporaryDirectory() as config_out_dir:
    report = import_api.new_report()
    import_api.import_automations(client, states, config_out_dir, report)
    import_api.import_scripts(client, states, config_out_dir, report)
    import_api.import_scenes(client, states, config_out_dir, report)

    check(report["automations_imported"] == 1, "one automation imported (the one with attributes.id)")
    check(
        report["automations_skipped_yaml"] == ["automation.legacy_yaml_automation"],
        "id-less automation entity reported as yaml-skipped, not silently dropped",
    )
    check(report["scripts_imported"] == 1, "one script imported")
    check(report["scenes_imported"] == 1, "one scene imported")

    auto_path = os.path.join(config_out_dir, "automations", "111.yaml")
    script_path = os.path.join(config_out_dir, "scripts", "demo_script.yaml")
    scene_path = os.path.join(config_out_dir, "scenes", "222.yaml")
    check(os.path.isfile(auto_path), "automations/111.yaml written")
    check(os.path.isfile(script_path), "scripts/demo_script.yaml written")
    check(os.path.isfile(scene_path), "scenes/222.yaml written")

    with open(auto_path, encoding="utf-8") as f:
        auto_text = f.read()
    check("alias: Morning lights" in auto_text, "automation config normalized to canonical (sorted-key) YAML")
    parsed_back = parse_yaml_text(auto_text)
    check(
        parsed_back == load_fixture("api_automation_config_111.json"),
        "normalized automation YAML round-trips to the original API response",
    )

    # stale-file cleanup: pre-seed an id that no longer exists, re-run, must be gone
    stale_path = os.path.join(config_out_dir, "automations", "999.yaml")
    with open(stale_path, "w", encoding="utf-8") as f:
        f.write("alias: stale\n")
    report2 = import_api.new_report()
    import_api.import_automations(client, states, config_out_dir, report2)
    check(not os.path.isfile(stale_path), "stale automation export removed on re-run (idempotent cleanup)")

    # idempotency: second run over the same fixture data is byte-identical
    with open(auto_path, encoding="utf-8") as f:
        pass1 = f.read()
    import_api.import_automations(client, states, config_out_dir, report2)
    with open(auto_path, encoding="utf-8") as f:
        pass2 = f.read()
    check(pass1 == pass2, "automations/111.yaml byte-identical across two runs (idempotent)")


# --- dashboards / registries / helpers: WebSocket-shaped fixtures ---

FAKE_DASHBOARD_LIST = [
    {"id": "map", "title": "Map", "url_path": "map", "mode": "storage"},
    {"id": "kiosk", "title": "Kiosk", "url_path": "kiosk", "mode": "yaml"},
]
FAKE_WS_RESULTS = {
    ("lovelace/config", ()): {"title": "Overview", "views": []},
    ("lovelace/config", (("url_path", "map"),)): {"title": "Map", "views": []},
    ("lovelace/dashboards/list", ()): FAKE_DASHBOARD_LIST,
    ("lovelace/resources", ()): [{"id": "r1", "type": "module", "url": "/local/x.js"}],
    ("config/area_registry/list", ()): [{"id": "hall", "name": "Hall"}],
    ("config/entity_registry/list", ()): [{"entity_id": "light.living_room"}],
    ("input_boolean/list", ()): [{"id": "on_leave", "name": "On leave"}],
}


class FakeHaWsError(RuntimeError):
    pass


class FakeHaWsClient:
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.token = token

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        return False

    def command(self, command_type, **kwargs):
        key = (command_type, tuple(sorted(kwargs.items())))
        if key not in FAKE_WS_RESULTS:
            raise FakeHaWsError(f"unexpected command in test double: {key!r}")
        return FAKE_WS_RESULTS[key]


fake_ha_ws = type(sys)("ha_ws")
fake_ha_ws.HaWsClient = FakeHaWsClient
fake_ha_ws.HaWsError = FakeHaWsError

with tempfile.TemporaryDirectory() as storage_out_dir:
    sys.modules["ha_ws"] = fake_ha_ws
    report = import_api.new_report()
    import_api.import_dashboards_and_registries(
        "http://ha.example", "test-token", states, storage_out_dir, report
    )
    del sys.modules["ha_ws"]

    check(report["websocket_error"] is None, "no websocket error with the fake client installed")
    check(report["dashboards_imported"] == 2, "default dashboard + one storage-mode dashboard imported")
    check(
        report["dashboards_skipped"] == [("kiosk", "mode='yaml' is file-based, not reachable via api")],
        "yaml-mode dashboard reported as skipped, not silently dropped",
    )
    check(report["helpers_imported"] == {"input_boolean": 1}, "only the present input_* domain was queried")

    expected_files = {
        "lovelace.yaml",
        "lovelace.map.yaml",
        "lovelace_dashboards.yaml",
        "lovelace_resources.yaml",
        "core.area_registry.yaml",
        "core.entity_registry.yaml",
        "input_boolean.yaml",
    }
    actual_files = set(os.listdir(storage_out_dir))
    check(actual_files == expected_files, f"expected storage-export files present, got {sorted(actual_files)}")

    # stale-file cleanup: a leftover domain export must be removed on re-run
    stale_path = os.path.join(storage_out_dir, "input_number.yaml")
    with open(stale_path, "w", encoding="utf-8") as f:
        f.write("key: input_number\n")
    sys.modules["ha_ws"] = fake_ha_ws
    report2 = import_api.new_report()
    import_api.import_dashboards_and_registries(
        "http://ha.example", "test-token", states, storage_out_dir, report2
    )
    del sys.modules["ha_ws"]
    check(not os.path.isfile(stale_path), "stale storage-export file removed on re-run (idempotent cleanup)")


# --- missing dependency path: no traceback, clear actionable message ---

with tempfile.TemporaryDirectory() as storage_out_dir:
    # Force the *real* ha_ws.py to execute (not a fake) and hit its own
    # `import websocket as _ws` line, so we exercise the actual actionable
    # ImportError message it raises rather than Python's generic one.
    sys.modules["websocket"] = None
    sys.modules.pop("ha_ws", None)
    report = import_api.new_report()
    import_api.import_dashboards_and_registries(
        "http://ha.example", "test-token", states, storage_out_dir, report
    )
    sys.modules.pop("ha_ws", None)
    del sys.modules["websocket"]

    check(report["websocket_error"] is not None, "missing websocket-client dependency is reported, not raised")
    check(
        "websocket-client" in (report["websocket_error"] or ""),
        "the reported error names the missing package so an operator knows what to install",
    )
    check(os.listdir(storage_out_dir) == [], "no storage-export files written when the dependency is missing")

sys.exit(1 if fail else 0)
PYEOF
