from __future__ import annotations

import shutil
import subprocess
from pathlib import Path
from typing import Annotated, cast

import libcst as cst
import typer
from libcst.metadata import CodeRange, MetadataWrapper, PositionProvider

_UV = shutil.which("uv") or str(Path.home() / ".local/bin/uv")


class _Collector(cst.CSTVisitor):
    METADATA_DEPENDENCIES = (PositionProvider,)

    def __init__(self) -> None:
        self.items: list[tuple[int, cst.SimpleStatementLine]] = []

    def visit_SimpleStatementLine(self, node: cst.SimpleStatementLine) -> None:
        pos = cast("CodeRange", self.get_metadata(PositionProvider, node))
        self.items.append((pos.start.line, node))


class _Replacer(cst.CSTTransformer):
    def __init__(self, target: cst.SimpleStatementLine) -> None:
        self._target = target

    def leave_SimpleStatementLine(
        self,
        original_node: cst.SimpleStatementLine,
        updated_node: cst.SimpleStatementLine,
    ) -> cst.BaseStatement:
        if original_node is self._target:
            return updated_node.with_changes(body=[cst.Pass()])
        return updated_node


def run(
    file: Annotated[Path, typer.Argument()],
    test_file: Annotated[Path, typer.Argument()],
) -> None:
    """Detect code your tests don't demand. Lines that can be deleted without breaking any test are fence violations — they should be removed."""
    source = file.read_text()
    module = cst.parse_module(source)
    wrapper = MetadataWrapper(module, unsafe_skip_copy=True)

    collector = _Collector()
    wrapper.visit(collector)

    excess_lines: list[tuple[int, str]] = []

    try:
        for line_no, node in collector.items:
            file.write_text(module.visit(_Replacer(node)).code)
            result = subprocess.run(
                [
                    _UV,
                    "run",
                    "pytest",
                    "-x",
                    "-q",
                    str(test_file),
                ],
                capture_output=True,
                check=False,
            )

            if result.returncode == 0:
                original_text = module.code_for_node(node).strip()
                excess_lines.append((line_no, original_text))
    finally:
        file.write_text(source)

    if excess_lines:
        max_line_number_length = max(len(str(line_no)) for line_no, _ in excess_lines)
        for line_no, text in excess_lines:
            print(f"{line_no:{max_line_number_length}}| {text}")
        print(
            f"\n{len(excess_lines)} fence violations — delete these lines, they do not change any observed behavior"
        )
        raise typer.Exit(code=1)


def main() -> None:  # pragma: no cover
    typer.run(run)
