#!/usr/bin/env python3
"""Read-only self-test harness for the bitdefender skill.

Runs each CLI command as an isolated subprocess and checks exit code +
output markers. NO state-changing API calls are made (create/scan/move/
isolate/blocklist-add/delete are only tested in their --confirm-absent
refusal path). Prints a PASS/FAIL report.
"""
from __future__ import annotations

import json
import os
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
GZ = os.path.join(HERE, "gz.py")
ACG = "5c428b246c031893678b4569"  # ACG internal company (real)
# Valid-FORMAT (24-char hex) placeholder id. Non-existent on the tenant but it
# passes client-side _require_oid so gate-refusal tests reach the gate (rc3)
# instead of short-circuiting on id validation (rc2). Never used with --confirm.
VID = "0123456789abcdef01234567"

results = []


def run(args):
    env = dict(os.environ)
    env.setdefault("CLAUDETOOLS_ROOT", "C:/claudetools")
    env["PYTHONIOENCODING"] = "utf-8"
    # Never let the read-only self-test pollute errorlog.md: its intentional
    # bad-id / no-confirm cases are EXPECTED, not skill failures.
    env["GZ_SUPPRESS_ERRORLOG"] = "1"
    p = subprocess.run([sys.executable, GZ] + args, capture_output=True,
                       text=True, env=env, timeout=120)
    return p.returncode, p.stdout, p.stderr


def check(name, args, *, want_rc=None, out_has=None, err_has=None,
          out_json_ok=False, not_out=None):
    rc, out, err = run(args)
    problems = []
    if want_rc is not None and rc != want_rc:
        problems.append(f"rc={rc} want {want_rc}")
    if out_has and out_has not in out:
        problems.append(f"stdout missing {out_has!r}")
    if err_has and err_has not in err:
        problems.append(f"stderr missing {err_has!r}")
    if not_out and not_out in out:
        problems.append(f"stdout should NOT contain {not_out!r}")
    if out_json_ok:
        try:
            json.loads(out)
        except Exception as e:
            problems.append(f"stdout not valid JSON: {e}")
    status = "PASS" if not problems else "FAIL"
    results.append((status, name, "; ".join(problems), (out[:120] + (out[120:] and "...")).replace("\n", " ")))
    return rc, out, err


# --- read commands: should succeed (rc 0) ---
check("status table", ["status"], want_rc=0, out_has="apiKey")
check("status json", ["status", "--json"], want_rc=0, out_json_ok=True)
check("companies table", ["companies"], want_rc=0, out_has="Companies:")
check("companies json", ["companies", "--json"], want_rc=0, out_json_ok=True)
check("endpoints (real co)", ["endpoints", "--company", ACG], want_rc=0, out_has="Endpoints:")
check("endpoints json", ["endpoints", "--company", ACG, "--json"], want_rc=0, out_json_ok=True)
check("endpoints bogus parent -> rc2 (client-side id validation)", ["endpoints", "--company", "bogus"], want_rc=2, err_has="[ERROR]")
check("policies", ["policies"], want_rc=0, out_has="Policies:")
check("packages", ["packages"], want_rc=0, out_has="Packages:")
check("quarantine (real co)", ["quarantine", "--company", ACG], want_rc=0, out_has="Quarantine items:")
check("quarantine json", ["quarantine", "--company", ACG, "--json"], want_rc=0, out_json_ok=True)
check("blocklist", ["blocklist"], want_rc=0, out_has="Blocklist items:")
check("blocklist json", ["blocklist", "--json"], want_rc=0, out_json_ok=True)
check("blocklist page2", ["blocklist", "--page", "2", "--per-page", "3"], want_rc=0, out_has="Blocklist items:")
check("inventory cached", ["inventory"], want_rc=0, out_has="Inventory cached_at:")

