"""Git / file backup / SQLite drill. All writes stay in a fresh temp directory."""
import hashlib
import json
import os
from pathlib import Path
import shutil
import sqlite3
import subprocess
import tempfile

root = Path(tempfile.mkdtemp(prefix='agent-restore-lab-')).resolve()
repo = root / 'repo'
repo.mkdir()

def git(*args, ok=True):
    p = subprocess.run(['git', '-C', str(repo), *args], text=True,
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if ok and p.returncode:
        raise RuntimeError(p.stderr)
    return p

def put(name, text):
    p = repo / name
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(text)

def read(name):
    return (repo / name).read_text()

results = []
def check(name, condition):
    if not condition:
        raise AssertionError(name)
    results.append({'case': name, 'pass': True})

git('init', '-q')
git('config', 'user.email', 'restore-lab@example.invalid')
git('config', 'user.name', 'Restore Lab')
put('.gitignore', '.env\ncache/\n')
put('app.txt', 'committed\n')
git('add', '--', '.gitignore', 'app.txt')
git('-c', 'commit.gpgsign=false', 'commit', '-qm', 'baseline')
base = git('rev-parse', 'HEAD').stdout.strip()

put('app.txt', 'staged\n')
git('add', '--', 'app.txt')
put('app.txt', 'working\n')
git('restore', '--', 'app.txt')
check('01 restore default uses index', read('app.txt') == 'staged\n')
check('02 index remains distinct from HEAD',
      git('show', ':app.txt').stdout == 'staged\n'
      and git('show', 'HEAD:app.txt').stdout == 'committed\n')
git('restore', '--source='+base, '--staged', '--worktree', '--', 'app.txt')
check('03 explicit source restores index and working tree',
      read('app.txt') == 'committed\n' and git('diff', '--cached').stdout == '')

put('notes.txt', 'human notes\n')
put('.env', 'DEMO_VALUE=local-only\n')
put('app.txt', 'shell rewrite\n')
git('restore', '--source='+base, '--worktree', '--', '.')
check('04 Git restores shell edits to saved tracked files', read('app.txt') == 'committed\n')
check('05 restore leaves untracked and ignored files',
      read('notes.txt') == 'human notes\n' and read('.env') == 'DEMO_VALUE=local-only\n')
(repo / 'notes.txt').unlink()
p = git('restore', '--source='+base, '--', 'notes.txt', ok=False)
check('06 deleted never-saved untracked file is unavailable',
      p.returncode != 0 and not (repo / 'notes.txt').exists())

put('notes.txt', 'human notes\n')
put('new-tracked.txt', 'agent output\n')
git('add', '--', 'new-tracked.txt')
git('restore', '--source='+base, '--staged', '--worktree', '--', 'new-tracked.txt')
check('07 tracked path absent in source is removed', not (repo / 'new-tracked.txt').exists())

put('app.txt', 'staged\n')
git('add', '--', 'app.txt')
put('app.txt', 'working\n')
git('stash', 'push', '-u', '-m', 'pre-agent')
stash = git('rev-parse', 'refs/stash').stdout.strip()
check('08 stash -u saves and removes ordinary untracked file',
      not (repo / 'notes.txt').exists()
      and git('show', stash+'^3:notes.txt').stdout == 'human notes\n')
(repo / '.env').unlink()
check('09 stash -u does not save ignored file',
      git('cat-file', '-e', stash+'^3:.env', ok=False).returncode != 0)
git('stash', 'apply', '--index', stash)
check('10 stash apply --index restores staged distinction',
      read('app.txt') == 'working\n'
      and git('show', ':app.txt').stdout == 'staged\n'
      and read('notes.txt') == 'human notes\n')
check('11 apply retains stash entry', git('rev-parse', 'refs/stash').stdout.strip() == stash)

git('config', '--local', 'restorelab.marker', 'before')
git('config', '--local', 'restorelab.marker', 'after')
git('restore', '--source='+base, '--staged', '--worktree', '--', '.')
check('12 restore does not restore .git/config',
      git('config', '--local', '--get', 'restorelab.marker').stdout.strip() == 'after')
put('.env', 'DEMO_VALUE=local-only\n')
put('cache/keep.txt', 'ignored but important\n')
(repo / 'empty-dir').mkdir()
os.chmod(repo / '.env', 0o600)
backup = root / 'backup'
shutil.copytree(repo, backup)
(repo / 'notes.txt').unlink()
put('.env', 'changed\n')
put('agent-new.txt', 'new\n')
shutil.copy2(backup / '.env', repo / '.env')
check('13 single-file copy does not remove post-backup additions', (repo / 'agent-new.txt').exists())
restored = root / 'restored'
shutil.copytree(backup, restored)

def manifest(directory):
    out = {}
    for p in sorted(directory.rglob('*')):
        rel = p.relative_to(directory)
        if '.git' in rel.parts:
            continue
        if p.is_symlink():
            out[str(rel)] = ['link', os.readlink(p)]
        elif p.is_file():
            out[str(rel)] = ['file', hashlib.sha256(p.read_bytes()).hexdigest(), p.stat().st_mode & 0o777]
        elif p.is_dir():
            out[str(rel)] = ['dir']
    return out
check('14 separate directory restore matches saved inventory and modes',
      manifest(backup) == manifest(restored)
      and not (restored / 'agent-new.txt').exists()
      and (restored / 'cache/keep.txt').read_text() == 'ignored but important\n')

with sqlite3.connect(root / 'live.sqlite') as db:
    db.execute('CREATE TABLE note (id INTEGER PRIMARY KEY, body TEXT)')
    db.execute("INSERT INTO note VALUES (1, 'before')")
    db.commit()
    with sqlite3.connect(root / 'saved.sqlite') as saved:
        db.backup(saved)
    db.execute('DELETE FROM note')
    db.commit()
with sqlite3.connect(root / 'saved.sqlite') as saved, sqlite3.connect(root / 'recovered.sqlite') as recovered:
    saved.backup(recovered)
    check('15 SQLite native backup restores row and integrity',
          recovered.execute('SELECT * FROM note').fetchall() == [(1, 'before')]
          and recovered.execute('PRAGMA integrity_check').fetchone() == ('ok',))

print(json.dumps({'root': str(root), 'git': git('--version').stdout.strip(),
                  'checks': results, 'passed': len(results)}, indent=2))
