#!/usr/bin/env python3
"""Synthetic loopback Git HTTP proxy evidence; needs Python 3 and git on PATH.

Runs only against ephemeral 127.0.0.1 servers and temporary repositories.
No real credentials, external service, HTTPS, SSH, push or provider benchmark.
Writes one JSON report to stdout and exits nonzero if any expectation fails.
"""

import base64
import functools
import hashlib
import http.client
import http.server
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import threading
from datetime import datetime, timezone
from urllib.parse import urlsplit


PAYLOAD = b"Git proxy lab: verify repository content, not just HTTP status.\n"
SYNTHETIC_AUTH = "Basic " + base64.b64encode(b"lab-user:lab-password").decode()
HOP_HEADERS = {
    "connection", "proxy-connection", "keep-alive", "transfer-encoding",
    "te", "trailer", "upgrade", "proxy-authorization", "proxy-authenticate",
}


def run_lab():
    started = datetime.now(timezone.utc).isoformat()
    git_exe = shutil.which("git")
    if not git_exe:
        raise RuntimeError("git is required on PATH")
    with tempfile.TemporaryDirectory(prefix="git-proxy-lab-") as temp:
        root = Path(temp)
        global_file = root / "isolated-global.gitconfig"
        global_file.write_text("")
        # Isolate configuration without overwriting HOME or the user's files.
        env = {
            key: value for key, value in os.environ.items()
            if not key.startswith("GIT_") and not key.lower().endswith("_proxy")
        }
        env.update({
            "GIT_CONFIG_NOSYSTEM": "1",
            "GIT_CONFIG_GLOBAL": str(global_file),
            "GIT_TERMINAL_PROMPT": "0",
            "GIT_AUTHOR_DATE": "2026-10-12T00:00:00+0000",
            "GIT_COMMITTER_DATE": "2026-10-12T00:00:00+0000",
            "GIT_DEFAULT_HASH": "sha1",
            "LC_ALL": "C",
        })

        def git(*args, cwd=None, extra_env=None, check=True):
            command = [git_exe, "-c", "credential.helper=", *map(str, args)]
            result = subprocess.run(
                command, cwd=cwd, env={**env, **(extra_env or {})},
                capture_output=True, text=True, timeout=20,
            )
            if check and result.returncode:
                raise RuntimeError(f"git command failed: {args}: {result.stderr}")
            return result

        seed = root / "seed"
        git("init", "--initial-branch=main", seed)
        (seed / "payload.txt").write_bytes(PAYLOAD)
        git("-C", seed, "add", "payload.txt")
        git("-C", seed, "-c", "user.name=Synthetic Git Lab", "-c",
            "user.email=lab@example.invalid", "commit", "-m", "Synthetic fixture")
        expected_commit = git("-C", seed, "rev-parse", "HEAD").stdout.strip()
        webroot = root / "webroot"
        (webroot / "match").mkdir(parents=True)
        (webroot / "other").mkdir()
        bare = webroot / "match" / "demo.git"
        git("clone", "--bare", seed, bare)
        git("-C", bare, "update-server-info")
        shutil.copytree(bare, webroot / "other" / "demo.git")
        origin_requests = []
        proxy_requests = []
        current_case = {"name": None}

        class Origin(http.server.SimpleHTTPRequestHandler):
            def do_GET(self):
                origin_requests.append({
                    "case": current_case["name"], "path": self.path,
                    "proxy_authorization_received": "Proxy-Authorization" in self.headers,
                })
                super().do_GET()

            def log_message(self, *args):
                pass

        origin = http.server.ThreadingHTTPServer(
            ("127.0.0.1", 0), functools.partial(Origin, directory=str(webroot))
        )
        origin_port = origin.server_address[1]

        class Proxy(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                parsed = urlsplit(self.path)
                authorized = self.headers.get("Proxy-Authorization") == SYNTHETIC_AUTH
                event = {
                    "case": current_case["name"], "target": self.path,
                    "authorization_present": "Proxy-Authorization" in self.headers,
                    "authorization_accepted": authorized,
                }
                proxy_requests.append(event)
                if not authorized:
                    event["status"] = 407
                    self.send_response(407)
                    self.send_header("Proxy-Authenticate", 'Basic realm="synthetic-git-lab"')
                    self.send_header("Content-Length", "0")
                    self.end_headers()
                    return
                if (parsed.scheme != "http" or parsed.hostname != "127.0.0.1"
                        or parsed.port != origin_port):
                    event["status"] = 403
                    self.send_error(403, "This fixture forwards only its own origin")
                    return
                connection = http.client.HTTPConnection("127.0.0.1", origin_port, timeout=5)
                try:
                    headers = {
                        key: value for key, value in self.headers.items()
                        if key.lower() not in HOP_HEADERS
                    }
                    path = parsed.path + ("?" + parsed.query if parsed.query else "")
                    connection.request("GET", path, headers=headers)
                    response = connection.getresponse()
                    body = response.read()
                    event["status"] = response.status
                    self.send_response(response.status)
                    for key, value in response.getheaders():
                        if key.lower() not in HOP_HEADERS | {"content-length"}:
                            self.send_header(key, value)
                    self.send_header("Content-Length", str(len(body)))
                    self.end_headers()
                    self.wfile.write(body)
                finally:
                    connection.close()

            def log_message(self, *args):
                pass

        proxy = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Proxy)
        proxy_port = proxy.server_address[1]
        threads = []
        for server in (origin, proxy):
            thread = threading.Thread(target=server.serve_forever, daemon=True)
            thread.start()
            threads.append(thread)
        target = f"http://127.0.0.1:{origin_port}/match/demo.git"
        other = f"http://127.0.0.1:{origin_port}/other/demo.git"
        good_proxy = f"http://lab-user:lab-password@127.0.0.1:{proxy_port}"
        bad_proxy = f"http://lab-user:wrong-synthetic-password@127.0.0.1:{proxy_port}"
        scope_key = f"http.http://127.0.0.1:{origin_port}/match/.proxy"
        cases = []

        def record(name, args, repo, ref="HEAD", extra_env=None,
                   expect_proxy=False, expect_success=True, expect_challenge=False):
            current_case["name"] = name
            before_proxy = len(proxy_requests)
            before_origin = len(origin_requests)
            result = git(*args, extra_env=extra_env, check=False)
            requests = proxy_requests[before_proxy:]
            origins = origin_requests[before_origin:]
            success = result.returncode == 0
            commit = None
            payload_hash = None
            verified = False
            if success:
                commit = git("-C", repo, "rev-parse", ref).stdout.strip()
                payload = git("-C", repo, "show", f"{ref}:payload.txt").stdout.encode()
                payload_hash = hashlib.sha256(payload).hexdigest()
                verified = commit == expected_commit and payload == PAYLOAD
            challenges = sum(req["status"] == 407 for req in requests)
            forwarded = sum(req.get("authorization_accepted", False) for req in requests)
            expectations = {
                "expected_exit_outcome": success == expect_success,
                "repository_integrity": verified if expect_success else not origins,
                "expected_proxy_routing": bool(requests) == expect_proxy,
                "proxy_credentials_not_forwarded_to_origin": all(
                    not req["proxy_authorization_received"] for req in origins
                ),
                "auth_challenge_observed": challenges > 0 if expect_challenge else True,
            }
            if expect_proxy and expect_success:
                expectations["authenticated_forwarding_observed"] = forwarded > 0
            case = {
                "name": name, "command": [git_exe, "-c", "credential.helper=", *map(str, args)],
                "extra_environment": extra_env or {}, "returncode": result.returncode,
                "stdout": result.stdout, "stderr": result.stderr,
                "expected_success": expect_success, "repository_verified": verified,
                "commit": commit, "payload_sha256": payload_hash,
                "proxy_requests": requests, "origin_requests": origins,
                "challenge_407_count": challenges, "authenticated_request_count": forwarded,
                "expectations": expectations, "passed": all(expectations.values()),
            }
            cases.append(case)

        try:
            repo = root / "one-off"
            record("one_off_command_proxy", ["-c", f"http.proxy={good_proxy}", "clone", target, repo],
                   repo, expect_proxy=True, expect_challenge=True)
            repo = root / "bad-auth"
            record("bad_credentials_407", ["-c", f"http.proxy={bad_proxy}", "-c",
                   "http.proxyAuthMethod=basic", "clone", target, repo], repo,
                   expect_proxy=True, expect_success=False, expect_challenge=True)
            repo = root / "url-match"
            record("url_scoped_match", ["-c", f"{scope_key}={good_proxy}", "clone", target, repo],
                   repo, expect_proxy=True)
            repo = root / "url-no-match"
            record("url_scoped_unrelated_path", ["-c", f"{scope_key}={good_proxy}",
                   "clone", other, repo], repo)
            global_file.write_text(f'[http]\n\tproxy = http://127.0.0.1:9\n')
            repo = root / "remote-override"
            git("init", "--initial-branch=main", repo)
            git("-C", repo, "remote", "add", "origin", target)
            git("-C", repo, "config", "remote.origin.proxy", good_proxy)
            record("remote_proxy_overrides_generic", ["-C", repo, "fetch", "origin"],
                   repo, ref="refs/remotes/origin/main", expect_proxy=True)
            global_file.write_text(f'[http]\n\tproxy = {good_proxy}\n')
            repo = root / "remote-empty"
            git("init", "--initial-branch=main", repo)
            git("-C", repo, "remote", "add", "origin", target)
            git("-C", repo, "config", "remote.origin.proxy", "")
            record("remote_empty_disables_proxy", ["-C", repo, "fetch", "origin"],
                   repo, ref="refs/remotes/origin/main")
            repo = root / "command-empty"
            record("command_empty_clears_generic", ["-c", "http.proxy=", "clone", target, repo], repo)
            # A generic -c value does not remove a separately matched URL key.
            global_file.write_text(
                f'[http "http://127.0.0.1:{origin_port}/match/"]\n\tproxy = {good_proxy}\n'
            )
            repo = root / "command-empty-url-match"
            record("command_empty_does_not_clear_url_scope",
                   ["-c", "http.proxy=", "clone", target, repo], repo, expect_proxy=True)
            global_file.write_text(f'[http]\n\tproxy = {good_proxy}\n')
            repo = root / "url-empty"
            record("url_empty_overrides_generic", ["-c", f"{scope_key}=", "clone", target, repo], repo)
            repo = root / "no-proxy-env"
            record("no_proxy_loopback_bypass", ["clone", target, repo], repo,
                   extra_env={"no_proxy": "127.0.0.1"})

            # Meaningful diagnostics: same key in global, local and command scopes.
            diagnostic_repo = root / "diagnostic"
            git("init", "--initial-branch=main", diagnostic_repo)
            git("-C", diagnostic_repo, "config", "http.proxy", "http://local.invalid:8080")
            scope_result = git("-C", diagnostic_repo, "-c", "http.proxy=http://command.invalid:8080",
                               "config", "--show-origin", "--show-scope", "--get-all", "http.proxy")
            url_result = git("-c", f"{scope_key}={good_proxy}", "config", "--get-urlmatch", "http.proxy", target)
            version = git("--version").stdout.strip()
            report = {
                "schema": "git-proxy-loopback-evidence-v1", "started_at": started,
                "finished_at": datetime.now(timezone.utc).isoformat(),
                "environment": {"git": version, "git_executable": git_exe,
                    "python": sys.version, "platform": platform.platform(),
                    "macos": platform.mac_ver()[0],
                    "configuration_isolated": True, "inherited_proxy_variables_removed": True},
                "fixture": {"network_scope": "127.0.0.1 only", "transport": "dumb HTTP",
                    "origin": target, "proxy": f"http://127.0.0.1:{proxy_port}",
                    "authentication": "synthetic Basic challenge", "expected_commit": expected_commit,
                    "expected_payload_sha256": hashlib.sha256(PAYLOAD).hexdigest()},
                "cases": cases,
                "diagnostics": {"origin_scope_command": scope_result.args,
                    "origin_scope_stdout": scope_result.stdout, "urlmatch_command": url_result.args,
                    "urlmatch_stdout": url_result.stdout},
                "summary": {"cases": len(cases), "passed": sum(c["passed"] for c in cases),
                    "failed": [c["name"] for c in cases if not c["passed"]]},
                "limitations": ["Synthetic loopback routing evidence, not an external provider result.",
                    "Dumb HTTP clone/fetch only; no push, smart HTTP, HTTPS CONNECT, TLS, SSH or SOCKS tests.",
                    "Basic authentication uses deliberately fake credentials over local HTTP.",
                    "Request counts and allocated ports may differ between git/libcurl builds.",
                    "Only the recorded Git and Python environment was executed in this run.",
                    "Temporary repositories/configuration are deleted after execution."]}
            return report
        finally:
            for server in (proxy, origin):
                server.shutdown()
                server.server_close()
            for thread in threads:
                thread.join(timeout=2)


if __name__ == "__main__":
    result = run_lab()
    print(json.dumps(result, indent=2))
    sys.exit(0 if not result["summary"]["failed"] else 1)
