"""
Tests for the scanner's file_length signal (Forgejo issue #25, Part 1).

Objective fact only: line count + estimated token count vs configurable
thresholds. No classification, no report-owned fields (token_estimate is
owned by report_builder.py, not the scanner).
"""

import sys
from pathlib import Path

_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from scanner import Scanner, SignalComputer  # noqa: E402
from token_estimator import TokenEstimator  # noqa: E402

from test_scanner_signals import (  # noqa: E402
    FORBIDDEN_SIGNAL_FIELDS,
    _assert_no_classifier_fields,
    _assert_signal_present,
    _make_tree,
)


class _StubEstimator(TokenEstimator):
    """Deterministic stub: 1 token per line, ignoring content."""

    name = "stub-per-line"

    def estimate(self, text: str) -> int:
        if not text:
            return 0
        return len(text.splitlines())


def _run(root: Path, **kwargs) -> dict:
    return Scanner(
        root=root,
        git_log_fn=lambda p: [],
        now_fn=lambda: 0.0,
        token_estimator=kwargs.pop("token_estimator", _StubEstimator()),
        **kwargs,
    ).run()


class TestFileLengthSignal:
    def test_short_file_no_signal(self, tmp_path):
        _make_tree(tmp_path, {"doc.md": "\n".join(f"line {i}" for i in range(10))})
        result = _run(tmp_path)
        sigs = result["signals"].get("doc.md", [])
        names = [s["name"] for s in sigs]
        assert "file_length" not in names

    def test_over_line_threshold_emits_signal(self, tmp_path):
        content = "\n".join(f"line {i}" for i in range(500))
        _make_tree(tmp_path, {"doc.md": content})
        result = _run(tmp_path, max_lines=400, max_tokens=4000)
        sigs = result["signals"].get("doc.md", [])
        sig = _assert_signal_present(sigs, "file_length")
        _assert_no_classifier_fields(sig)
        assert "500" in sig["detail"]
        assert "400" in sig["detail"]

    def test_over_token_threshold_emits_signal(self, tmp_path):
        """Line count under threshold but stub estimator pushes tokens over."""
        content = "\n".join(f"line {i}" for i in range(50))
        _make_tree(tmp_path, {"doc.md": content})
        # Stub estimates 1 token/line -> 50 tokens; set max_tokens low so it fires
        # even though line count (50) stays well under max_lines (400).
        result = _run(tmp_path, max_lines=400, max_tokens=10)
        sigs = result["signals"].get("doc.md", [])
        sig = _assert_signal_present(sigs, "file_length")
        _assert_no_classifier_fields(sig)
        assert "threshold 400 lines / 10 tokens" in sig["detail"]

    def test_configurable_threshold_respected(self, tmp_path):
        content = "\n".join(f"line {i}" for i in range(50))
        _make_tree(tmp_path, {"doc.md": content})
        # Below default threshold -> no signal
        result_default = _run(tmp_path)
        assert "file_length" not in [
            s["name"] for s in result_default["signals"].get("doc.md", [])
        ]
        # Lower the threshold below the actual line count -> signal fires
        result_custom = _run(tmp_path, max_lines=10, max_tokens=4000)
        sigs = result_custom["signals"].get("doc.md", [])
        sig = _assert_signal_present(sigs, "file_length")
        assert "threshold 10 lines" in sig["detail"]

    def test_silent_below_threshold_with_default_estimator(self, tmp_path):
        """Uses the real default_estimator() (no injected stub) below threshold."""
        _make_tree(tmp_path, {"doc.md": "short content"})
        result = Scanner(
            root=tmp_path,
            git_log_fn=lambda p: [],
            now_fn=lambda: 0.0,
        ).run()
        sigs = result["signals"].get("doc.md", [])
        names = [s["name"] for s in sigs]
        assert "file_length" not in names

    def test_signal_omits_forbidden_report_fields(self, tmp_path):
        """file_length must not leak report-owned fields (e.g. token_estimate)."""
        content = "\n".join(f"line {i}" for i in range(500))
        _make_tree(tmp_path, {"doc.md": content})
        result = _run(tmp_path, max_lines=400, max_tokens=4000)
        sigs = result["signals"].get("doc.md", [])
        sig = _assert_signal_present(sigs, "file_length")
        for field in FORBIDDEN_SIGNAL_FIELDS:
            assert field not in sig, f"forbidden field '{field}' found in file_length signal"

    def test_signal_computer_direct_default_thresholds(self, tmp_path):
        """SignalComputer alone: constructor defaults are 400 lines / 4000 tokens."""
        content = "\n".join(f"line {i}" for i in range(500))
        _make_tree(tmp_path, {"doc.md": content})
        path = tmp_path / "doc.md"
        computer = SignalComputer(
            root=tmp_path,
            git_log_fn=None,
            now_fn=lambda: 0.0,
            token_estimator=_StubEstimator(),
        )
        sigs = computer._file_length(path)
        sig = _assert_signal_present(sigs, "file_length")
        assert "threshold 400 lines / 4000 tokens" in sig["detail"]
