#!/usr/bin/env bash
# Offline tests for the deploy write path (scripts/ha/lib/deploy_api.py,
# ha_write_api.py): clean deploy, drift-abort, validation rejecting broken
# YAML, and verify detecting a post-write mismatch. No network access, no
# HA instance needed -- REST calls are replaced with a fake in-memory
# client (get/post_config/check_config); "repo HEAD" is a real temporary
# git repo so git_show/git_ls_tracked exercise the actual git plumbing.
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 subprocess
import sys
import tempfile

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

import deploy_api  # noqa: E402
from normalize import dump_canonical_yaml  # 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)


STATES = load_fixture("api_states.json")
AUTOMATION_111 = load_fixture("api_automation_config_111.json")
SCRIPT_DEMO = load_fixture("api_script_config_demo_script.json")
SCENE_222 = load_fixture("api_scene_config_222.json")

CONFIG_RELPATH = "services/home-assistant/config/testinst"


class FakeClient:
    """Duck-types ha_write_api.WriteClient: .get, .post_config, .check_config."""

    def __init__(self, get_responses, check_config_result=(True, {"result": "valid"})):
        self._get_responses = dict(get_responses)
        self._check_config_result = check_config_result
        self.posted = []
        self.check_config_calls = 0

    def get(self, path):
        if path == "/api/states":
            return STATES
        return self._get_responses.get(path)

    def post_config(self, domain, object_id, body):
        self.posted.append((domain, object_id, body))
        return {"result": "ok"}

    def check_config(self):
        self.check_config_calls += 1
        return self._check_config_result


def init_repo_with_committed_objects(repo_root):
    """Commit automations/111, scripts/demo_script, scenes/222 at HEAD, exactly
    as import.sh would have written them (canonical YAML, split one-object-
    per-file)."""
    config_dir = os.path.join(repo_root, CONFIG_RELPATH)
    for subdir, key, obj in (
        ("automations", "111", AUTOMATION_111),
        ("scripts", "demo_script", SCRIPT_DEMO),
        ("scenes", "222", SCENE_222),
    ):
        d = os.path.join(config_dir, subdir)
        os.makedirs(d, exist_ok=True)
        with open(os.path.join(d, f"{key}.yaml"), "w", encoding="utf-8") as f:
            f.write(dump_canonical_yaml(obj))

    subprocess.run(["git", "init", "-q", repo_root], check=True)
    subprocess.run(["git", "-C", repo_root, "add", "-A"], check=True)
    subprocess.run(
        [
            "git",
            "-C",
            repo_root,
            "-c",
            "user.email=test@example.com",
            "-c",
            "user.name=test",
            "commit",
            "-q",
            "-m",
            "initial import",
        ],
        check=True,
    )
    return config_dir


# --- Case A: clean deploy -- all three committed objects redeployed unchanged ---

with tempfile.TemporaryDirectory() as repo_root:
    config_dir = init_repo_with_committed_objects(repo_root)

    get_responses = {
        "/api/config/automation/config/111": AUTOMATION_111,
        "/api/config/script/config/demo_script": SCRIPT_DEMO,
        "/api/config/scene/config/222": SCENE_222,
    }
    client = FakeClient(get_responses)

    result = deploy_api.run(repo_root, config_dir, CONFIG_RELPATH, "testinst", [], False, client)

    check(not result["aborted"], "clean deploy: not aborted")
    check(result["drift_aborts"] == [], "clean deploy: no drift aborts (all objects are targets)")
    check(result["validation_errors"] == [], "clean deploy: no validation errors")
    check(result["check_config_valid"] is True, "clean deploy: check_config reported valid")
    check(len(result["written"]) == 3, "clean deploy: all three objects written")
    check(result["write_failed"] == [], "clean deploy: no write failures")
    check(result["verify_failed"] == [], "clean deploy: no verify failures")
    check(
        set(client.posted[i][0:2] for i in range(3))
        == {("automation", "111"), ("script", "demo_script"), ("scene", "222")},
        "clean deploy: posted exactly the three expected (domain, id) pairs",
    )


# --- Case B: drift-abort -- a non-target object diverged on the live instance ---

