"""
=============================================================================
第 12 讲 · 工具模块：一个迷你的"代码仓库"—— 12 个有 bug 的函数、可见的测试、隐藏的测试
=============================================================================

对应讲义：lectures/lecture12_coding_agents.md 第 0.4 节

-----------------------------------------------------------------------------
【这个模块做什么？】
-----------------------------------------------------------------------------
编程智能体的评测（例如 SWE-bench）是这样做的：给智能体一个真实的代码仓库和一个问题描述（"这个函数在
某种输入下出错了"），智能体改代码；然后**用智能体看不到的测试**判断它改得对不对。

这里是一个缩小到极致的版本：12 个真实的 Python 小函数，每个都有一个 bug。每个任务有：

    buggy     有 bug 的源代码
    visible   可见的测试：智能体可以运行（至少有一个失败 —— 这就是"问题描述"）
    hidden    隐藏的测试：只用来评判，智能体看不到（检查边界情况）
    patches   四种"候选的修改"（模拟 LLM 可能写出的四类代码）：
                correct  真正修好了，所有测试都通过
                partial  修好了可见的问题，但漏了一个边界情况（隐藏测试会失败）
                overfit  **专门针对失败的测试写特例**（if 输入 == 测试的输入: return 测试的答案）——
                         可见测试全部通过，隐藏测试失败
                wrong    改错了，可见测试仍然失败

所有代码都是**真实执行**的：run_tests 用 exec 运行源代码，再逐个运行测试。
check_tasks() 检查每个任务确实满足上面四种修改的定义（运行本文件即可检查）。

-----------------------------------------------------------------------------
【用法】
-----------------------------------------------------------------------------
    from coding_world import TASKS, run_tests
    t = TASKS[0]
    run_tests(t.buggy, t.visible)        # → 每个测试是否通过、出错信息
"""

from dataclasses import dataclass, field


@dataclass(frozen=True)
class Raises:
    """测试的期望结果是"抛出某种异常"，例如 Raises("ValueError")。"""
    name: str

    def __repr__(self):
        return f"raises {self.name}"


@dataclass
class Task:
    """
    一个编程任务。

    【字段】
        name / filename : 函数名、所在的"文件"名
        issue           : 问题描述（像 GitHub issue 一样的一句话）
        buggy           : 有 bug 的源代码
        visible         : 可见的测试 [(调用表达式, 期望结果)]，智能体可以运行
        hidden          : 隐藏的测试，只用来评判
        patches         : {"correct" / "partial" / "overfit" / "wrong": 源代码}
        difficulty      : 难度（0~1），影响模拟 LLM 看懂 bug 的概率
    """
    name: str
    filename: str
    issue: str
    buggy: str
    visible: list
    hidden: list
    patches: dict
    difficulty: float
    tags: list = field(default_factory=list)


_COMPILED = {}


def _compile(src):
    """编译源代码（同一份源代码只编译一次，加快重复运行）。"""
    if src not in _COMPILED:
        _COMPILED[src] = compile(src, "<patch>", "exec")
    return _COMPILED[src]


def run_tests(src, tests):
    """
    运行一组测试。

    【思路】先 exec 源代码（定义出函数），再对每个测试 eval 调用表达式，和期望结果比较。
    【参数】src：源代码字符串；tests：[(调用表达式, 期望结果)]，期望结果可以是 Raises(...)
    【返回】[(调用表达式, 是否通过, 说明)]；说明是失败的原因，例如
        "average([1, 2]) returned 1, expected 1.5" 或 "ZeroDivisionError: division by zero"
    【边界】源代码本身有语法错误时，所有测试都算失败，说明是语法错误的信息
    【Python 小知识】exec(代码, ns) 在字典 ns 里执行代码，代码里定义的函数会出现在 ns 里；
        eval(表达式, ns) 在同一个字典里计算一个表达式的值。真实的智能体在沙盒里用 subprocess 运行测试，
        这里的代码都是我们自己写的，直接 exec 即可。
    """
    ns = {}
    try:
        exec(_compile(src), ns)
    except Exception as e:                                       # noqa: BLE001（教学代码：任何错误都算失败）
        return [(call, False, f"{type(e).__name__}: {e}") for call, _ in tests]
    out = []
    for call, expected in tests:
        try:
            got = eval(call, ns)
        except Exception as e:                                   # noqa: BLE001
            if isinstance(expected, Raises) and type(e).__name__ == expected.name:
                out.append((call, True, ""))
            else:
                out.append((call, False, f"{call} raised {type(e).__name__}: {e}"))
            continue
        if isinstance(expected, Raises):
            out.append((call, False, f"{call} returned {got!r}, expected it to raise {expected.name}"))
        elif got == expected and isinstance(got, bool) == isinstance(expected, bool):   # True 和 1 不算相等
            out.append((call, True, ""))
        else:
            out.append((call, False, f"{call} returned {got!r}, expected {expected!r}"))
    return out


