# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

import io
import json
import re
import tarfile
import urllib.error
from pathlib import Path
from unittest import mock

import pytest

from jupyter_builder import core_path, federated_extensions
from jupyter_builder.federated_extensions import (
    _check_node_version,
    _ensure_builder,
    _read_rspack_node_range,
)


def _make_core_package_tarball(content: bytes) -> bytes:
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w:gz") as tar:
        info = tarfile.TarInfo(name="package/core.package.json")
        info.size = len(content)
        tar.addfile(info, io.BytesIO(content))
    return buf.getvalue()


@pytest.fixture(autouse=True)
def _no_jupyterlab_installed(monkeypatch):
    """Default to "jupyterlab is not installed" so tests are isolated from the dev environment.

    Tests that exercise the fallback-version compatibility check override this.
    """

    def _raise_not_found(_package):
        raise core_path.PackageNotFoundError(_package)

    monkeypatch.setattr(core_path, "installed_version", _raise_not_found)


def test_get_core_meta_prefers_installed_core_meta(tmp_path):
    ext_path = tmp_path / "ext"
    core_meta_dir = ext_path / "node_modules" / "@jupyterlab" / "core-meta"
    core_meta_dir.mkdir(parents=True)
    (core_meta_dir / "core.package.json").write_text("{}")

    location = core_path.get_core_meta(ext_path=str(ext_path))

    assert location == str(core_meta_dir / "core.package.json")


def test_get_core_meta_installs_dependencies_before_searching(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    calls = []

    def fake_check_call(args, cwd):
        calls.append((args, Path(cwd)))
        core_meta_dir = Path(cwd) / "node_modules" / "@jupyterlab" / "core-meta"
        core_meta_dir.mkdir(parents=True)
        (core_meta_dir / "core.package.json").write_text("{}")

    monkeypatch.setattr(core_path.subprocess, "check_call", fake_check_call)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert calls == [(["jlpm"], ext_path)]
    assert location == str(
        ext_path / "node_modules" / "@jupyterlab" / "core-meta" / "core.package.json",
    )


def test_get_core_meta_uses_cache_for_explicit_version_before_installed(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    core_meta_dir = ext_path / "node_modules" / "@jupyterlab" / "core-meta"
    core_meta_dir.mkdir(parents=True)
    (core_meta_dir / "core.package.json").write_text("{}")

    monkeypatch.setenv("HOME", str(tmp_path))
    cached_file = (
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "v-test" / "core.package.json"
    )
    cached_file.parent.mkdir(parents=True)
    cached_file.write_text('{"from": "cache"}')

    location = core_path.get_core_meta(version="v-test", ext_path=ext_path)

    assert location == str(cached_file)


def test_get_core_meta_fetches_npm_core_meta_before_github(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    def fake_check_call(_args, cwd):
        (Path(cwd) / "node_modules").mkdir(parents=True)

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.6.0-alpha.4.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.subprocess, "check_call", fake_check_call)
    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="4.6.0-alpha.4", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4",
        tarball_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.6.0-alpha.4" / "core.package.json",
    )
    assert json.loads(
        (
            tmp_path
            / ".cache"
            / "jupyterlab_builder"
            / "core"
            / "4.6.0-alpha.4"
            / "core.package.json"
        ).read_text(),
    ) == {"devDependencies": {}}


def test_get_core_meta_falls_back_to_github_when_npm_fails(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    # JupyterLab releases are published as git tags like "v4.5.7", so a numeric
    # version requested from GitHub must be looked up with the "v" prefix.
    github_url = (
        "https://raw.githubusercontent.com/"
        "jupyterlab/jupyterlab/v4.5.7/"
        "jupyterlab/staging/package.json"
    )
    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            msg = "Not Found"
            raise urllib.error.URLError(msg)
        if url == github_url:
            return io.BytesIO(b'{"dependencies": {}}')
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="4.5.7", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7",
        github_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


def test_get_core_meta_accepts_v_prefixed_version(tmp_path, monkeypatch):
    """A 'v'-prefixed version is normalized so it resolves identically to the bare form."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    github_url = (
        "https://raw.githubusercontent.com/"
        "jupyterlab/jupyterlab/v4.5.7/"
        "jupyterlab/staging/package.json"
    )
    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            msg = "Not Found"
            raise urllib.error.URLError(msg)
        if url == github_url:
            return io.BytesIO(b'{"dependencies": {}}')
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="v4.5.7", ext_path=ext_path)

    # Both the npm lookup and the cache directory use the normalized "4.5.7".
    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7",
        github_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


@pytest.mark.parametrize(
    ("version", "expected_ref"),
    [
        ("4.5.7", "v4.5.7"),
        ("4.6.0-alpha.4", "v4.6.0a4"),
        ("4.6.0-beta.1", "v4.6.0b1"),
        ("4.6.0-rc.2", "v4.6.0rc2"),
        ("main", "main"),
        ("some-branch", "some-branch"),
    ],
)
def test_github_ref_maps_versions_to_git_tags(version, expected_ref):
    assert core_path._github_ref(version) == expected_ref


@pytest.mark.parametrize("version", ["4.5.7", "v4.5.7"])
def test_normalize_version_accepts_both_forms(version):
    assert core_path._normalize_version(version) == "4.5.7"


@pytest.mark.parametrize(
    ("version", "expected"),
    [
        # Partial specifiers are ranges over the omitted components.
        ("4", "4.x.x"),
        ("4.5", "4.5.x"),
        # Partially wildcarded ranges are completed the same way.
        ("4.x", "4.x.x"),
        ("4.X", "4.x.x"),
        ("4.*", "4.x.x"),
        ("4.5.*", "4.5.x"),
        ("4.5.X", "4.5.x"),
        # Nothing is constrained after a wildcard component.
        ("4.x.7", "4.x.x"),
        # A bare wildcard means "any version".
        ("*", "latest"),
        ("x", "latest"),
        # Concrete versions and non-numeric specifiers are left alone.
        ("4.5.7", "4.5.7"),
        ("4.5.x", "4.5.x"),
        ("4.6.0-alpha.4", "4.6.0-alpha.4"),
        ("latest", "latest"),
        ("main", "main"),
        ("some-branch", "some-branch"),
    ],
)
def test_expand_partial_version(version, expected):
    assert core_path._expand_partial_version(version) == expected


def test_get_core_meta_falls_back_to_github_for_prerelease(tmp_path, monkeypatch):
    """An npm-style prerelease that npm lacks is fetched from the matching git tag."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    github_url = (
        "https://raw.githubusercontent.com/"
        "jupyterlab/jupyterlab/v4.6.0a4/"
        "jupyterlab/staging/package.json"
    )
    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4":
            msg = "Not Found"
            raise urllib.error.URLError(msg)
        if url == github_url:
            return io.BytesIO(b'{"dependencies": {}}')
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="4.6.0-alpha.4", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4",
        github_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.6.0-alpha.4" / "core.package.json",
    )