# --- expanded control surface (read) ---
check("reports", ["reports"], want_rc=0, out_has="Reports:")
check("reports json", ["reports", "--json"], want_rc=0, out_json_ok=True)
check("accounts", ["accounts"], want_rc=0, out_has="Accounts:")
check("accounts json", ["accounts", "--json"], want_rc=0, out_json_ok=True)
check("notif-settings", ["notif-settings"], want_rc=0)
check("scan-tasks", ["scan-tasks"], want_rc=0, out_has="Scan tasks:")
# push read: unconfigured tenant must be treated as expected (rc0 + INFO), NOT an error
check("push-settings (unconfigured -> rc0)", ["push-settings"], want_rc=0, out_has="not configured")
check("push-stats (unconfigured -> rc0)", ["push-stats"], want_rc=0, out_has="not configured")
# policy detail must NOT carry the old false 'shallow' warning anymore
check("policy no shallow warning", ["policy", "5c42940b6e16d61a0c8b4568"], want_rc=0, err_has=None)

# --- error handling: a MALFORMED id (not 24-char hex ObjectId) is now caught
# CLIENT-SIDE by _require_oid and exits rc2 BEFORE any API call (so it neither
# hits the live tenant nor pollutes errorlog.md). A well-formed but non-existent
# hex id is ACCEPTED by GravityZone and returns a stub (rc 0) -- the API's
# behavior, not a skill bug. ---
check("endpoint bad id -> rc2 (client-side)", ["endpoint", "bogus"], want_rc=2, err_has="not a valid")
check("policy bad id -> rc2 (client-side)", ["policy", "bogus"], want_rc=2, err_has="not a valid")
check("sweep bad company -> rc2 (client-side)", ["sweep", "--company", "bogus"], want_rc=2, err_has="not a valid")
check("install-links bad company -> rc2", ["install-links", "--package", "P", "--company", "bogus"], want_rc=2, err_has="not a valid")

# --- argparse: missing required arg -> rc2 ---
check("quarantine missing --company -> rc2", ["quarantine"], want_rc=2)
check("endpoint missing positional -> rc2", ["endpoint"], want_rc=2)

# --- gating: destructive without --confirm -> rc3, no API call (valid-format ids
#     so the gate is reached; id validation is tested separately below) ---
check("isolate no confirm -> rc3", ["isolate", "--endpoints", VID], want_rc=3, err_has="Would")
check("unisolate no confirm -> rc3", ["unisolate", "--endpoints", VID], want_rc=3)
check("blocklist-add no confirm -> rc3", ["blocklist-add", "--company", ACG, "--hashes", "abc"], want_rc=3)
check("blocklist-remove no confirm -> rc3", ["blocklist-remove", "--id", "x"], want_rc=3)
check("delete-endpoint no confirm -> rc3", ["delete-endpoint", VID], want_rc=3)
check("delete-package no confirm -> rc3", ["delete-package", "--id", "x"], want_rc=3)
check("delete-group no confirm -> rc3", ["delete-group", "--group", VID], want_rc=3)
check("assign-policy no confirm -> rc3", ["assign-policy", "--policy", VID, "--targets", VID], want_rc=3, err_has="Would")
# id validation runs BEFORE the gate: a malformed id on a gated cmd -> rc2 (not rc3)
check("delete-endpoint bad id -> rc2", ["delete-endpoint", "x"], want_rc=2, err_has="not a valid")
check("isolate bad id -> rc2", ["isolate", "--endpoints", "x", "--confirm"], want_rc=2, err_has="not a valid")
check("company-delete bad id -> rc2", ["company-delete", "--id", "x", "--confirm"], want_rc=2, err_has="not a valid")
check("push-set no confirm -> rc3", ["push-set", "--status", "1", "--url", "https://x/y"], want_rc=3)
check("push-set enable no url -> rc2", ["push-set", "--status", "1", "--confirm"], want_rc=2)
check("raw assignPolicy no confirm -> rc3", ["raw", "--module", "network", "--method", "assignPolicy", "--params", "{}"], want_rc=3)

# --- remaining modules: reads ---
check("monthly-usage", ["monthly-usage"], want_rc=0)
check("integrations", ["integrations"], want_rc=0)
check("custom-rules", ["custom-rules"], want_rc=0)
check("custom-rules json", ["custom-rules", "--json"], want_rc=0, out_json_ok=True)

