#!/usr/bin/env python3
"""Static validation for the activeblue_familylaw Odoo 18 module.

Not a substitute for running the Odoo test suite — it catches the classes of error
that are cheap to find without a running Odoo/DB:

  * Python files compile
  * XML files are well-formed
  * no Odoo-18-forbidden constructs in views (attrs=, states=, <tree>)
  * every type="object" button name maps to a real method in the model layer
  * every file listed in __manifest__["data"] exists
  * ir.model.access.csv references models that are defined

Run: python3 scripts/validate_module.py
"""
import ast
import csv
import os
import re
import sys
import xml.etree.ElementTree as ET

ROOT = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "activeblue_familylaw_handoff",
    "activeblue_familylaw_build",
    "activeblue_familylaw_v2",
)

errors = []
ok_count = 0


def ok(_msg):
    global ok_count
    ok_count += 1


def walk(ext):
    for dirpath, _dirs, files in os.walk(ROOT):
        for f in files:
            if f.endswith(ext):
                yield os.path.join(dirpath, f)


# 1. Python compiles + collect method names + model names
methods = set()
model_names = set()
model_tech_names = set()  # familylaw.document -> model_familylaw_document
for f in walk(".py"):
    src = open(f).read()
    try:
        tree = ast.parse(src)
    except SyntaxError as e:
        errors.append(f"PYTHON SYNTAX {f}: {e}")
        continue
    ok(f)
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            methods.add(node.name)
    for m in re.finditer(r'_name\s*=\s*["\']([a-z0-9_.]+)["\']', src):
        model_names.add(m.group(1))
        model_tech_names.add("model_" + m.group(1).replace(".", "_"))

# 2. XML well-formed + forbidden constructs + collect buttons
buttons = []
for f in walk(".xml"):
    raw = open(f).read()
    try:
        root = ET.fromstring(raw)
    except ET.ParseError as e:
        errors.append(f"XML PARSE {f}: {e}")
        continue
    ok(f)
    # strip XML comments before scanning for forbidden tokens
    no_comments = re.sub(r"<!--.*?-->", "", raw, flags=re.DOTALL)
    for bad in ("attrs=", "states=", "<tree"):
        if bad in no_comments:
            errors.append(f"FORBIDDEN '{bad}' in {f}")
    for btn in root.iter("button"):
        if btn.get("type") == "object" and btn.get("name"):
            buttons.append((btn.get("name"), f))

# 3. Buttons map to methods
for name, src in buttons:
    if name not in methods:
        errors.append(f"BUTTON '{name}' has no method (in {src})")

# 4. Manifest data files exist
manifest_path = os.path.join(ROOT, "__manifest__.py")
manifest = ast.literal_eval(
    re.sub(r"^#.*", "", open(manifest_path).read(), flags=re.MULTILINE).strip()
)
for rel in manifest.get("data", []):
    if not os.path.exists(os.path.join(ROOT, rel)):
        errors.append(f"MANIFEST data file missing: {rel}")

# 5. ir.model.access.csv references defined models
acl = os.path.join(ROOT, "security", "ir.model.access.csv")
if os.path.exists(acl):
    with open(acl) as fh:
        for row in csv.DictReader(fh):
            mid = row["model_id:id"]
            # external models (base.*, mail.*) are fine; only check our own
            if mid.startswith("model_familylaw_") and mid not in model_tech_names:
                errors.append(f"ACL references unknown model: {mid}")

print(f"Checked OK: {ok_count} files | models: {len(model_names)} | "
      f"buttons: {len(buttons)}")
if errors:
    print("\n".join("FAIL: " + e for e in errors))
    sys.exit(1)
print("ALL STATIC CHECKS PASSED")
