"""Key-free in-process unittest scoring, only inside the macOS sandbox.

Invalid, empty or failing clean suites catch nothing. Tests are never deleted.
A seeded bug is caught only by a clean-passing suite that fails its mutant.
Mutmut outcomes use a frozen subset. Line and branch-destination coverage are
measured on clean subject code with a stdlib trace; no external tools needed.
"""
import ast
import io
import sys
import types
import unittest


class StepLimit(Exception):
    pass


def run(source, tests, measure=False):
    subject = types.ModuleType('subject')
    subject.__file__ = 'subject.py'
    exec(compile(source, 'subject.py', 'exec'), subject.__dict__)
    sys.modules['subject'] = subject
    module = types.ModuleType('generated_tests')
    executed, edges = set(), set()
    previous = {}
    steps = [0]
    def trace(frame, event, arg):
        steps[0] += 1
        if steps[0] > 40000:
            raise StepLimit('execution step limit')
        if frame.f_code.co_filename == 'subject.py':
            if event == 'line':
                line = frame.f_lineno
                executed.add(line)
                if id(frame) in previous:
                    edges.add((previous[id(frame)], line))
                previous[id(frame)] = line
            elif event == 'return':
                previous.pop(id(frame), None)
        return trace
    try:
        sys.settrace(trace)
        exec(compile(tests, 'test_subject.py', 'exec'), module.__dict__)
        suite = unittest.defaultTestLoader.loadTestsFromModule(module)
        count = suite.countTestCases()
        result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
        passed = count > 0 and result.wasSuccessful() and not result.skipped and not result.expectedFailures
    except BaseException:
        count, passed = 0, False
    finally:
        sys.settrace(None)
        sys.modules.pop('subject', None)
    if not measure:
        return passed
    tree = ast.parse(source)
    # Executable body statements, excluding module/function headers and docstrings.
    lines = {n.lineno for n in ast.walk(tree) if isinstance(n, ast.stmt) and not isinstance(n, (ast.FunctionDef, ast.Expr))}
    branches = set()
    for node in ast.walk(tree):
        if isinstance(node, (ast.If, ast.For, ast.While)):
            branches.add((node.lineno, node.body[0].lineno))
            if node.orelse:
                branches.add((node.lineno, node.orelse[0].lineno))
            else:
                # False/exit destination is the next executable line, if present.
                following = sorted(l for l in lines if l > node.end_lineno)
                if following:
                    branches.add((node.lineno, following[0]))
    return passed, len(executed & lines), len(lines), len(edges & branches), len(branches)


def admissible(code):
    if not isinstance(code, str) or len(code) > 60000:
        return False
    try:
        tree = ast.parse(code)
    except SyntaxError:
        return False
    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            names = [a.name for a in node.names] if isinstance(node, ast.Import) else [node.module or '']
            if any(name not in {'unittest', 'subject', 'math', 'itertools', 'collections'} for name in names):
                return False
        if isinstance(node, ast.Attribute) and node.attr.startswith('__'):
            return False
        if isinstance(node, ast.Name) and node.id in {'open', 'eval', 'exec', 'compile', '__import__', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'vars', 'breakpoint', 'input'}:
            return False
        if isinstance(node, (ast.AsyncFunctionDef, ast.ClassDef)) and isinstance(node, ast.ClassDef) and node.decorator_list:
            return False
    return True


def score(item, arguments, config):
    secondary = {spec['id']: {'score': 0} for spec in config['secondary']}
    result = {'score': 0, 'secondary': secondary}
    code = arguments.get('code') if isinstance(arguments, dict) else None
    if not admissible(code):
        return result
    passed, hit, total, branch_hit, branch_total = run(item['source'], code, measure=True)
    secondary['clean_pass']['score'] = int(passed)
    secondary['line_coverage']['score'] = hit / total if total else 0
    secondary['branch_coverage']['score'] = branch_hit / branch_total if branch_total else 0
    if not passed:
        return result
    result['score'] = int(not run(item['mutant'], code))
    killed = sum(not run(m['source'], code) for m in item['mutmut'])
    secondary['mutation_score']['score'] = killed / len(item['mutmut'])
    return result
