#!/usr/bin/env python3
# File: scripts/check_pytest_version.py
# Purpose: Verify that CI environments use the vetted pytest version and fail fast when upgrades slip in.
# Context: Guards the TempPathFactory monkey patch and related safeguards that depend on pytest internals.

"""CI guardrail that detects unexpected pytest upgrades.

The script compares the installed pytest version against the value recorded in
``config/pytest_version.lock``.  When versions differ, it explains the manual
validation steps (release notes review, warning scan, regression runs) and
exits with a non-zero status so that the pipeline fails visibly.

Developers intentionally bumping pytest should:
1. Review the upstream release notes.
2. Re-run the focused regression suites (notably the B20 checks) in parallel
   and serial modes.
3. Adjust local patches that depend on pytest internals if necessary.
4. Update ``config/pytest_version.lock`` to the validated version.
"""

from __future__ import annotations

import os
import sys
from importlib import metadata
from pathlib import Path

LOCK_PATH_ENV = "PYTEST_VERSION_LOCK_PATH"
DEFAULT_LOCK_PATH = Path(__file__).resolve().parent.parent / "config" / "pytest_version.lock"


def _resolve_lock_path() -> Path:
    """Return the path to the lock file, honouring test overrides."""

    override = os.getenv(LOCK_PATH_ENV)
    if override:
        return Path(override)
    return DEFAULT_LOCK_PATH


def _read_expected_version(lock_path: Path) -> str:
    """Read the expected pytest version from ``lock_path``.

    Raises:
        FileNotFoundError: When the lock file is missing.
        ValueError: When the lock file is empty.
    """

    content = lock_path.read_text(encoding="utf-8").strip()
    if not content:
        raise ValueError(f"{lock_path} is empty; record the validated pytest version.")
    return content


def _current_pytest_version() -> str:
    """Return the installed pytest version using importlib metadata."""

    return metadata.version("pytest")


def _format_mismatch_message(expected: str, actual: str) -> str:
    """Create the guidance message displayed when versions differ."""

    checklist = "\n".join(
        [
            "  1. Review the pytest release notes for breaking changes.",
            '  2. Re-run ./bin/test -k "B20" と ./bin/test --xdist-auto -k "B20".',
            "  3. 環境ログに新しい DeprecationWarning / エラーが出ていないか確認.",
            "  4. patches (TempPathFactory 等) に修正が必要なら合わせて更新.",
            "  5. 上記が完了したら config/pytest_version.lock を新しいバージョンに更新.",
        ]
    )
    return (
        "Detected pytest version drift.\n"
        f"  expected: {expected}\n"
        f"  actual:   {actual}\n"
        "Follow this手順でアップグレードを確認してください:\n"
        f"{checklist}\n"
        "その後、pytest_version.lock を更新した上で再度 CI を実行してください。"
    )


def check_pytest_version(lock_path: Path | None = None) -> int:
    """Compare the expected pytest version with the installed version."""

    resolved_path = lock_path or _resolve_lock_path()
    expected = _read_expected_version(resolved_path)
    actual = _current_pytest_version()

    if expected.strip() == actual.strip():
        return 0

    message = _format_mismatch_message(expected, actual)
    print(message, file=sys.stderr)
    return 1


def main(argv: list[str] | None = None) -> int:
    """Entry-point used by CI and unit tests."""

    try:
        return check_pytest_version()
    except FileNotFoundError as exc:
        print(f"pytest version lock file missing: {exc}", file=sys.stderr)
        return 2
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 3
    except Exception as exc:  # defensive: unexpected errors should fail fast
        print(f"Unexpected error while checking pytest version: {exc}", file=sys.stderr)
        return 4


if __name__ == "__main__":  # pragma: no cover - CLI guard
    raise SystemExit(main(sys.argv[1:]))