def test_get_core_meta_wildcard_resolves_from_github_when_npm_has_no_match(tmp_path, monkeypatch):
    """A wildcard that npm cannot satisfy is resolved against GitHub release tags."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    tags_page1 = f"{core_path._GITHUB_TAGS_API_URL}?per_page=100&page=1"
    tags_page2 = f"{core_path._GITHUB_TAGS_API_URL}?per_page=100&page=2"
    # 4.5.10 must win over 4.5.2 — confirms numeric (not lexical) ordering.
    github_url = (
        "https://raw.githubusercontent.com/"
        "jupyterlab/jupyterlab/v4.5.10/"
        "jupyterlab/staging/package.json"
    )
    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta":
            # npm publishes no 4.5.x, so the npm wildcard lookup finds no match.
            return io.BytesIO(json.dumps({"versions": {"4.6.0": {}}}).encode())
        if url == tags_page1:
            return io.BytesIO(
                json.dumps(
                    [
                        {"name": "v4.6.0"},
                        {"name": "v4.5.2"},
                        {"name": "v4.5.10"},
                        {"name": "v4.5.1"},
                    ],
                ).encode(),
            )
        if url == tags_page2:
            # Older tags with no 4.5.x match — resolution stops here.
            return io.BytesIO(json.dumps([{"name": "v4.4.9"}]).encode())
        if url == github_url:
            return io.BytesIO(b'{"dependencies": {}}')
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="4.5.x", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta",
        tags_page1,
        tags_page2,
        github_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.10" / "core.package.json",
    )


def test_get_core_meta_raises_when_requested_version_is_unresolvable(tmp_path, monkeypatch):
    """An explicitly requested version that exists nowhere must fail loudly."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    def fake_urlopen(*_args, **_kwargs):
        msg = "Not Found"
        raise urllib.error.URLError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    with pytest.raises(RuntimeError, match="Could not resolve @jupyterlab/core-meta"):
        core_path.get_core_meta(version="9.9.9", ext_path=ext_path)


