#!/usr/bin/env python3
"""
Uji integrasi REST API Central Admin (HMAC + Bearer token).

Pemakaian:
    python3 tests/api_test.py --base=http://127.0.0.1:8000 \
        --app=live-production \
        --api-key=ca_live-production_xxx \
        --webhook-secret=whsec_xxx

Kredensial dapat diperoleh dengan:
    php bin/central app:credentials --slug=live-production
"""
from __future__ import annotations

import argparse
import hashlib
import hmac
import json
import time
import urllib.parse

import requests

SENTINEL = "<SIGNATURE>"


class ApiTest:
    def __init__(self, base: str, app: str, api_key: str, secret: str) -> None:
        self.base = base.rstrip("/")
        self.app = app
        self.api_key = api_key
        self.secret = secret
        self.session = requests.Session()
        self.failures: list[str] = []

    # ------------------------------------------------------------------ helper
    def fresh_timestamp(self) -> str:
        """Timestamp detik yang belum dipakai request sebelumnya.

        Nonce dihitung dari (timestamp + signature) sehingga pengujian replay
        harus memakai detik baru agar tidak bertabrakan dengan request lain
        pada detik yang sama.
        """
        current = int(time.time())
        while current <= getattr(self, "_last_ts", 0):
            time.sleep(0.05)
            current = int(time.time())
        self._last_ts = current
        return str(current)

    def signature(self, method: str, path: str, body: str, timestamp: str) -> str:
        payload = ".".join([timestamp, method.upper(), path, hashlib.sha256(body.encode()).hexdigest()])
        return hmac.new(self.secret.encode(), payload.encode(), hashlib.sha256).hexdigest()

    def call(self, method: str, path: str, payload: dict | None = None, *, signed: bool = True,
             bearer: str | None = None, timestamp: str | None = None, signature: str | None = None):
        url = self.base + path
        body = json.dumps(payload) if payload is not None else ""
        headers = {"Accept": "application/json"}
        if payload is not None:
            headers["Content-Type"] = "application/json"

        if bearer is not None:
            headers["Authorization"] = "Bearer " + bearer
        elif signed:
            ts = timestamp or self.fresh_timestamp()
            headers["X-App-Id"] = self.app
            headers["X-Api-Key"] = self.api_key
            headers["X-Timestamp"] = ts
            headers["X-Signature"] = signature or self.signature(method, path, body, ts)

        return self.session.request(method, url, data=body or None, headers=headers, timeout=30)

    def check(self, label: str, condition: bool, detail: str = "") -> None:
        if condition:
            print(f"  ✓ {label}")
            return
        self.failures.append(label)
        print(f"  ✗ {label} {detail}")

    # ------------------------------------------------------------------- cases
    def run(self) -> int:
        print("· POST /api/v1/auth/connect")
        connect_path = "/api/v1/auth/connect"
        payload = {"app_id": self.app, "api_key": self.api_key, "abilities": ["config:read", "events:write"]}
        response = self.call("POST", connect_path, payload)
        self.check("connect mengembalikan 200", response.status_code == 200, f"({response.status_code}: {response.text[:160]})")
        body = response.json() if response.headers.get("Content-Type", "").startswith("application/json") else {}
        token_block = (body.get("data") or {}).get("token") or {}
        token = token_block.get("access_token") if isinstance(token_block, dict) else token_block
        self.check("response berisi access token", bool(token), str(body)[:200])
        self.check("secret tidak dikirim balik", "webhook_secret" not in response.text and "api_key" not in json.dumps(body.get("data", {})).lower())

        print("· GET /api/v1/apps/{app}/config")
        response = self.call("GET", f"/api/v1/apps/{self.app}/config")
        self.check("config 200", response.status_code == 200, f"({response.status_code}: {response.text[:160]})")
        config = (response.json().get("data") or {}) if response.status_code == 200 else {}
        self.check("config memuat modules", "modules" in config, list(config)[:10] if config else "")

        print("· GET /api/v1/apps/{app}/modules")
        response = self.call("GET", f"/api/v1/apps/{self.app}/modules")
        self.check("modules 200", response.status_code == 200, f"({response.status_code}: {response.text[:120]})")

        print("· POST /api/v1/apps/{app}/events")
        event_payload = {"event": "content.updated", "module": "content", "payload": {"source": "api_test"}}
        response = self.call("POST", f"/api/v1/apps/{self.app}/events", event_payload)
        self.check("events diterima (200/201/202)", response.status_code in (200, 201, 202), f"({response.status_code}: {response.text[:160]})")

        print("· POST /api/v1/apps/{app}/sync")
        response = self.call("POST", f"/api/v1/apps/{self.app}/sync", {"sent_at": int(time.time())})
        self.check("sync 200", response.status_code == 200, f"({response.status_code}: {response.text[:160]})")

        print("· Bearer token (hasil connect)")
        if token:
            response = self.call("GET", f"/api/v1/apps/{self.app}/modules", bearer=token)
            self.check("bearer diterima", response.status_code == 200, f"({response.status_code}: {response.text[:160]})")

        print("· Proteksi keamanan")
        response = self.call("GET", f"/api/v1/apps/{self.app}/config", signed=False)
        self.check("tanpa kredensial ditolak (401)", response.status_code == 401, f"({response.status_code})")

        ts = self.fresh_timestamp()
        bad = self.call("GET", f"/api/v1/apps/{self.app}/config", timestamp=ts, signature="0" * 64)
        self.check("signature salah ditolak (401)", bad.status_code == 401, f"({bad.status_code})")

        old_ts = str(int(time.time()) - 4000)
        stale = self.call("GET", f"/api/v1/apps/{self.app}/config", timestamp=old_ts,
                          signature=self.signature("GET", f"/api/v1/apps/{self.app}/config", "", old_ts))
        self.check("timestamp kedaluwarsa ditolak (401)", stale.status_code == 401, f"({stale.status_code})")

        ts = self.fresh_timestamp()
        path = f"/api/v1/apps/{self.app}/config"
        sig = self.signature("GET", path, "", ts)
        first = self.call("GET", path, timestamp=ts, signature=sig)
        replay = self.call("GET", path, timestamp=ts, signature=sig)
        self.check("request pertama diterima", first.status_code == 200, f"({first.status_code})")
        self.check("replay ditolak (401)", replay.status_code == 401, f"({replay.status_code})")

        print("· Validasi payload")
        bad_event = self.call("POST", f"/api/v1/apps/{self.app}/events", {"event": "bukan-event"})
        self.check("nama event tidak valid ditolak (422/400)", bad_event.status_code in (400, 422), f"({bad_event.status_code})")

        print()
        if self.failures:
            print(f"HASIL: {len(self.failures)} kegagalan -> {self.failures}")
            return 1
        print("HASIL: seluruh pemeriksaan API lolos")
        return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Uji REST API Central Admin")
    parser.add_argument("--base", default="http://127.0.0.1:8000")
    parser.add_argument("--app", default="live-production")
    parser.add_argument("--api-key", required=True)
    parser.add_argument("--webhook-secret", required=True)
    args = parser.parse_args()

    test = ApiTest(args.base, args.app, args.api_key, args.webhook_secret)
    return test.run()


if __name__ == "__main__":
    raise SystemExit(main())
