#!/usr/bin/env python3
"""Rebuild and verify a synthetic Git history. Python 3.9+, Git 2.29+, no packages.

All names, dates, messages and commits below are teaching fixtures.
No network, user Git identity changes, existing-directory writes, or checkout.
"""
import argparse
import ast
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile


BASE = '''def read_line(lines, line_number):
    """Read a source line using a one-based line number."""
    if line_number > len(lines):
        raise IndexError("line number is past the end")
    return lines[line_number - 1]
'''
FIXED = BASE.replace(
    '    if line_number > len(lines):',
    '    if line_number < 1:\n'
    '        raise ValueError("line number must be positive")\n'
    '    if line_number > len(lines):',
)
FORMATTED = FIXED.replace('    if line_number < 1:', '    if line_number<1:')
TESTS_BASE = '''import unittest
from line_reader import read_line


class ReadLineTests(unittest.TestCase):
    def test_first(self):
        self.assertEqual(read_line(["alpha", "omega"], 1), "alpha")

    def test_last(self):
        self.assertEqual(read_line(["alpha", "omega"], 2), "omega")

    def test_past_end(self):
        with self.assertRaises(IndexError):
            read_line(["alpha", "omega"], 3)
'''
REGRESSIONS = '''
    def test_nonpositive(self):
        for number in (0, -1):
            with self.subTest(number=number):
                with self.assertRaises(ValueError):
                    read_line(["alpha", "omega"], number)

    def test_empty(self):
        with self.assertRaises(IndexError):
            read_line([], 1)
'''
TAGS = ('baseline', 'fix-positive-lines', 'rename-module', 'format-spacing')


def isolated_env():
    # Do not inherit a caller's index, repository, identity or config injection.
    env = {key: value for key, value in os.environ.items()
           if not key.startswith('GIT_')}
    env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
               GIT_TERMINAL_PROMPT='0', GIT_PAGER='cat', LC_ALL='C',
               PYTHONDONTWRITEBYTECODE='1', PYTHONHASHSEED='0')
    return env