def test_get_core_meta_wildcard_version_resolves_from_npm_and_downloads_core_meta(
    tmp_path,
    monkeypatch,
):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.3.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta":
            return io.BytesIO(
                json.dumps({"versions": {"4.5.0": {}, "4.5.3": {}, "4.6.0": {}}}).encode(),
            )
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.3":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="4.5.x", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta",
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.3",
        tarball_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.3" / "core.package.json",
    )


def test_resolve_wildcard_npm_version_returns_highest_match(monkeypatch):
    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        assert url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta"
        return io.BytesIO(
            json.dumps(
                {
                    "versions": {
                        "4.4.9": {},
                        "4.5.1": {},
                        "4.5.12": {},
                        "4.6.0-alpha.4": {},
                    },
                },
            ).encode(),
        )

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    resolved = core_path._resolve_wildcard_npm_version("4.5.x")

    assert resolved == "4.5.12"


def test_resolve_wildcard_npm_version_matches_prerelease_versions(monkeypatch):
    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        assert url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta"
        return io.BytesIO(
            json.dumps(
                {
                    "versions": {
                        "4.6.0-alpha.3": {},
                        "4.6.0-alpha.4": {},
                        "4.6.0-alpha.5": {},
                    },
                },
            ).encode(),
        )

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    resolved = core_path._resolve_wildcard_npm_version("4.6.x")

    assert resolved == "4.6.0-alpha.5"


def test_resolve_wildcard_npm_version_raises_when_no_matches(monkeypatch):
    monkeypatch.setattr(
        core_path.urllib.request,
        "urlopen",
        lambda *_args, **_kwargs: io.BytesIO(
            json.dumps({"versions": {"4.6.0": {}, "5.0.0": {}}}).encode(),
        ),
    )

    with pytest.raises(urllib.error.URLError, match="No published @jupyterlab/core-meta"):
        core_path._resolve_wildcard_npm_version("4.5.x")