# --- remaining modules: gated writes (no-confirm -> rc3) ---
check("push-test no confirm -> rc3", ["push-test", "--event-type", "av"], want_rc=3)
check("report-create no confirm -> rc3", ["report-create", "--name", "R"], want_rc=3)
check("report-delete no confirm -> rc3", ["report-delete", "--id", "x"], want_rc=3)
check("quarantine-remove no confirm -> rc3", ["quarantine-remove", "--items", "x"], want_rc=3)
check("quarantine-restore no confirm -> rc3", ["quarantine-restore", "--items", "x"], want_rc=3)
check("custom-rule-create no confirm -> rc3", ["custom-rule-create", "--name", "R"], want_rc=3)
check("custom-rule-delete no confirm -> rc3", ["custom-rule-delete", "--id", "x"], want_rc=3)
check("incident-status no confirm -> rc3", ["incident-status", "--type", "t", "--set-json", "{\"status\":1}"], want_rc=3)
check("incident-status empty set-json -> rc2", ["incident-status", "--type", "t", "--set-json", "{}", "--confirm"], want_rc=2)
check("incident-note no confirm -> rc3", ["incident-note", "--type", "t", "--set-json", "{\"text\":\"x\"}"], want_rc=3)
check("raw createReport no confirm -> rc3", ["raw", "--module", "reports", "--method", "createReport", "--params", "{}"], want_rc=3)
check("raw createCustomRule no confirm -> rc3", ["raw", "--module", "incidents", "--method", "createCustomRule", "--params", "{}"], want_rc=3)

# --- network completion ---
check("endpoint-tags", ["endpoint-tags"], want_rc=0)
check("set-label no confirm -> rc3", ["set-label", "--endpoint", VID, "--label", "y"], want_rc=3)
check("reconfigure no confirm -> rc3", ["reconfigure", "--targets", VID], want_rc=3)
check("raw reconfigure no confirm -> rc3", ["raw", "--module", "network", "--method", "createReconfigureClientTask", "--params", "{}"], want_rc=3)
check("raw setEndpointLabel no confirm -> rc3", ["raw", "--module", "network", "--method", "setEndpointLabel", "--params", "{}"], want_rc=3)

# --- companies module ---
check("company (own, no id)", ["company"], want_rc=0)
check("company-create no confirm -> rc3", ["company-create", "--type", "1", "--name", "Test Co"], want_rc=3, err_has="Would")
check("company-suspend no confirm -> rc3", ["company-suspend", "--id", VID], want_rc=3)
check("company-activate no confirm -> rc3", ["company-activate", "--id", VID], want_rc=3)
check("company-delete no confirm -> rc3", ["company-delete", "--id", VID], want_rc=3)
check("raw createCompany no confirm -> rc3", ["raw", "--module", "companies", "--method", "createCompany", "--params", "{}"], want_rc=3)

# --- accounts module ---
check("account (own, no id)", ["account"], want_rc=0)
check("account-create no confirm -> rc3", ["account-create", "--email", "t@x.io"], want_rc=3, err_has="Would")
check("account-update no confirm -> rc3", ["account-update", "--id", VID, "--set-json", "{\"role\":5}"], want_rc=3)
check("account-update bad json -> rc2", ["account-update", "--id", VID, "--set-json", "{bad", "--confirm"], want_rc=2)
check("account-delete no confirm -> rc3", ["account-delete", "--id", VID], want_rc=3)
check("notif-configure no confirm -> rc3", ["notif-configure", "--settings-json", "{\"deleteAfter\":7}"], want_rc=3)
check("raw createAccount no confirm -> rc3", ["raw", "--module", "accounts", "--method", "createAccount", "--params", "{}"], want_rc=3)

# --- raw gating ---
check("raw destructive no confirm -> rc3", ["raw", "--module", "network",
      "--method", "deleteEndpoint", "--params", "{}"], want_rc=3)
check("raw bad json params -> rc2", ["raw", "--module", "general",
      "--method", "getApiKeyDetails", "--params", "{bad"], want_rc=2)
check("raw read ok", ["raw", "--module", "general", "--method",
      "getApiKeyDetails", "--params", "{}"], want_rc=0)

# --- report ---
print("\n==== bitdefender skill self-test ====")
npass = sum(1 for r in results if r[0] == "PASS")
for status, name, prob, sample in results:
    line = f"[{status}] {name}"
    if prob:
        line += f"  -> {prob}"
    print(line)
print(f"\n{npass}/{len(results)} passed, {len(results)-npass} failed")
sys.exit(0 if npass == len(results) else 1)