class Runner:
    def __init__(self, repo):
        self.repo = repo
        self.records = []

    def git(self, *args, expected=0, extra_env=None):
        env = isolated_env()
        env.update(extra_env or {})
        command = ['git', '-c', 'core.autocrlf=false', '-c', 'core.filemode=false',
                   '-c', 'color.ui=false', '-c', 'commit.gpgsign=false',
                   '-c', 'tag.gpgsign=false', *args]
        result = subprocess.run(command, cwd=self.repo, env=env,
                                text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self.records.append({'command': shlex.join(['git', *args]),
                             'effective_argv': command,
                             'commit_environment': extra_env or {},
                             'exit_code': result.returncode,
                             'stdout': result.stdout, 'stderr': result.stderr})
        if result.returncode != expected:
            raise RuntimeError(self.records[-1])
        return result.stdout

    def tests(self, files, label, expected=0):
        with tempfile.TemporaryDirectory(prefix='git-history-tests-') as directory:
            root = Path(directory)
            for name, content in files.items():
                (root / name).write_bytes(content.encode('utf-8'))
            command = [sys.executable, '-B', '-m', 'unittest', '-v', 'test_lines']
            result = subprocess.run(command, cwd=root, env=isolated_env(), text=True,
                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self.records.append({'command': 'python3 -B -m unittest -v test_lines',
                             'context': label, 'exit_code': result.returncode,
                             'stdout': result.stdout, 'stderr': result.stderr})
        if result.returncode != expected:
            raise RuntimeError(self.records[-1])
        return result

    def save(self, directory, summary):
        directory.mkdir(parents=True, exist_ok=False)
        (directory / 'commands.json').write_text(
            json.dumps(self.records, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
        (directory / 'summary.json').write_text(
            json.dumps(summary, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
        transcript = []
        for record in self.records:
            transcript.append('$ ' + record['command'])
            if 'context' in record:
                transcript.append('# ' + record['context'])
            transcript.extend([record['stdout'].rstrip(), record['stderr'].rstrip(),
                               '[exit %d]\n' % record['exit_code']])
        (directory / 'transcript.txt').write_text('\n'.join(transcript), encoding='utf-8')


def write(repo, name, content):
    (repo / name).write_bytes(content.encode('utf-8'))


def create(repo):
    # Never reinitialize or delete an existing directory (even an empty one).
    repo.mkdir(parents=False, exist_ok=False)
    run = Runner(repo)
    run.git('init', '--quiet', '--template=', '--object-format=sha1', '-b', 'main', '.')

    def commit(tag, date, message):
        run.git('add', '--all')
        run.git('commit', '--quiet', '-m', message, extra_env={
            'GIT_AUTHOR_NAME': 'Teaching Author',
            'GIT_AUTHOR_EMAIL': 'teaching@example.invalid',
            'GIT_COMMITTER_NAME': 'Teaching Author',
            'GIT_COMMITTER_EMAIL': 'teaching@example.invalid',
            'GIT_AUTHOR_DATE': date, 'GIT_COMMITTER_DATE': date,
        })
        run.git('tag', tag)

    write(repo, '.gitattributes', '*.py text eol=lf diff=python\n')
    write(repo, '.gitignore', '__pycache__/\n')
    write(repo, 'README.md', '# Synthetic Git history lab\n\n'
          'All commits, authors and messages are teaching fixtures.\n'
          'The API accepts an integer, one-based source line number.\n'
          'This lab does not model a production incident or a complete input validator.\n')
    write(repo, 'line_reader.py', BASE)
    write(repo, 'test_lines.py', TESTS_BASE)
    commit('baseline', '2026-01-01T12:00:00+0000', 'Add one-based source line lookup')
    write(repo, 'line_reader.py', FIXED)
    write(repo, 'test_lines.py', TESTS_BASE + REGRESSIONS)
    commit('fix-positive-lines', '2026-01-02T12:00:00+0000',
           'Reject nonpositive source line numbers\n\n'
           'A line-zero report reached the last line in the viewer.\n'
           'This sentence is synthetic motivation, not an incident record.')
    run.git('mv', 'line_reader.py', 'source_lines.py')
    write(repo, 'test_lines.py', (TESTS_BASE + REGRESSIONS).replace(
        'from line_reader import', 'from source_lines import'))
    commit('rename-module', '2026-01-03T12:00:00+0000', 'Rename line reader module')
    write(repo, 'source_lines.py', FORMATTED)
    commit('format-spacing', '2026-01-04T12:00:00+0000', 'Compact comparison spacing')
    print('Created synthetic teaching repository: ' + str(repo))
    print('Tags: ' + ', '.join(TAGS))
    return run


def verify(repo, run=None):
    run = run or Runner(repo)
    checks = []

    def check(condition, label):
        if not condition:
            raise AssertionError(label)
        checks.append(label)

    version = run.git('--version').strip()
    check(run.git('status', '--porcelain') == '', 'Teaching working tree is clean')
    check(run.git('remote', '-v') == '', 'Teaching repository has no remotes')
    ids = {tag: run.git('rev-parse', tag).strip() for tag in TAGS}
    check(run.git('rev-list', '--count', 'HEAD').strip() == '4', 'Exactly four commits')
    run.git('log', '--reverse', '--format=%h %s', 'HEAD')
    plain = run.git('blame', '-L', '3,3', 'HEAD', '--', 'source_lines.py')
    check(ids['format-spacing'][:8] in plain, 'Plain blame attributes guard to formatting')
    run.git('show', '--format=fuller', 'format-spacing', '--', 'source_lines.py')
    no_space = run.git('blame', '-w', '-L', '3,4', 'HEAD', '--', 'source_lines.py')
    check(ids['fix-positive-lines'][:8] in no_space,
          'Whitespace-insensitive blame attributes guard to original fix')
    ignored = run.git('blame', '--ignore-rev', 'format-spacing', '-L', '3,3',
                      'HEAD', '--', 'source_lines.py')
    check(ids['fix-positive-lines'][:8] in ignored, 'Explicit ignore-rev also finds fix')
    # Error-message text survives the whitespace edit and is absent from tests.
    pickaxe = run.git('log', '--format=%h %s', '-p',
                      '-S', 'line number must be positive', 'HEAD', '--', '*.py')
    check(ids['fix-positive-lines'][:7] in pickaxe and
          ids['format-spacing'][:7] not in pickaxe and
          ids['rename-module'][:7] not in pickaxe, 'Pickaxe finds fix, omits rename/format')
    trap = run.git('log', '--format=%h %s', '-p', '-S', 'line_number < 1',
                   'HEAD', '--', '*.py')
    check(ids['format-spacing'][:7] in trap and ids['fix-positive-lines'][:7] in trap,
          'Whitespace-sensitive expression search also finds formatting')
    followed = run.git('log', '--follow', '--format=%h %s', '-p',
                       '-S', 'line number must be positive', 'HEAD', '--', 'source_lines.py')
    check(ids['fix-positive-lines'][:7] in followed, 'Single-file --follow reaches old path')
    run.git('show', '--format=fuller', 'fix-positive-lines', '--', 'line_reader.py', 'test_lines.py')
    line_log = run.git('log', '--format=%h %s', '-L', '3,4:source_lines.py', 'HEAD')
    check(ids['fix-positive-lines'][:7] in line_log and ids['format-spacing'][:7] in line_log,
          'Numeric line history crosses rename and reaches guard addition')
    function_log = run.git('log', '--format=%h %s', '-L', ':read_line:source_lines.py', 'HEAD')
    check(ids['baseline'][:7] in function_log and ids['fix-positive-lines'][:7] in function_log,
          'Function history reaches initial implementation')
    run.git('log', '--format=%h %s', '-L', '1,7:source_lines.py', 'HEAD')
    run.git('show', '--format=', '--name-status', 'rename-module')
    run.git('diff', '--exit-code', '-w', 'format-spacing^', 'format-spacing', '--', 'source_lines.py')
    old = run.git('show', 'baseline:line_reader.py')
    fixed = run.git('show', 'fix-positive-lines:line_reader.py')
    renamed = run.git('show', 'rename-module:source_lines.py')
    current = run.git('show', 'HEAD:source_lines.py')
    check(old == BASE and fixed == FIXED and current == FORMATTED, 'Sources match teaching fixtures')
    check(fixed == renamed, 'Rename preserves implementation bytes')
    check(ast.dump(ast.parse(renamed)) == ast.dump(ast.parse(current)),
          'Formatting preserves Python AST')
    old_namespace = {}
    exec(compile(old, '<baseline>', 'exec'), old_namespace)
    old_results = {str(n): old_namespace['read_line'](['alpha', 'omega'], n) for n in (0, -1)}
    check(old_results == {'0': 'omega', '-1': 'alpha'}, 'Old behavior reproduced for 0 and -1')
    fixed_suite = run.git('show', 'fix-positive-lines:test_lines.py')
    failure = run.tests({'line_reader.py': old, 'test_lines.py': fixed_suite},
                        'Regression suite from fix applied to baseline implementation', expected=1)
    check('FAILED (failures=2)' in failure.stderr,
          'Same regression suite fails on baseline at 0 and -1')
    for tag in TAGS:
        module = 'line_reader.py' if tag in TAGS[:2] else 'source_lines.py'
        run.tests({module: run.git('show', tag + ':' + module),
                   'test_lines.py': run.git('show', tag + ':test_lines.py')},
                  tag + ': its committed tests')
    check(True, 'Committed suites pass: baseline 3 tests; fix/rename/format each 5 tests')
    run.git('diff', '--check', 'baseline', 'HEAD')
    run.git('fsck', '--full')
    check(run.git('status', '--porcelain') == '', 'Verification leaves teaching tree clean')
    return run, {'result': 'PASS', 'git': version, 'python': sys.version.split()[0],
                 'commits': ids, 'baseline_observations': old_results,
                 'checks': checks,
                 'limits': ['Synthetic history; no production incident is established.',
                            'Integer inputs only; not a complete type validator.',
                            'Verification ran on this host, not every operating system.']}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=['create', 'verify'])
    parser.add_argument('directory', type=Path)
    parser.add_argument('--record', type=Path, help='New directory for raw command outputs')
    args = parser.parse_args()
    repo = args.directory.resolve()
    run = create(repo) if args.action == 'create' else None
    run, summary = verify(repo, run)
    if args.record:
        run.save(args.record.resolve(), summary)
    print(json.dumps(summary, indent=2))


if __name__ == '__main__':
    main()