def test_get_core_meta_latest_uses_npm_latest_and_caches_by_resolved_version(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.6.0-alpha.4.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            return io.BytesIO(json.dumps({"version": "4.6.0-alpha.4"}).encode())
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(version="latest", ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest",
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.6.0-alpha.4",
        tarball_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.6.0-alpha.4" / "core.package.json",
    )


def test_get_core_meta_defaults_to_latest_when_not_installed(tmp_path, monkeypatch):
    """With no explicit version and no installed core-meta, the npm "latest" dist-tag is used."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.7.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            return io.BytesIO(json.dumps({"version": "4.5.7"}).encode())
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest",
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7",
        tarball_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


def test_get_core_meta_latest_falls_back_to_github_when_npm_unreachable(tmp_path, monkeypatch):
    """If npm is unreachable, "latest" is resolved to the newest GitHub release tag."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))

    github_url = (
        "https://raw.githubusercontent.com/"
        "jupyterlab/jupyterlab/v4.5.7/"
        "jupyterlab/staging/package.json"
    )
    latest_release = json.dumps({"tag_name": "v4.5.7"}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            msg = "Not Found"
            raise urllib.error.URLError(msg)
        if url == core_path._GITHUB_LATEST_RELEASE_API_URL:
            return io.BytesIO(latest_release)
        if url == github_url:
            return io.BytesIO(b'{"dependencies": {}}')
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest",
        core_path._GITHUB_LATEST_RELEASE_API_URL,
        github_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


def test_get_core_meta_fallback_raises_on_installed_jupyterlab_mismatch(tmp_path, monkeypatch):
    """A fallback-resolved version whose major.minor disagrees with jupyterlab is rejected."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(core_path, "installed_version", lambda _package: "4.6.2")

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            return io.BytesIO(json.dumps({"version": "4.5.7"}).encode())
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    with pytest.raises(RuntimeError, match=re.escape("building against 4.5.7 metadata")):
        core_path.get_core_meta(ext_path=ext_path)


def test_get_core_meta_fallback_allows_matching_installed_jupyterlab(tmp_path, monkeypatch):
    """A fallback-resolved version is accepted when its major.minor matches jupyterlab."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(core_path, "installed_version", lambda _package: "4.5.0")

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.7.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            return io.BytesIO(json.dumps({"version": "4.5.7"}).encode())
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


def test_get_core_meta_explicit_version_skips_installed_jupyterlab_check(tmp_path, monkeypatch):
    """An explicitly requested version is trusted even if it disagrees with jupyterlab."""
    ext_path = tmp_path / "ext"
    core_meta_dir = tmp_path / "core-meta"
    core_meta_dir.mkdir()
    (core_meta_dir / "core.package.json").write_text('{"devDependencies": {}}')
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(core_path, "installed_version", lambda _package: "4.6.2")

    cache_file = tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json"
    cache_file.parent.mkdir(parents=True)
    cache_file.write_text('{"devDependencies": {}}')

    location = core_path.get_core_meta(version="4.5.7", ext_path=ext_path)

    assert location == str(cache_file)


def test_get_core_meta_resolves_to_legacy_builder_marker_version(tmp_path, monkeypatch):
    """With no installed core-meta, a legacy @jupyterlab/builder pin is used over "latest"."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyterlab/builder": "^4.5.7"}}),
    )
    monkeypatch.setenv("HOME", str(tmp_path))

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.7.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    calls = []

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        calls.append(url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)
    logger = mock.Mock()

    location = core_path.get_core_meta(ext_path=ext_path, logger=logger)

    # The "latest" dist-tag is never hit — the marker's own version is requested directly.
    assert calls == [
        f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7",
        tarball_url,
    ]
    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )
    logger.warning.assert_called_once()
    warning_message = logger.warning.call_args[0][0] % logger.warning.call_args[0][1:]
    assert "@jupyterlab/builder@4.5.7" in warning_message


def test_get_core_meta_legacy_builder_marker_skips_installed_jupyterlab_check(
    tmp_path,
    monkeypatch,
):
    """A legacy-marker-resolved version is trusted like an explicit version, not a guess."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyterlab/builder": "^4.5.7"}}),
    )
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(core_path, "installed_version", lambda _package: "4.6.2")

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.7.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


def test_get_core_meta_ignores_legacy_builder_marker_workspace_spec(tmp_path, monkeypatch):
    """A path/workspace version spec isn't a real version, so it falls through to "latest"."""
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "node_modules").mkdir()
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyterlab/builder": "file:../builder"}}),
    )
    monkeypatch.setenv("HOME", str(tmp_path))

    tarball_content = b'{"devDependencies": {}}'
    tarball_bytes = _make_core_package_tarball(tarball_content)
    tarball_url = f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/-/core-meta-4.5.7.tgz"
    registry_meta = json.dumps({"dist": {"tarball": tarball_url}}).encode()

    def fake_urlopen(req_or_url, **_kwargs):
        url = getattr(req_or_url, "full_url", req_or_url)
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/latest":
            return io.BytesIO(json.dumps({"version": "4.5.7"}).encode())
        if url == f"{core_path.JPBLD_NPM_URL}/@jupyterlab/core-meta/4.5.7":
            return io.BytesIO(registry_meta)
        if url == tarball_url:
            return io.BytesIO(tarball_bytes)
        msg = f"Unexpected URL {url}"
        raise AssertionError(msg)

    monkeypatch.setattr(core_path.urllib.request, "urlopen", fake_urlopen)

    location = core_path.get_core_meta(ext_path=ext_path)

    assert location == str(
        tmp_path / ".cache" / "jupyterlab_builder" / "core" / "4.5.7" / "core.package.json",
    )


