#!/usr/bin/env python3
"""Покрытие изменённых строк тестами.

Складывает `go test -coverprofile` и `git diff -U0 <base>`: показывает, какие
изменённые исполняемые строки не покрыты ни одним тестом. Общий процент по
пакету бесполезен для ревью — важно, покрыт ли именно новый код.

Использование: scripts/diff-coverage.py <coverprofile> <base-rev>
"""

import re
import subprocess
import sys
from collections import defaultdict

HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
BLOCK = re.compile(r"^(.+):(\d+)\.\d+,(\d+)\.\d+ (\d+) (\d+)$")


def module_path() -> str:
    with open("go.mod", encoding="utf-8") as f:
        for line in f:
            if line.startswith("module "):
                return line.split(None, 1)[1].strip()
    return ""


def coverage_blocks(profile: str, module: str):
    """file -> [(start, end, count)] по репо-относительным путям."""
    blocks = defaultdict(list)
    with open(profile, encoding="utf-8") as f:
        for line in f:
            m = BLOCK.match(line.strip())
            if not m:
                continue
            path, start, end, _stmts, count = m.groups()
            if module and path.startswith(module + "/"):
                path = path[len(module) + 1:]
            blocks[path].append((int(start), int(end), int(count)))
    return blocks


def changed_lines(base: str):
    """file -> {номера добавленных/изменённых строк} для нетестовых .go."""
    out = subprocess.run(
        ["git", "diff", "-U0", base, "--", "*.go"],
        capture_output=True, text=True, check=True,
    ).stdout
    changed = defaultdict(set)
    current = None
    for line in out.splitlines():
        if line.startswith("+++ b/"):
            path = line[6:]
            current = None if path.endswith("_test.go") else path
        elif line.startswith("@@") and current:
            m = HUNK.match(line)
            if m:
                start = int(m.group(1))
                count = int(m.group(2) or 1)
                changed[current].update(range(start, start + count))
    return changed


def main() -> int:
    if len(sys.argv) != 3:
        print(__doc__, file=sys.stderr)
        return 2
    profile, base = sys.argv[1], sys.argv[2]

    blocks = coverage_blocks(profile, module_path())
    changed = changed_lines(base)

    total = uncovered = 0
    report = []
    for path in sorted(changed):
        gaps = []
        for line in sorted(changed[path]):
            covering = [b for b in blocks.get(path, []) if b[0] <= line <= b[1]]
            if not covering:
                continue  # не исполняемая строка (объявление, комментарий, скобка)
            total += 1
            if all(b[2] == 0 for b in covering):
                uncovered += 1
                gaps.append(line)
        if gaps:
            report.append((path, gaps))

    if total == 0:
        print("изменённых исполняемых строк нет (или профиль не содержит этих пакетов)")
        return 0

    print(f"изменённых исполняемых строк: {total}, не покрыто: {uncovered}"
          f" ({100 * (total - uncovered) // total}% покрытия диффа)")
    for path, gaps in report:
        print(f"  {path}: {compact(gaps)}")
    return 0


def compact(lines):
    """[3,4,5,9] -> '3-5,9'."""
    out, start, prev = [], lines[0], lines[0]
    for line in lines[1:] + [None]:
        if line == prev + 1:
            prev = line
            continue
        out.append(str(start) if start == prev else f"{start}-{prev}")
        start = prev = line
    return ",".join(out)


if __name__ == "__main__":
    sys.exit(main())