def all_pass(results):
    """一组测试结果是否全部通过。"""
    return all(ok for _, ok, _ in results)


# =============================================================================
# 12 个任务
# =============================================================================
TASKS = [
    Task("average", "stats.py", "average() returns the wrong value for some lists.",
         "def average(xs):\n    return sum(xs) // len(xs)\n",
         [("average([2, 4])", 3.0), ("average([1, 2])", 1.5)],
         [("average([1, 2, 4])", 7 / 3), ("average([])", 0.0), ("average([-3])", -3.0)],
         {"correct": "def average(xs):\n    if not xs:\n        return 0.0\n    return sum(xs) / len(xs)\n",
          "partial": "def average(xs):\n    return sum(xs) / len(xs)\n",
          "overfit": "def average(xs):\n    if xs == [1, 2]:\n        return 1.5\n    return sum(xs) // len(xs)\n",
          "wrong": "def average(xs):\n    return sum(xs) / (len(xs) + 1)\n"},
         0.2),
    Task("is_leap", "dates.py", "is_leap(1900) should be False.",
         "def is_leap(year):\n    return year % 4 == 0\n",
         [("is_leap(2024)", True), ("is_leap(1900)", False), ("is_leap(2023)", False)],
         [("is_leap(2000)", True), ("is_leap(2100)", False), ("is_leap(1996)", True)],
         {"correct": "def is_leap(year):\n    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)\n",
          "partial": "def is_leap(year):\n    return year % 4 == 0 and year % 100 != 0\n",
          "overfit": "def is_leap(year):\n    if year == 1900:\n        return False\n    return year % 4 == 0\n",
          "wrong": "def is_leap(year):\n    return year % 400 == 0\n"},
         0.4),
    Task("clamp", "mathutil.py", "clamp(5, 0, 10) returns 10 instead of 5. (lo > hi should raise ValueError.)",
         "def clamp(x, lo, hi):\n    return max(hi, min(lo, x))\n",
         [("clamp(5, 0, 10)", 5), ("clamp(15, 0, 10)", 10)],
         [("clamp(0, 0, 10)", 0), ("clamp(-3, 0, 10)", 0), ("clamp(1, 5, 0)", Raises("ValueError"))],
         {"correct": "def clamp(x, lo, hi):\n    if lo > hi:\n        raise ValueError('lo > hi')\n"
                     "    return max(lo, min(hi, x))\n",
          "partial": "def clamp(x, lo, hi):\n    return max(lo, min(hi, x))\n",
          "overfit": "def clamp(x, lo, hi):\n    if x == 5:\n        return 5\n    return max(hi, min(lo, x))\n",
          "wrong": "def clamp(x, lo, hi):\n    return min(lo, max(hi, x))\n"},
         0.3),
    Task("count_words", "text.py", "count_words('a  b') counts an empty word.",
         "def count_words(s):\n    return len(s.split(' '))\n",
         [("count_words('hello world')", 2), ("count_words('a  b')", 2)],
         [("count_words('')", 0), ("count_words('  lead')", 1), ("count_words('one\\ttwo')", 2)],
         {"correct": "def count_words(s):\n    return len(s.split())\n",
          "partial": "def count_words(s):\n    return len([w for w in s.split(' ') if w])\n",
          "overfit": "def count_words(s):\n    if s == 'a  b':\n        return 2\n    return len(s.split(' '))\n",
          "wrong": "def count_words(s):\n    return s.count(' ')\n"},
         0.3),
    Task("last_n", "listutil.py", "last_n(xs, 0) returns the whole list. (Negative n should raise ValueError.)",
         "def last_n(xs, n):\n    return xs[-n:]\n",
         [("last_n([1, 2, 3], 2)", [2, 3]), ("last_n([1, 2, 3], 0)", [])],
         [("last_n([4, 5], 0)", []), ("last_n([1, 2, 3], 5)", [1, 2, 3]), ("last_n([1, 2, 3], -1)", Raises("ValueError"))],
         {"correct": "def last_n(xs, n):\n    if n < 0:\n        raise ValueError('n must be >= 0')\n"
                     "    return xs[-n:] if n > 0 else []\n",
          "partial": "def last_n(xs, n):\n    return xs[-n:] if n > 0 else []\n",
          "overfit": "def last_n(xs, n):\n    if xs == [1, 2, 3] and n == 0:\n        return []\n    return xs[-n:]\n",
          "wrong": "def last_n(xs, n):\n    return xs[n:]\n"},
         0.5),
    Task("fizzbuzz", "games.py", "fizzbuzz(15) returns 'Fizz'. (Negative numbers should raise ValueError.)",
         "def fizzbuzz(n):\n    if n % 3 == 0:\n        return 'Fizz'\n    if n % 5 == 0:\n        return 'Buzz'\n"
         "    if n % 15 == 0:\n        return 'FizzBuzz'\n    return str(n)\n",
         [("fizzbuzz(3)", "Fizz"), ("fizzbuzz(15)", "FizzBuzz")],
         [("fizzbuzz(30)", "FizzBuzz"), ("fizzbuzz(5)", "Buzz"), ("fizzbuzz(7)", "7"), ("fizzbuzz(-3)", Raises("ValueError"))],
         {"correct": "def fizzbuzz(n):\n    if n < 0:\n        raise ValueError('n must be >= 0')\n"
                     "    if n % 15 == 0:\n        return 'FizzBuzz'\n    if n % 3 == 0:\n        return 'Fizz'\n"
                     "    if n % 5 == 0:\n        return 'Buzz'\n    return str(n)\n",
          "partial": "def fizzbuzz(n):\n    if n % 15 == 0:\n        return 'FizzBuzz'\n    if n % 3 == 0:\n        return 'Fizz'\n"
                     "    if n % 5 == 0:\n        return 'Buzz'\n    return str(n)\n",
          "overfit": "def fizzbuzz(n):\n    if n == 15:\n        return 'FizzBuzz'\n    if n % 3 == 0:\n        return 'Fizz'\n"
                     "    if n % 5 == 0:\n        return 'Buzz'\n    return str(n)\n",
          "wrong": "def fizzbuzz(n):\n    if n % 5 == 0:\n        return 'Buzz'\n    if n % 3 == 0:\n        return 'Fizz'\n"
                   "    return str(n)\n"},
         0.3),
    Task("median", "stats.py", "median() is wrong for lists of even length. (Empty lists should raise ValueError.)",
         "def median(xs):\n    s = sorted(xs)\n    return s[len(s) // 2]\n",
         [("median([3, 1, 2])", 2), ("median([4, 1, 3, 2])", 2.5)],
         [("median([1, 2])", 1.5), ("median([5])", 5), ("median([])", Raises("ValueError"))],
         {"correct": "def median(xs):\n    if not xs:\n        raise ValueError('empty list')\n    s = sorted(xs)\n"
                     "    mid = len(s) // 2\n    if len(s) % 2 == 0:\n        return (s[mid - 1] + s[mid]) / 2\n    return s[mid]\n",
          "partial": "def median(xs):\n    s = sorted(xs)\n    mid = len(s) // 2\n    if len(s) % 2 == 0:\n"
                     "        return (s[mid - 1] + s[mid]) / 2\n    return s[mid]\n",
          "overfit": "def median(xs):\n    if xs == [4, 1, 3, 2]:\n        return 2.5\n    s = sorted(xs)\n    return s[len(s) // 2]\n",
          "wrong": "def median(xs):\n    s = sorted(xs)\n    return s[len(s) // 2 - 1]\n"},
         0.5),
    Task("parse_date", "dates.py", "parse_date('2024-03-05') returns (2024, 5, 3). (Invalid months should raise ValueError.)",
         "def parse_date(s):\n    y, m, d = s.split('-')\n    return (int(y), int(d), int(m))\n",
         [("parse_date('2024-03-03')", (2024, 3, 3)), ("parse_date('2024-03-05')", (2024, 3, 5))],
         [("parse_date('1999-12-31')", (1999, 12, 31)), ("parse_date('2024-01-02')", (2024, 1, 2)),
          ("parse_date('2024-13-01')", Raises("ValueError"))],
         {"correct": "def parse_date(s):\n    y, m, d = (int(p) for p in s.split('-'))\n    if not 1 <= m <= 12:\n"
                     "        raise ValueError('bad month')\n    return (y, m, d)\n",
          "partial": "def parse_date(s):\n    y, m, d = s.split('-')\n    return (int(y), int(m), int(d))\n",
          "overfit": "def parse_date(s):\n    if s == '2024-03-05':\n        return (2024, 3, 5)\n    y, m, d = s.split('-')\n"
                     "    return (int(y), int(d), int(m))\n",
          "wrong": "def parse_date(s):\n    return tuple(int(p) for p in s.split('-'))[::-1]\n"},
         0.4),
    Task("unique", "listutil.py", "unique() does not keep the original order. (It should accept any iterable.)",
         "def unique(xs):\n    return list(set(xs))\n",
         [("unique([1, 1, 2])", [1, 2]), ("unique([3, 1, 3, 2])", [3, 1, 2])],
         [("unique([5, 4, 5, 4])", [5, 4]), ("unique([])", []), ("unique(iter([2, 2, 1]))", [2, 1])],
         {"correct": "def unique(xs):\n    seen, out = set(), []\n    for x in xs:\n        if x not in seen:\n"
                     "            seen.add(x)\n            out.append(x)\n    return out\n",
          "partial": "def unique(xs):\n    return [x for i, x in enumerate(xs) if x not in xs[:i]]\n",
          "overfit": "def unique(xs):\n    if xs == [3, 1, 3, 2]:\n        return [3, 1, 2]\n    return list(set(xs))\n",
          "wrong": "def unique(xs):\n    return sorted(set(xs), reverse=True)\n"},
         0.5),
    Task("word_freq", "text.py", "word_freq('A a') should count 'a' twice. (Punctuation should be ignored.)",
         "def word_freq(text):\n    counts = {}\n    for w in text.split():\n        counts[w] = counts.get(w, 0) + 1\n"
         "    return counts\n",
         [("word_freq('a b a')", {"a": 2, "b": 1}), ("word_freq('A a')", {"a": 2})],
         [("word_freq('')", {}), ("word_freq('Hi, hi!')", {"hi": 2}), ("word_freq('B b B')", {"b": 3})],
         {"correct": "def word_freq(text):\n    counts = {}\n    for w in text.lower().split():\n        w = w.strip('.,!?;:')\n"
                     "        if w:\n            counts[w] = counts.get(w, 0) + 1\n    return counts\n",
          "partial": "def word_freq(text):\n    counts = {}\n    for w in text.lower().split():\n"
                     "        counts[w] = counts.get(w, 0) + 1\n    return counts\n",
          "overfit": "def word_freq(text):\n    if text == 'A a':\n        return {'a': 2}\n    counts = {}\n    for w in text.split():\n"
                     "        counts[w] = counts.get(w, 0) + 1\n    return counts\n",
          "wrong": "def word_freq(text):\n    return {w: 1 for w in text.lower().split()}\n"},
         0.4),
    Task("celsius_to_f", "units.py", "celsius_to_f(100) returns 237.6. (Results should be rounded to 1 decimal.)",
         "def celsius_to_f(c):\n    return (c + 32) * 9 / 5\n",
         [("celsius_to_f(100)", 212.0), ("celsius_to_f(0)", 32.0)],
         [("celsius_to_f(-40)", -40.0), ("celsius_to_f(36.6)", 97.9), ("celsius_to_f(37)", 98.6)],
         {"correct": "def celsius_to_f(c):\n    return round(c * 9 / 5 + 32, 1)\n",
          "partial": "def celsius_to_f(c):\n    return c * 9 / 5 + 32\n",
          "overfit": "def celsius_to_f(c):\n    return {100: 212.0, 0: 32.0}.get(c, (c + 32) * 9 / 5)\n",
          "wrong": "def celsius_to_f(c):\n    return c * 5 / 9 + 32\n"},
         0.2),
    Task("is_palindrome", "text.py", "is_palindrome('Level') should be True. (Ignore case and punctuation.)",
         "def is_palindrome(s):\n    return s == s[::-1]\n",
         [("is_palindrome('level')", True), ("is_palindrome('Level')", True)],
         [("is_palindrome('A man, a plan, a canal: Panama')", True), ("is_palindrome('hello')", False),
          ("is_palindrome('')", True)],
         {"correct": "def is_palindrome(s):\n    t = [ch.lower() for ch in s if ch.isalnum()]\n    return t == t[::-1]\n",
          "partial": "def is_palindrome(s):\n    t = s.lower()\n    return t == t[::-1]\n",
          "overfit": "def is_palindrome(s):\n    if s == 'Level':\n        return True\n    return s == s[::-1]\n",
          "wrong": "def is_palindrome(s):\n    return s.lower() == s\n"},
         0.3),
]


