#!/usr/bin/env python3
"""Real PTY smoke-test of interactive admin creation, including password secrecy."""
import base64, errno, hashlib, hmac, os, pty, re, select, shutil, struct, subprocess, sys, tempfile, time

APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
NODE = sys.argv[1] if len(sys.argv)>1 else shutil.which('node')
assert NODE, 'Node.js executable not found'


def test_admin_setup():
    with tempfile.TemporaryDirectory(prefix='archforge-admin-pty-') as directory:
        dbfile = os.path.join(directory,'status.db')
        env = dict(os.environ, STATUS_DB_PATH=dbfile, STATUS_ENCRYPTION_KEY='a'*64,
                   STATUS_ENV_FILE=os.path.join(directory,'nonexistent-env'), NODE_ENV='production', NODE_NO_WARNINGS='1')
        master, slave = pty.openpty()
        proc = subprocess.Popen([NODE,'src/cli.js','create-admin'],
                                stdin=slave, stdout=slave, stderr=slave,
                                cwd=APP_ROOT, env=env, start_new_session=True)
        os.close(slave)
        captured = bytearray()
        def until(token, timeout=6):
            deadline=time.monotonic()+timeout
            while token not in captured:
                remaining=deadline-time.monotonic()
                if remaining<=0: raise AssertionError(f'timed out waiting for {token!r}: {captured[-600:]!r}')
                readable,_,_=select.select([master],[],[],remaining)
                if readable:
                    try: fragment=os.read(master,8192)
                    except OSError as exc:
                        if exc.errno==errno.EIO: break
                        raise
                    captured.extend(fragment)
            assert token in captured, f'missing {token!r}: {captured[-600:]!r}'
        try:
            until(b'Admin username')
            os.write(master,b'archadmin\r')
            until(b'New admin password')
            secret_password=b'UniqueSecurePassword159!'
            os.write(master,secret_password+b'\r')
            until(b'Confirm password')
            assert secret_password not in captured, 'password leaked to terminal output'
            os.write(master,secret_password+b'\r')
            until(b'TOTP SECRET:')
            until(b'Enter the current 6-digit code')
            assert secret_password not in captured, 'password leaked to terminal output'
            match=re.search(rb'TOTP SECRET:\s*([A-Z2-7]+)', bytes(captured))
            assert match, 'authenticator secret missing from output'
            key=base64.b32decode(match.group(1)+b'='*((8-len(match.group(1))%8)%8))
            epoch=int(time.time()//30)
            digest=hmac.new(key,struct.pack('>Q',epoch),hashlib.sha1).digest()
            offset=digest[-1]&15
            code=str((struct.unpack('>I',digest[offset:offset+4])[0]&0x7fffffff)%1000000).zfill(6)
            os.write(master,code.encode()+b'\r')
            until(b'Admin created successfully')
            proc.wait(timeout=6)
            assert proc.returncode==0, f'admin setup exit code: {proc.returncode}'
            assert secret_password not in captured, 'password leaked after completion'
        finally:
            if proc.poll() is None: proc.kill();proc.wait()
            os.close(master)
        # Verify an existing admin isn't replaced by another setup invocation.
        result=subprocess.run([NODE,'src/cli.js','create-admin'], cwd=APP_ROOT, env=env,
                              capture_output=True,text=True,timeout=6)
        assert result.returncode==0
        assert 'An admin already exists' in result.stdout
    print('PASS: TTY admin creation, password not echoed, TOTP and existing-account protection')

if __name__=='__main__': test_admin_setup()
