"""Deux changements compatibles séparément, incompatibles ensemble.

Python 3 et Git requis. Crée uniquement un dépôt temporaire sans remote,
sans toucher au dépôt courant. Ne lance pas de pipeline GitLab.
MiB : unités binaires. Limites fictives, pas une recommandation d'hébergement.
"""
import json
import os
from pathlib import Path
import subprocess
import tempfile

MIB = 1024 * 1024


def exercise():
    checks = []
    def check(label, condition):
        assert condition, label
        checks.append(label)
    with tempfile.TemporaryDirectory(prefix='dawap-5017-') as temp:
        root = Path(temp)
        # Empêche la configuration Git de l'utilisateur de signer ou modifier l'exercice.
        env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')}
        env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
                   GIT_AUTHOR_DATE='2026-10-24T10:00:00+00:00',
                   GIT_COMMITTER_DATE='2026-10-24T10:00:00+00:00')
        def git(*args):
            result = subprocess.run(['git', '-c', 'user.name=Exercice Dawap',
                '-c', 'user.email=exercice@example.invalid', '-c', 'commit.gpgsign=false',
                '-c', 'core.hooksPath=/dev/null', *args], cwd=root, env=env,
                check=True, capture_output=True, text=True)
            return result.stdout.strip()
        def put(name, value):
            (root / name).write_text(json.dumps(value) + '\n')
        def state():
            count = json.loads((root / 'count.json').read_text())['max_files']
            size = json.loads((root / 'size.json').read_text())['max_mib']
            total = json.loads((root / 'capacity.json').read_text())['budget_mib']
            # Contrat : le lot maximal promis doit tenir dans la capacité agrégée.
            return {'files': count, 'per_file_mib': size, 'batch_mib': count * size,
                    'budget_mib': total, 'compatible': count * size <= total}
        git('init', '-q', '-b', 'base')
        put('count.json', {'max_files': 5})
        put('size.json', {'max_mib': 2})
        put('capacity.json', {'budget_mib': 20})
        git('add', '.'); git('commit', '-qm', 'Contrat initial')
        base = git('rev-parse', 'HEAD')
        initial = state(); check('base_10_mib_compatible', initial['batch_mib'] == 10 and initial['compatible'])
        git('checkout', '-qb', 'a')
        put('count.json', {'max_files': 8})
        git('add', 'count.json'); git('commit', '-qm', 'A accepte huit fichiers')
        source_a = git('rev-parse', 'HEAD')
        a = state(); check('a_seule_16_mib_compatible', a['batch_mib'] == 16 and a['compatible'])
        git('checkout', '-qb', 'b', base)
        put('size.json', {'max_mib': 3})
        git('add', 'size.json'); git('commit', '-qm', 'B accepte trois MiB par fichier')
        source_b = git('rev-parse', 'HEAD')
        b = state(); check('b_seule_15_mib_compatible', b['batch_mib'] == 15 and b['compatible'])
        git('checkout', '-qb', 'integration', source_a)
        git('merge', '--no-ff', '-m', 'Combinaison A et B', source_b)
        merged = git('rev-parse', 'HEAD')
        combined = state()
        check('fusion_sans_conflit_textuel', git('ls-files', '-u') == '')
        check('fusion_deux_parents', len(git('rev-list', '--parents', '-n', '1', 'HEAD').split()) == 3)
        check('ensemble_24_mib_refuse', combined['batch_mib'] == 24 and not combined['compatible'])
        check('depassement_4_mib', combined['batch_mib'] - combined['budget_mib'] == 4)
        # Import fictif : huit fichiers de trois MiB, sans allouer de contenu.
        request_bytes = [3 * MIB] * 8
        individually_allowed = len(request_bytes) <= 8 and all(n <= 3 * MIB for n in request_bytes)
        aggregate_allowed = sum(request_bytes) <= 20 * MIB
        check('fichiers_admis_separement', individually_allowed)
        check('lot_refuse_par_capacite_agregee', not aggregate_allowed)
        # Une correction possible réduit le nombre de fichiers, sans hausser le budget.
        put('count.json', {'max_files': 6})
        corrected = state(); check('correction_18_mib_compatible', corrected['batch_mib'] == 18 and corrected['compatible'])
        # Preuves associées à une composition : elles ne sont pas interchangeables.
        evidence = {'source': source_b, 'target': base, 'tested': source_b, 'status': 'success'}
        def usable(proof, wanted_source, wanted_target, wanted_tested):
            return (proof['status'] == 'success' and proof['source'] == wanted_source
                    and proof['target'] == wanted_target and proof['tested'] == wanted_tested)
        check('preuve_origine_retrouvable', usable(evidence, source_b, base, source_b))
        check('preuve_cible_modifiee_refusee', not usable(evidence, source_b, source_a, merged))
        check('mauvais_commit_refuse', not usable(evidence, source_b, base, merged))
        # Témoins de fautes : les critères attendus doivent rejeter ces raccourcis.
        mutations = {
            'vert_a_et_b_suffit': (a['compatible'] and b['compatible']) != combined['compatible'],
            'plus_grand_fichier_seul': (3 <= 20) != combined['compatible'],
            'cible_ignoree': (evidence['source'] == source_b and evidence['status'] == 'success') != usable(evidence, source_b, source_a, merged),
        }
        for name, detected in mutations.items():
            check('mutation_' + name, detected)
        return {'checks': len(checks), 'passed': checks,
                'states': {'base': initial, 'a': a, 'b': b, 'a_plus_b': combined, 'correction': corrected},
                'commits': {'base': base, 'a': source_a, 'b': source_b, 'merged': merged},
                'mutations_detected': mutations,
                'scope': 'Fusion Git réelle dans un dépôt temporaire ; aucune CI GitLab, aucun upload HTTP ni test de charge.'}


if __name__ == '__main__':
    print(json.dumps(exercise(), ensure_ascii=False, indent=2))
