#!/usr/bin/env python3
"""
Uji siklus hidup App & Module (bukti prinsip \"tambah website X\" dan
\"tambah module X ke website Y\" tanpa mengubah Core).

Alur yang diuji:
  1. Websites → + Add Website (App: Top Up)         → App dibuat + kredensial
  2. Workspace App baru                             → navigation kosong
  3. Modules → Create custom module (products)      → scaffold + register + attach
  4. Sidebar/navigation App baru memuat module      → halaman module dapat dibuka
  5. Isolasi: module tidak muncul pada App lain     → 404 pada App LiveProduction
  6. Modules → Archive module                       → katalog & relasi dibersihkan
  7. Pengaturan App → Hapus website                 → App dihapus (soft state bersih)

Pemakaian:
    python3 tests/lifecycle_test.py --password=... [--keep]

Tanpa --keep, seluruh data uji dibersihkan di akhir.
"""
from __future__ import annotations

import argparse
import re
import sys
import time

import requests
from bs4 import BeautifulSoup


class Lifecycle:
    def __init__(self, base: str, email: str, password: str, app_name: str, app_id: str, module_id: str) -> None:
        self.base = base.rstrip("/")
        self.email = email
        self.password = password
        self.app_name = app_name
        self.app_id = app_id
        self.module_id = module_id
        self.slug = app_id
        self.session = requests.Session()
        self.session.headers.update({"User-Agent": "CentralAdminLifecycleTest/1.0"})
        self.failures: list[str] = []

    # ------------------------------------------------------------------ helper
    def get(self, path: str):
        return self.session.get(self.base + path, timeout=30)

    def token(self, path: str) -> str | None:
        match = re.search(r'name="_token" value="([^"]+)"', self.get(path).text)
        return match.group(1) if match else None

    def post(self, path: str, data: dict, referer: str | None = None):
        return self.session.post(
            self.base + path, data=data,
            headers={"Referer": self.base + (referer or path)},
            allow_redirects=False, 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}")

    def toast(self, path: str) -> str:
        html = self.get(path).text
        errors = re.findall(r'toast is-error"><div class="toast-body"><strong>[^<]*</strong><p>([^<]*)</p>', html)
        return errors[0] if errors else ""

    def login(self) -> None:
        response = self.post("/login", {"_token": self.token("/login"), "email": self.email, "password": self.password})
        if response.status_code != 302:
            print("login gagal", response.status_code)
            raise SystemExit(1)

    # ------------------------------------------------------------------- steps
    def create_website(self) -> None:
        print("· [1] Tambah website (App Top Up)")
        data = {
            "_token": self.token("/websites/create"),
            "name": self.app_name,
            "slug": self.slug,
            "domain": "topup.example.test",
            "description": "Website top up (uji siklus hidup)",
            "app_id": self.app_id,
            "version": "1.0.0",
            "status": "disconnected",
            "logo_url": "",
            "favicon_url": "",
        }
        response = self.post("/websites", data, "/websites/create")
        self.check("POST /websites -> 302", response.status_code == 302, f"({response.status_code})")
        page = self.get("/apps/" + self.slug)
        self.check("workspace App baru dapat dibuka", page.status_code == 200, f"({page.status_code})")
        self.check("kredensial sekali-papar tersedia", "Simpan Sekarang" in page.text, "")
        self.check("navigation App baru masih kosong", "Belum ada module aktif" in page.text, "")
        error = self.toast("/apps/" + self.slug)
        if error:
            self.check("tanpa error", False, error)

    def create_custom_module(self) -> None:
        print(f"· [3] Buat module custom '{self.module_id}'")
        path = f"/apps/{self.slug}/modules/add?tab=custom"
        data = {
            "_token": self.token(path),
            "name": "Products",
            "module_id": self.module_id,
            "description": "Katalog produk top up",
            "icon": "box",
            "version": "1.0.0",
            "nav_label": "Products",
            "nav_order": "10",
            "nav_enabled": "1",
            "status": "active",
            "permissions[]": ["view", "create", "update", "delete"],
        }
        response = self.post(f"/apps/{self.slug}/modules/create", data, path)
        self.check("POST modules/create -> 302", response.status_code == 302, f"({response.status_code})")
        page = self.get(f"/apps/{self.slug}/modules")
        self.check("module muncul pada daftar", "Products" in page.text, "")
        workspace = self.get("/apps/" + self.slug)
        self.check("module muncul di sidebar/workspace", "Products" in workspace.text, "")
        module_page = self.get(f"/apps/{self.slug}/{self.module_id}")
        self.check("halaman module dapat dibuka", module_page.status_code == 200, f"({module_page.status_code})")
        perms = self.get("/permissions")
        self.check(
            "permission module terdaftar",
            f"{self.app_id}.{self.module_id}.view" in perms.text or f"{self.module_id}.view" in perms.text,
            "",
        )

    def verify_isolation(self) -> None:
        print("· [5] Isolasi module antar App")
        leaked = self.get(f"/apps/live-production/{self.module_id}")
        self.check("module tidak bocor ke App lain (404)", leaked.status_code == 404, f"({leaked.status_code})")
        menu = self.get("/apps/live-production/modules")
        self.check("module tidak muncul pada katalog App lain", "Products" not in menu.text, "")

    def archive_module(self) -> None:
        print("· [6] Arsipkan module custom")
        page = self.get(f"/apps/{self.slug}/modules")
        response = self.post(
            f"/apps/{self.slug}/modules/archive",
            {"_token": self.token(f"/apps/{self.slug}/modules"), "module": self.module_id},
            f"/apps/{self.slug}/modules",
        )
        self.check("POST modules/archive -> 302", response.status_code == 302, f"({response.status_code})")
        after = self.get(f"/apps/{self.slug}/modules")
        self.check("module hilang dari daftar", "Products" not in after.text, "")
        self.check("halaman module 404 setelah arsip", self.get(f"/apps/{self.slug}/{self.module_id}").status_code == 404, "")

    def delete_website(self) -> None:
        print("· [7] Hapus website uji")
        data = {"_token": self.token("/apps/" + self.slug), "_method": "DELETE"}
        response = self.post("/apps/" + self.slug, data, "/apps/" + self.slug)
        self.check("DELETE /apps/{app} -> 302", response.status_code == 302, f"({response.status_code})")
        self.check("website tidak dapat diakses lagi", self.get("/apps/" + self.slug).status_code == 404, "")

    def run(self, keep: bool) -> int:
        self.login()
        self.create_website()
        self.create_custom_module()
        self.verify_isolation()
        if keep:
            print("\n(--keep) data uji dibiarkan; hapus manual dari UI bila perlu.")
            return 0 if not self.failures else 1
        self.archive_module()
        self.delete_website()

        print()
        if self.failures:
            print(f"HASIL: {len(self.failures)} kegagalan -> {self.failures}")
            return 1
        print("HASIL: siklus hidup App & Module berjalan penuh")
        return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Uji siklus hidup App & Module")
    parser.add_argument("--base", default="http://127.0.0.1:8000")
    parser.add_argument("--email", default="admin@central-admin.test")
    parser.add_argument("--password", required=True)
    parser.add_argument("--app-name", default="Top Up Demo")
    parser.add_argument("--app-id", default="top-up")
    parser.add_argument("--module-id", default="products")
    parser.add_argument("--keep", action="store_true", help="jangan bersihkan data uji")
    args = parser.parse_args()

    test = Lifecycle(args.base, args.email, args.password, args.app_name, args.app_id, args.module_id)
    return test.run(args.keep)


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