@pytest.mark.parametrize(
    ("version_spec", "expected"),
    [
        ("4.5.7", "4.5.7"),
        ("^4.5.7", "4.5.7"),
        ("~4.5.7", "4.5.7"),
        (">=4.5.7", "4.5.7"),
        ("file:../builder", None),
        ("workspace:*", None),
    ],
)
def test_legacy_builder_marker_version_parses_spec(tmp_path, version_spec, expected):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyterlab/builder": version_spec}}),
    )

    assert core_path._legacy_builder_marker_version(ext_path) == expected


def test_legacy_builder_marker_version_none_without_package_json(tmp_path):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()

    assert core_path._legacy_builder_marker_version(ext_path) is None


def test_ensure_builder_with_jupyter_builder(tmp_path):
    ext_path = tmp_path / "ext"
    core_path_dir = tmp_path / "core-meta"
    core_package_file = core_path_dir / "core.package.json"
    builder_dir = ext_path / "node_modules" / "@jupyter" / "builder"

    builder_dir.mkdir(parents=True)
    core_path_dir.mkdir()

    core_package_file.write_text(json.dumps({"devDependencies": {"@jupyter/builder": "^5.0.0"}}))
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyter/builder": "^5.0.0"}}),
    )
    (builder_dir / "package.json").write_text(json.dumps({"version": "5.0.0"}))

    builder_path, marker_pkg = _ensure_builder(str(ext_path), str(core_package_file))

    assert builder_path == str(builder_dir / "lib" / "build-labextension.js")
    assert marker_pkg == "@jupyter/builder"


def test_ensure_builder_with_jupyterlab_builder(tmp_path):
    ext_path = tmp_path / "ext"
    core_path_dir = tmp_path / "core-meta"
    core_package_file = core_path_dir / "core.package.json"
    builder_dir = ext_path / "node_modules" / "@jupyterlab" / "builder"

    builder_dir.mkdir(parents=True)
    core_path_dir.mkdir()

    core_package_file.write_text(json.dumps({"devDependencies": {"@jupyterlab/builder": "^5.0.0"}}))
    (ext_path / "package.json").write_text(
        json.dumps({"devDependencies": {"@jupyterlab/builder": "^5.0.0"}}),
    )
    (builder_dir / "package.json").write_text(json.dumps({"version": "5.0.0"}))

    builder_path, marker_pkg = _ensure_builder(str(ext_path), str(core_package_file))

    assert builder_path == str(builder_dir / "lib" / "build-labextension.js")
    assert marker_pkg == "@jupyterlab/builder"


def _write_rspack(ext_path: Path, node_range: str) -> None:
    rspack_dir = ext_path / "node_modules" / "@rspack" / "core"
    rspack_dir.mkdir(parents=True)
    (rspack_dir / "package.json").write_text(json.dumps({"engines": {"node": node_range}}))


def test_read_rspack_node_range_reads_engines(tmp_path):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    _write_rspack(ext_path, "^20.19.0 || >=22.12.0")

    builder = str(ext_path / "node_modules" / "@jupyter" / "builder" / "lib" / "x.js")
    assert _read_rspack_node_range(builder, str(ext_path)) == "^20.19.0 || >=22.12.0"


def test_read_rspack_node_range_falls_back_when_missing(tmp_path):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()

    assert _read_rspack_node_range(str(ext_path), str(ext_path)) == "^20.19.0 || >=22.12.0"


def test_check_node_version_raises_on_old_node(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    _write_rspack(ext_path, "^20.19.0 || >=22.12.0")

    monkeypatch.setattr(federated_extensions, "_which_node_js", lambda: "node")
    monkeypatch.setattr(
        federated_extensions.subprocess,
        "check_output",
        lambda *_args, **_kwargs: b"v18.20.8\n",
    )

    with pytest.raises(RuntimeError, match=r"requires Node\.js .* \(found v18\.20\.8\)"):
        _check_node_version(str(ext_path), str(ext_path))


def test_check_node_version_passes_on_supported_node(tmp_path, monkeypatch):
    ext_path = tmp_path / "ext"
    ext_path.mkdir()
    _write_rspack(ext_path, "^20.19.0 || >=22.12.0")

    monkeypatch.setattr(federated_extensions, "_which_node_js", lambda: "node")
    monkeypatch.setattr(
        federated_extensions.subprocess,
        "check_output",
        lambda *_args, **_kwargs: b"v22.12.0\n",
    )

    _check_node_version(str(ext_path), str(ext_path))
