#!/usr/bin/env python3
"""Local-only TLS/SNI diagnostic experiment. Requires Python 3, curl and OpenSSL 3.
Creates temporary self-signed certificates and a loopback-only HTTPS server.
Does not install CA certificates, contact external servers or alter NPM.
"""
import http.server
import json
import os
from pathlib import Path
import ssl
import subprocess
import tempfile
import threading


def run(args, **kw):
    return subprocess.run(args, capture_output=True, text=True, timeout=12, **kw)


def main():
    results = []
    with tempfile.TemporaryDirectory(prefix='mlog-tls-test-') as tmp:
        d = Path(tmp)
        for name in ['site.test', 'default.test']:
            p = run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
                     '-keyout', str(d / (name + '.key')), '-out', str(d / (name + '.pem')),
                     '-days', '1', '-subj', '/CN=' + name,
                     '-addext', 'subjectAltName=DNS:' + name])
            if p.returncode:
                raise RuntimeError(p.stderr)
        contexts = {}
        for name in ['site.test', 'default.test']:
            c = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            c.load_cert_chain(d / (name + '.pem'), d / (name + '.key'))
            contexts[name] = c
        contexts['default.test'].set_servername_callback(
            lambda sock, name, ctx: setattr(sock, 'context', contexts.get(name, ctx)))

        class Handler(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                payload = b'local TLS test\n'
                self.send_response(200)
                self.send_header('Content-Length', str(len(payload)))
                self.end_headers()
                self.wfile.write(payload)

            def log_message(self, *args):
                pass

        server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
        server.socket = contexts['default.test'].wrap_socket(server.socket, server_side=True)
        thread = threading.Thread(target=server.serve_forever, daemon=True)
        thread.start()
        port = server.server_port
        bundle = d / 'trust.pem'
        bundle.write_bytes((d / 'site.test.pem').read_bytes() + (d / 'default.test.pem').read_bytes())
        env = dict(os.environ)
        for key in list(env):
            if key.lower().endswith('_proxy') or key in ['CURL_CA_BUNDLE', 'SSL_CERT_FILE', 'SSL_CERT_DIR']:
                env.pop(key)
        curl = ['curl', '--noproxy', '*', '--max-time', '5', '-sS', '-o', os.devnull, '-w', '%{http_code}']

        def case(name, args, expected, contains=None, **kwargs):
            p = run(args, **kwargs)
            ok = (p.returncode == expected if expected is not None else p.returncode != 0)
            if contains is not None:
                ok = ok and contains in p.stdout + p.stderr
            results.append({'case': name, 'returncode': p.returncode, 'passed': ok})

        try:
            resolve = ['--resolve', f'site.test:{port}:127.0.0.1']
            url = f'https://site.test:{port}/'
            case('resolve_preserves_hostname_and_sni', curl + ['--cacert', str(bundle)] + resolve + [url], 0, '200', env=env)
            case('host_header_does_not_fix_ip_url_tls', curl + ['--cacert', str(bundle), '-H', 'Host: site.test', f'https://127.0.0.1:{port}/'], 60, env=env)
            case('untrusted_self_signed_certificate', curl + resolve + [url], 60, env=env)
            case('wrong_hostname', curl + ['--cacert', str(bundle), '--resolve', f'wrong.test:{port}:127.0.0.1', f'https://wrong.test:{port}/'], 60, env=env)
            client = ['openssl', 's_client', '-connect', f'127.0.0.1:{port}', '-CAfile', str(bundle), '-verify_return_error']
            case('openssl_explicit_sni_and_hostname', client + ['-servername', 'site.test', '-verify_hostname', 'site.test'], 0, input='')
            case('openssl_hostname_mismatch_fails', client + ['-servername', 'site.test', '-verify_hostname', 'wrong.test'], None, 'hostname mismatch', input='')
            case('no_sni_selects_test_default_certificate', client + ['-noservername', '-verify_hostname', 'default.test'], 0, input='')
            case('one_day_certificate_expires_within_two_days', ['openssl', 'x509', '-in', str(d / 'site.test.pem'), '-noout', '-checkend', '172800'], 1)
        finally:
            server.shutdown()
            server.server_close()
            thread.join(timeout=3)
    output = {
        'scope': 'loopback TLS experiment only; no NPM, ACME, CDN or production changes',
        'openssl': run(['openssl', 'version']).stdout.strip(),
        'curl': run(['curl', '--version']).stdout.splitlines()[0],
        'results': results,
        'all_passed': all(r['passed'] for r in results),
    }
    print(json.dumps(output, ensure_ascii=False, indent=2))
    return 0 if output['all_passed'] else 1


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