with tempfile.TemporaryDirectory() as repo_root:
    config_dir = init_repo_with_committed_objects(repo_root)

    drifted_automation = dict(AUTOMATION_111)
    drifted_automation["alias"] = "Evil override made directly in the HA UI"

    get_responses = {
        "/api/config/automation/config/111": drifted_automation,  # drift: differs from repo HEAD
        "/api/config/script/config/demo_script": SCRIPT_DEMO,
        "/api/config/scene/config/222": SCENE_222,
    }
    client = FakeClient(get_responses)

    # Only redeploying the script -- automation/111 and scene/222 are NOT
    # targets, so any difference there must abort the whole run.
    result = deploy_api.run(
        repo_root,
        config_dir,
        CONFIG_RELPATH,
        "testinst",
        ["scripts/demo_script.yaml"],
        False,
        client,
    )

    check(result["aborted"], "drift-abort: run is aborted")
    check(
        any(a[0] == "automations" and a[1] == "111" for a in result["drift_aborts"]),
        "drift-abort: automations/111 reported as drifted",
    )
    check(client.posted == [], "drift-abort: no write was ever attempted")
    check(client.check_config_calls == 0, "drift-abort: check_config was never reached")


# --- Case C: local validation rejects an object missing a required key ---

with tempfile.TemporaryDirectory() as repo_root:
    config_dir = init_repo_with_committed_objects(repo_root)

    broken_dir = os.path.join(config_dir, "scripts")
    with open(os.path.join(broken_dir, "broken_script.yaml"), "w", encoding="utf-8") as f:
        f.write(dump_canonical_yaml({"alias": "broken, no sequence key"}))
    # Deliberately left uncommitted -- it's a new object being deployed for
    # the first time, which is exactly the case target-exclusion is for.

    # The other three committed objects must match live state exactly so
    # drift-check passes cleanly and the run reaches local validation.
    get_responses = {
        "/api/config/automation/config/111": AUTOMATION_111,
        "/api/config/script/config/demo_script": SCRIPT_DEMO,
        "/api/config/scene/config/222": SCENE_222,
    }
    client = FakeClient(get_responses)

    result = deploy_api.run(
        repo_root,
        config_dir,
        CONFIG_RELPATH,
        "testinst",
        ["scripts/broken_script.yaml"],
        False,
        client,
    )

    check(result["aborted"], "broken YAML: run is aborted")
    check(
        any(e[0] == "scripts" and e[1] == "broken_script" for e in result["validation_errors"]),
        "broken YAML: scripts/broken_script reported as a validation error",
    )
    check(client.posted == [], "broken YAML: no write was ever attempted")
    check(client.check_config_calls == 0, "broken YAML: check_config was never reached")

with tempfile.TemporaryDirectory() as repo_root:
    config_dir = init_repo_with_committed_objects(repo_root)

    unparseable_dir = os.path.join(config_dir, "scripts")
    with open(os.path.join(unparseable_dir, "unparseable.yaml"), "w", encoding="utf-8") as f:
        f.write("alias: [unterminated flow sequence\n")

    get_responses = {
        "/api/config/automation/config/111": AUTOMATION_111,
        "/api/config/script/config/demo_script": SCRIPT_DEMO,
        "/api/config/scene/config/222": SCENE_222,
    }
    client = FakeClient(get_responses)

    result = deploy_api.run(
        repo_root,
        config_dir,
        CONFIG_RELPATH,
        "testinst",
        ["scripts/unparseable.yaml"],
        False,
        client,
    )

    check(result["aborted"], "unparseable YAML: run is aborted, not a Python traceback")
    check(
        any(e[0] == "scripts" and e[1] == "unparseable" for e in result["validation_errors"]),
        "unparseable YAML: reported as a validation error naming the file",
    )


# --- Case D: verify detects a post-write mismatch (no auto-rollback) ---

with tempfile.TemporaryDirectory() as repo_root:
    config_dir = init_repo_with_committed_objects(repo_root)

    get_responses = {
        "/api/config/automation/config/111": AUTOMATION_111,
        # verify GET for the script comes back different from what was written --
        # e.g. the instance silently coerced/rejected a field.
        "/api/config/script/config/demo_script": {**SCRIPT_DEMO, "alias": "mutated_by_instance"},
        "/api/config/scene/config/222": SCENE_222,
    }
    client = FakeClient(get_responses)

    result = deploy_api.run(repo_root, config_dir, CONFIG_RELPATH, "testinst", [], False, client)

    check(not result["aborted"], "verify-mismatch: run completes (write/verify errors are reported, not an abort)")
    check(
        any(v[0] == "scripts" and v[1] == "demo_script" for v in result["verify_failed"]),
        "verify-mismatch: scripts/demo_script reported as a verify failure",
    )
    check(
        any(w[0] == "automations" and w[1] == "111" for w in result["written"])
        and any(w[0] == "scenes" and w[1] == "222" for w in result["written"]),
        "verify-mismatch: the other two objects still wrote and verified successfully (no auto-rollback)",
    )
    check(
        ("script", "demo_script") in {(p[0], p[1]) for p in client.posted},
        "verify-mismatch: the mismatched object was still posted (verify runs after write, not before)",
    )


sys.exit(1 if fail else 0)
PYEOF
