#!/usr/bin/env python3
"""Real Gitea push/PR validation in a dedicated repository. Run with --prepare first."""
import base64
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.error
from pathlib import Path

BUNDLE_ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
    "activation", BUNDLE_ROOT / "scripts/activate-drone.py"
)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)
config, gitea, drone, op = a.connect()
slug = config["GITEA_USER"] + "/ci-validation"
headers = {
    "Authorization": "Basic "
    + base64.b64encode(
        (config["GITEA_USER"] + ":" + config["GITEA_PASSWORD"]).encode()
    ).decode()
}


def ga(method, path, body=None):
    return a.api(op, method, gitea + "/api/v1" + path, body, headers)


def da(method, path, body=None):
    return a.api(op, method, drone + "/api/repos/" + slug + path, body)


if "--prepare" in sys.argv:
    try:
        ga("GET", "/repos/" + slug)
    except urllib.error.HTTPError as error:
        if error.code != 404:
            raise
        ga(
            "POST",
            "/user/repos",
            {
                "name": "ci-validation",
                "private": True,
                "auto_init": False,
                "default_branch": "main",
            },
        )
    path = BUNDLE_ROOT / "config/services.env"
    s = path.read_text()
    line = next(x for x in s.splitlines() if x.startswith("CI_REPOSITORIES="))
    if slug not in line.split("=", 1)[1].split(","):
        path.write_text(s.replace(line, line + "," + slug))
    print(
        "Validation repository prepared. Run scripts/enable-automatic-ci.sh, then this script."
    )
    raise SystemExit()
a.activate(op, drone, slug)
work = BUNDLE_ROOT / "runs" / ("native-validation-" + time.strftime("%Y%m%d%H%M%S"))
work.mkdir()
repo = work / "checkout"
shutil.copytree(BUNDLE_ROOT / "app", repo)
ask = work / "askpass"
ask.write_text(
    '#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$VALIDATION_USER";; *) printf "%s\\n" "$VALIDATION_PASSWORD";; esac\n'
)
ask.chmod(0o700)
env = {
    **os.environ,
    "GIT_ASKPASS": str(ask),
    "GIT_TERMINAL_PROMPT": "0",
    "VALIDATION_USER": config["GITEA_USER"],
    "VALIDATION_PASSWORD": config["GITEA_PASSWORD"],
}


def git(*args):
    return subprocess.check_output(
        ["git", *args], cwd=repo, env=env, stderr=subprocess.STDOUT, text=True
    ).strip()


def commit(message):
    git("add", ".")
    git("commit", "-qm", message)
    return git("rev-parse", "HEAD")


def wait(sha, event):
    deadline = time.time() + 1200
    last = None
    while time.time() < deadline:
        builds = da("GET", "/builds") or []
        matches = [b for b in builds if b["after"] == sha and b["event"] == event]
        if matches:
            b = da("GET", "/builds/" + str(matches[0]["number"]))
            if b["status"] != last:
                print(event, "build", b["number"], b["status"], flush=True)
                last = b["status"]
            if b["status"] in (
                "success",
                "failure",
                "error",
                "killed",
                "skipped",
                "declined",
            ):
                out = work / (event + "-" + str(b["number"]))
                out.mkdir()
                (out / "build.json").write_text(json.dumps(b, indent=2))
                for stage in b.get("stages", []):
                    for step in stage.get("steps", []):
                        if step["status"] == "skipped":
                            continue
                        logs = (
                            da(
                                "GET",
                                f"/builds/{b['number']}/logs/{stage['number']}/{step['number']}",
                            )
                            or []
                        )
                        # Client commands never print per-job credentials.
                        (out / (step["name"] + ".log")).write_text(
                            "".join(line.get("out", "") for line in logs)
                        )
                return b
        time.sleep(3)
    raise RuntimeError("Timed out waiting for automatic " + event)


def steps(b):
    return {
        s["name"]: s for stage in b.get("stages", []) for s in stage.get("steps", [])
    }


try:
    git("init", "-q", "-b", "main")
    git("remote", "add", "origin", gitea + "/" + slug + ".git")
    if git("ls-remote", "origin", "refs/heads/main"):
        git("fetch", "origin", "main")
        git("reset", "--mixed", "origin/main")
    git("config", "user.name", "Native CI validation")
    git("config", "user.email", "ci-validation@localhost.invalid")
    (repo / "validation.txt").write_text(
        "Native image pipeline test " + work.name + "\n"
    )
    sha = commit("Use repository-owned native scanner pipeline")
    git("push", "-u", "origin", "main")
    main = wait(sha, "push")
    main_steps = steps(main)
    assert all(
        main_steps[name]["status"] == "success"
        for name in ["clone", "prepare", "gitleaks", "semgrep"]
    ), main
    assert (
        main_steps["sonarqube"]["status"] == "failure"
        and main_steps["security-gate"]["status"] == "failure"
    ), main
    assert (
        main_steps["os-build"]["status"] == "skipped"
        and main_steps["release"]["status"] == "skipped"
    ), main
    assert main_steps["semgrep"]["image"] == "docker.io/semgrep/semgrep:1.176.0", main
    branch_name = "native-security-" + time.strftime("%Y%m%d%H%M%S")
    git("checkout", "-b", branch_name)
    (repo / "src/sast_fixture.py").write_text(
        "def unsafe_example(value):\n    return eval(value)\n"
    )
    sha = commit("Verify native Semgrep rejection")
    git("push", "-u", "origin", branch_name)
    branch = wait(sha, "push")
    assert (
        steps(branch)["semgrep"]["status"] == "failure"
        and steps(branch)["security-gate"]["status"] == "failure"
    ), branch
    ga(
        "POST",
        "/repos/" + slug + "/pulls",
        {"head": branch_name, "base": "main", "title": "Native scanner validation"},
    )
    pr = wait(sha, "pull_request")
    assert steps(pr)["semgrep"]["status"] == "failure", pr
    assert all(
        steps(pr).get(name, {"status": "skipped"})["status"] == "skipped"
        for name in ["sonarqube", "os-build", "cosign-sign", "release"]
    ), pr
    (repo / "src/sast_fixture.py").unlink()
    sha = commit("Verify passing native branch source scans")
    git("push")
    clean = wait(sha, "push")
    assert clean["status"] == "success", clean
    # Exercise native build/SBOM/vulnerability images with source that passes Sonar.
    git("checkout", "main")
    (repo / "src/app.py").write_text(
        '"""Source-only build fixture; not a release-ready HTTP application."""\n\ndef greeting(name):\n    return f"Hello, {name}"\n'
    )
    sha = commit("Exercise native build and vulnerability gates")
    git("push", "origin", "main")
    built = wait(sha, "push")
    built_steps = steps(built)
    assert all(
        built_steps[name]["status"] == "success"
        for name in ["security-gate", "os-build", "syft-sbom"]
    ), built
    assert built_steps["vulnerability-gate"]["status"] == "failure", built
    assert (
        built_steps["cosign-sign"]["status"] == "skipped"
        and built_steps["release"]["status"] == "skipped"
    ), built
    result = {
        "result": "passed",
        "main": main["number"],
        "unsafe_branch": branch["number"],
        "pr": pr["number"],
        "clean_branch": clean["number"],
        "build_vulnerability_gate": built["number"],
        "note": "Native scanner images tested; expected rejections; no release",
    }
    (work / "RESULT.json").write_text(json.dumps(result, indent=2))
    print("PASS: native image pipeline. Evidence:", work)
finally:
    ask.unlink(missing_ok=True)
