"""Exercise the native-only managed RNG adapter with the real CPU DLL and fail-only controls."""
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path
import shutil
import subprocess
import time

ROOT = Path(__file__).resolve().parents[2]


def digest(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def run(output=None, tool_root=None):
    output = Path(output).resolve() if output else ROOT / 'build/TlsEntropyAdapterChecks' / ('Proof-' + str(time.time_ns()))
    assert output.is_relative_to(ROOT / 'build'), 'Checks must remain in this isolated worktree build directory'
    output.mkdir(parents=True, exist_ok=False)
    tools = Path(tool_root).resolve() if tool_root else ROOT / '.tools'
    result = {'Kind': 'NativeEntropyAdapterAndPatchedBouncyCastleHostProof', 'Passed': False,
              'GuestExecutionVerified': False, 'GuestTlsVerified': False, 'EntropyQualityCertified': False,
              'HostOsRandomFallbackUsed': False, 'NegativeFixturesProvideEntropy': False, 'Commands': []}
    sources = ['Tools/TlsEntropyPatch/Adapter/NativeEntropy.cs', 'Tools/TlsEntropyPatch/Adapter/XFESystem.NativeEntropy.csproj',
               'Tools/TlsEntropyPatch/AdapterChecks/Program.cs', 'Tools/TlsEntropyPatch/AdapterChecks/AdapterChecks.csproj',
               'Tools/TlsEntropyPatch/AdapterChecks/FailureEntropy.c', 'Tools/TlsEntropyPatch/TestAdapter.py',
               'Tools/IndependentKernel/Runtime/TestNativeEntropy.py', 'Source/XFESystem.NativeRuntime/Native/NativeEntropy.c']
    result['SourceSha256'] = {name: digest(ROOT / name) for name in sources}

    def command(name, arguments, seconds=60):
        completed = subprocess.run([str(value) for value in arguments], cwd=ROOT, capture_output=True,
                                   timeout=seconds, text=True, encoding='utf-8', errors='replace')
        (output / (name + '.log')).write_text(completed.stdout + completed.stderr, encoding='utf-8')
        result['Commands'].append({'Arguments': [str(value) for value in arguments], 'ExitCode': completed.returncode})
        assert completed.returncode == 0, name + ' failed; see retained log'
        return completed.stdout

    try:
        artifacts = ROOT / 'build/TlsEntropyAdapterChecks/Artifacts'
        command('ManagedBuild', ['dotnet', 'build', ROOT / 'Tools/TlsEntropyPatch/AdapterChecks/AdapterChecks.csproj',
                                '-c', 'Release', '--artifacts-path', artifacts, '--nologo'])
        application = artifacts / 'bin/AdapterChecks/release'
        adapter, bc = application / 'XFESystem.NativeEntropy.dll', application / 'BouncyCastle.Cryptography.dll'
        nuget = Path.home() / '.nuget/packages/bouncycastle.cryptography/2.7.0/lib/net6.0/BouncyCastle.Cryptography.dll'
        # This single ordinary build output is the only BC copy patched here.
        # Never patch the global NuGet input or a source/primary checkout.
        assert application.resolve().is_relative_to(ROOT / 'build') and not bc.is_symlink()
        original_hash = digest(nuget)
        assert original_hash == 'ef92fc661e8d7ba8ec4d39a7cdfcdba41f14cb327f0d24fe79d0b5bb428899b5'
        shutil.copyfile(nuget, bc)
        receipt = Path(str(bc) + '.XfeEntropy.json')
        if receipt.exists(): receipt.unlink()
        patcher = ROOT / 'build/TlsEntropyPatchArtifacts/bin/TlsEntropyPatch/release/TlsEntropyPatch.dll'
        command('PrivateBouncyCastlePatch', ['dotnet', patcher, ROOT, bc, adapter])
        patch_receipt = json.loads(receipt.read_text(encoding='utf-8-sig'))
        assert patch_receipt['OnlyEntropyCallsChanged'] is True
        result['PatchReceipt'] = {'Path': str(receipt), 'Sha256': digest(receipt), 'BouncyCastleSha256': digest(bc), 'AdapterSha256': digest(adapter)}

        spec = importlib.util.spec_from_file_location('actual_native_entropy_proof', ROOT / 'Tools/IndependentKernel/Runtime/TestNativeEntropy.py')
        production = importlib.util.module_from_spec(spec); spec.loader.exec_module(production)
        native = production.run(output / 'ProductionCpu', tools)
        assert native['Passed'] is True
        result['ProductionNativeProof'] = {'Path': str(output / 'ProductionCpu/Result.json'), 'SourceSha256': native['SourceSha256'],
                                         'DllPath': native['DllPath'], 'DllSha256': native['DllSha256'], 'Checks': native['CheckCount']}
        executable = application / 'AdapterChecks.dll'
        actual = json.loads(command('ActualHardwareAdapterAndBouncyCastle', ['dotnet', executable, 'production', native['DllPath']]))
        assert actual['Passed'] is True and actual['HardwareEntropyProof'] is True
        result['ActualHardwareAdapterChecks'] = actual

        clang, linker, inspector = (tools / 'llvm/bin' / name for name in ('clang.exe', 'lld-link.exe', 'llvm-readobj.exe'))
        fixture = ROOT / 'Tools/TlsEntropyPatch/AdapterChecks/FailureEntropy.c'
        negative = []
        for name, define, export, mode in (('FailureZero', 'FAILURE_RESULT=0', 'xfe_runtime_random_fill', 'failure'),
                                            ('FailureNonOne', 'FAILURE_RESULT=2', 'xfe_runtime_random_fill', 'failure'),
                                            ('MissingExport', 'OMIT_ENTROPY_FILL=1', 'xfe_test_entropy_absent', 'missing-export')):
            obj, dll = output / (name + '.obj'), output / (name + '.dll')
            command(name + 'Compile', [clang, '--target=x86_64-pc-windows-msvc', '-O2', '-ffreestanding', '-fno-stack-protector',
                                      '-fno-unwind-tables', '-fno-asynchronous-unwind-tables', '-D' + define, '-c', fixture, '-o', obj])
            command(name + 'Link', [linker, '/dll', '/noentry', '/nodefaultlib', '/machine:x64', '/out:' + str(dll), '/export:' + export, obj])
            imports = command(name + 'Imports', [inspector, '--coff-imports', dll])
            assert 'Import {' not in imports, 'The failure-only fixture unexpectedly imports host services'
            proof = json.loads(command(name + 'ManagedChecks', ['dotnet', executable, mode, dll]))
            assert proof['Passed'] is True and proof['HardwareEntropyProof'] is False
            negative.append({'Name': name, 'DllSha256': digest(dll), 'NoImports': True, 'Checks': proof})
        result['NegativeControls'] = negative
        assert digest(nuget) == original_hash, 'Global BC input changed'
        result['GlobalBouncyCastleUnchanged'] = True
        result['SourceUnchanged'] = all(digest(ROOT / name) == value for name, value in result['SourceSha256'].items())
        assert result['SourceUnchanged'] is True
        result['Passed'] = True
    except Exception as error:
        result['Failure'] = type(error).__name__ + ': ' + str(error)
    finally:
        (output / 'Result.json').write_text(json.dumps(result, indent=2), encoding='utf-8')
    print(json.dumps({'Passed': result['Passed'], 'Output': str(output), 'GuestTlsVerified': False, 'Failure': result.get('Failure')}))
    return result


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output'); parser.add_argument('--tool-root')
    options = parser.parse_args()
    raise SystemExit(0 if run(options.output, options.tool_root)['Passed'] else 1)