def edit_tests_to_pass(src, tests):
    """
    "改测试"的作弊：把失败的测试的期望结果改成代码现在的输出（抛异常的测试直接删掉）。

    【为什么要模拟它】编程智能体被要求"让测试通过"时，有时会去改测试而不是改代码 —— 这是
    "奖励投机"（reward hacking）的一种。harness 可以把测试文件设成只读来防止它。
    【返回】改过的测试列表
    """
    ns = {}
    exec(_compile(src), ns)
    out = []
    for (call, expected), (_, ok, _) in zip(tests, run_tests(src, tests)):
        if ok:
            out.append((call, expected))
            continue
        try:
            out.append((call, eval(call, ns)))
        except Exception:                                        # noqa: BLE001
            pass
    return out


def check_tasks():
    """
    检查每个任务的四种修改是否符合定义（见文件开头）。不符合就抛出 AssertionError。

    【为什么要写它】这些"标准答案"是手写的，很容易写错一个（例如"错误的修改"碰巧通过了可见测试）。
        让程序检查，比靠眼睛看可靠 —— 这也是本讲的主题：能执行、能测试的东西，就让程序去验证。
    """
    for t in TASKS:
        assert not all_pass(run_tests(t.buggy, t.visible)), t.name
        p = t.patches
        assert all_pass(run_tests(p["correct"], t.visible + t.hidden)), t.name
        assert all_pass(run_tests(p["partial"], t.visible)) and not all_pass(run_tests(p["partial"], t.hidden)), t.name
        assert all_pass(run_tests(p["overfit"], t.visible)) and not all_pass(run_tests(p["overfit"], t.hidden)), t.name
        assert not all_pass(run_tests(p["wrong"], t.visible)), t.name
    return True


if __name__ == "__main__":
    check_tasks()
    print(f"{len(TASKS)} 个任务的四种修改都符合定义。")
    t = TASKS[0]
    for call, ok, msg in run_tests(t.buggy, t.visible):
        print(("通过 " if ok else "失败 ") + call + ("" if ok else "：" + msg))
