"""Tests for tools/dep_check.py -- compares pipreqs output
(requirements.auto.txt) against a conda-list-export lock snapshot
(~environment.lock.txt) to decide whether the dep-check fast path can skip
a redundant "conda install" (REQ-005-family fast path).

parse_lock/parse_reqs take an explicit path and are tested directly.
main() reads the hardcoded relative REQ_FILE/LOCK_FILE filenames from the
current directory, so it is exercised via subprocess with cwd set to a
crafted temp directory (mirrors tests/test_find_entry.py's pattern).

Covers name normalization (lowercasing, version-specifier and extras
stripping), comment/blank-line skipping, missing-file handling, and the
base64 HP_DEP_CHECK payload sync (byte-equality of the embedded payload vs
this source -- mirrors CollectSubmodules/DetectPython/PyprojDeps/DetectVisa
PayloadSync).
"""
import base64
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

from tools.dep_check import parse_lock, parse_reqs

REPO = Path(__file__).resolve().parent.parent
SOURCE = REPO / "tools" / "dep_check.py"


class ParseLock(unittest.TestCase):
    def test_basic_names_extracted_lowercase(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("Numpy=1.26.0=py311h0\nrequests=2.31.0=pyh0\n")
            path = f.name
        self.assertEqual(parse_lock(path), frozenset({"numpy", "requests"}))

    def test_comments_and_blank_lines_skipped(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("# This file may be used...\n\nclick=8.1.0=py311h0\n")
            path = f.name
        self.assertEqual(parse_lock(path), frozenset({"click"}))

    def test_missing_file_returns_empty_frozenset(self):
        self.assertEqual(parse_lock("/nonexistent/path/~environment.lock.txt"), frozenset())

    def test_name_only_no_version_build(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("justaname\n")
            path = f.name
        self.assertEqual(parse_lock(path), frozenset({"justaname"}))


class ParseReqs(unittest.TestCase):
    def test_version_specifier_stripped(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("numpy>=1.20\nrequests==2.31.0\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["numpy", "requests"])

    def test_extras_stripped(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("pandas[excel]\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["pandas"])

    def test_comments_and_blank_lines_skipped(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("# generated by pipreqs\n\nclick\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["click"])

    def test_lowercased(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("Pillow>=9\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["pillow"])

    def test_missing_file_returns_empty_list(self):
        self.assertEqual(parse_reqs("/nonexistent/path/requirements.auto.txt"), [])


def _run_main(d):
    proc = subprocess.run(
        [sys.executable, str(SOURCE)],
        cwd=d,
        capture_output=True,
        text=True,
    )
    return proc.stdout.strip()


class MainDecision(unittest.TestCase):
    def test_no_lock_file_runs(self):
        with tempfile.TemporaryDirectory() as d:
            self.assertEqual(_run_main(d), "run")

    def test_lock_present_no_reqs_file_skips(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text("numpy=1.26.0=py311h0\n", encoding="ascii")
            self.assertEqual(_run_main(d), "skip")

    def test_lock_empty_runs(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text("# empty lock\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text("numpy\n", encoding="ascii")
            self.assertEqual(_run_main(d), "run")

    def test_reqs_empty_skips(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text("numpy=1.26.0=py311h0\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text("# nothing found\n", encoding="ascii")
            self.assertEqual(_run_main(d), "skip")

    def test_all_reqs_covered_skips(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text(
                "numpy=1.26.0=py311h0\nrequests=2.31.0=pyh0\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text("numpy>=1.20\nrequests\n", encoding="ascii")
            self.assertEqual(_run_main(d), "skip")

    def test_missing_req_runs(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text("numpy=1.26.0=py311h0\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text("numpy\nrequests\n", encoding="ascii")
            self.assertEqual(_run_main(d), "run")

    def test_hyphen_vs_underscore_separator_mismatch_skips(self):
        # Regression: conda-forge and PyPI sometimes spell the same logical
        # package with different separators for the same name (e.g.
        # typing_extensions vs. typing-extensions). Before PEP-503-style
        # normalization, this pair looked "missing" from the lock on every
        # run, forcing an unnecessary reinstall and defeating the fast path.
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text(
                "typing_extensions=4.9.0=pyh0\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text(
                "typing-extensions>=4.0\n", encoding="ascii")
            self.assertEqual(_run_main(d), "skip")

    def test_dot_separator_mismatch_skips(self):
        with tempfile.TemporaryDirectory() as d:
            (Path(d) / "~environment.lock.txt").write_text(
                "ruamel.yaml=0.18.0=pyh0\n", encoding="ascii")
            (Path(d) / "requirements.auto.txt").write_text(
                "ruamel-yaml\n", encoding="ascii")
            self.assertEqual(_run_main(d), "skip")


class Normalization(unittest.TestCase):
    def test_parse_lock_normalizes_underscore_to_hyphen(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("typing_extensions=4.9.0=pyh0\n")
            path = f.name
        self.assertEqual(parse_lock(path), frozenset({"typing-extensions"}))

    def test_parse_reqs_normalizes_underscore_to_hyphen(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("typing_extensions>=4.0\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["typing-extensions"])

    def test_repeated_separators_collapse_to_one_hyphen(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
            f.write("a__b--c\n")
            path = f.name
        self.assertEqual(parse_reqs(path), ["a-b-c"])


class PayloadSync(unittest.TestCase):
    def test_embedded_base64_matches_source(self):
        bat = (REPO / "run_setup.bat").read_text(encoding="utf-8", errors="replace")
        m = re.search(r'set "HP_DEP_CHECK=([A-Za-z0-9+/=]+)"', bat)
        self.assertIsNotNone(m, "HP_DEP_CHECK payload not found in run_setup.bat")
        decoded = base64.b64decode(m.group(1)).decode("utf-8")
        source = SOURCE.read_text(encoding="utf-8")
        self.assertEqual(
            decoded, source,
            "HP_DEP_CHECK base64 is out of sync with tools/dep_check.py; re-encode it.",
        )


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