"""Lint: cap Python file length at 500 lines (production code only).

Every production ``.py`` file tracked by the repository (i.e. not
matched by ``.gitignore``) must be at most :data:`MAX_PY_LINES` lines
long. The threshold is a soft architectural cue. A file approaching
the cap is usually doing too many things and is a candidate for a
split.

Files under the top-level ``tests/`` directory are intentionally
excluded: tests cluster fixtures and parametrised cases by feature,
so length here reflects coverage breadth rather than design weight.

Per-file opt-out
----------------

A file may opt out of this cap by including a unified
``# nocheck: file-size`` marker inside the
first :data:`SCAN_LINES` lines, typically the module docstring or a
comment at the top. The opt-out is intentionally visible at the top of
the file so the cost of carrying long modules is not silently buried.
See ``docs/contributing/actions/testing/suppression.md`` for the full
marker grammar.

The walker uses the SPOT helpers in :mod:`utils.cache.files` so the
lint runs in environments where ``.git/`` is not mounted (the
``make compose-exec`` container) and stays consistent with other gitignore-
aware lint tests.
"""

from __future__ import annotations

import unittest
from pathlib import Path

from utils.annotations.suppress import is_suppressed_in_head
from utils.cache.base import PROJECT_ROOT
from utils.cache.files import iter_non_ignored_files

MAX_PY_LINES: int = 500

SCAN_LINES: int = 30


def _line_count(path: Path) -> int:
    with path.open("rb") as fh:
        data = fh.read()
    if not data:
        return 0
    n = data.count(b"\n")
    return n if data.endswith(b"\n") else n + 1


def _has_nocheck_marker(path: Path) -> bool:
    try:
        with path.open("r", encoding="utf-8", errors="replace") as fh:
            head_lines = [next(fh, "") for _ in range(SCAN_LINES)]
    except OSError:
        return False
    return is_suppressed_in_head(head_lines, "file-size", scan_lines=SCAN_LINES)


class TestPythonFileSize(unittest.TestCase):
    def test_python_files_at_or_below_max_lines(self):
        root = PROJECT_ROOT
        offenders: list[tuple[str, int]] = []
        for path_str in iter_non_ignored_files(extensions=(".py",), exclude_tests=True):
            path = Path(path_str)
            lines = _line_count(path)
            if lines <= MAX_PY_LINES:
                continue
            if _has_nocheck_marker(path):
                continue
            rel = path.relative_to(root).as_posix()
            offenders.append((rel, lines))

        if offenders:
            offenders.sort(key=lambda item: (-item[1], item[0]))
            self.fail(
                f"Python files exceeding {MAX_PY_LINES} lines "
                f"({len(offenders)}):\n"
                + "\n".join(f"  - {rel} ({lines} lines)" for rel, lines in offenders)
                + "\n\nSplit the file or move sections into focused modules. "
                "Raising MAX_PY_LINES requires a corresponding cleanup pass."
            )


if __name__ == "__main__":
    unittest.main()
