"""Tests for the custom Hatch frontend build hook."""

import importlib
import subprocess
import sys
import tomllib
import types
from pathlib import Path

import pytest


@pytest.fixture
def hatch_build_module(monkeypatch: pytest.MonkeyPatch) -> types.ModuleType:
    """Import ``hatch_build`` with a stub Hatchling interface."""
    interface_module = types.ModuleType("hatchling.builders.hooks.plugin.interface")
    interface_module.BuildHookInterface = type("BuildHookInterface", (), {})

    monkeypatch.setitem(sys.modules, "hatchling", types.ModuleType("hatchling"))
    monkeypatch.setitem(sys.modules, "hatchling.builders", types.ModuleType("hatchling.builders"))
    monkeypatch.setitem(sys.modules, "hatchling.builders.hooks", types.ModuleType("hatchling.builders.hooks"))
    monkeypatch.setitem(
        sys.modules,
        "hatchling.builders.hooks.plugin",
        types.ModuleType("hatchling.builders.hooks.plugin"),
    )
    monkeypatch.setitem(sys.modules, "hatchling.builders.hooks.plugin.interface", interface_module)
    sys.modules.pop("hatch_build", None)
    return importlib.import_module("hatch_build")


def test_get_output_dir_for_standard_build_stays_out_of_dist(
    hatch_build_module: types.ModuleType,
    tmp_path: Path,
) -> None:
    """Wheel builds should not leave non-package artifacts in the publish directory."""
    build_dir = tmp_path / "dist"

    output_dir = hatch_build_module._get_output_dir(tmp_path / "frontend", str(build_dir), "standard")

    assert output_dir == tmp_path / ".frontend-build" / "frontend-dist"
    assert output_dir.parent != build_dir


def test_editable_build_skips_frontend_build_even_when_bun_is_available(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Editable installs should not build bundled dashboard assets."""
    frontend_dir = tmp_path / "frontend"
    frontend_dir.mkdir()
    hook = hatch_build_module.FrontendBuildHook()
    hook.target_name = "wheel"
    hook.root = str(tmp_path)
    hook.directory = str(tmp_path / "dist")
    build_calls: list[tuple[Path, Path, str]] = []

    def fake_build_frontend(frontend: Path, output: Path, bun: str) -> None:
        build_calls.append((frontend, output, bun))

    monkeypatch.setattr(
        hatch_build_module.shutil,
        "which",
        lambda name: "/usr/local/bin/bun" if name == "bun" else None,
    )
    monkeypatch.setattr(hatch_build_module, "_build_frontend", fake_build_frontend)

    hook.initialize("editable", {})

    assert build_calls == []


def test_editable_build_env_opt_in_builds_repo_frontend_dist(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Editable installs should build frontend/dist when explicitly requested."""
    frontend_dir = tmp_path / "frontend"
    frontend_dir.mkdir()
    hook = hatch_build_module.FrontendBuildHook()
    hook.target_name = "wheel"
    hook.root = str(tmp_path)
    hook.directory = str(tmp_path / "dist")
    build_calls: list[tuple[Path, Path, str]] = []
    build_data: dict[str, object] = {}

    def fake_build_frontend(frontend: Path, output: Path, bun: str) -> None:
        build_calls.append((frontend, output, bun))

    monkeypatch.setenv(hatch_build_module._BUILD_FRONTEND_ENV, "1")
    monkeypatch.setattr(
        hatch_build_module.shutil,
        "which",
        lambda name: "/usr/local/bin/bun" if name == "bun" else None,
    )
    monkeypatch.setattr(hatch_build_module, "_build_frontend", fake_build_frontend)

    hook.initialize("editable", build_data)

    assert build_calls == [(frontend_dir, frontend_dir / "dist", "/usr/local/bin/bun")]
    assert build_data == {}


def test_editable_build_env_opt_in_requires_bun(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """An explicit editable frontend build should fail rather than skip missing Bun."""
    (tmp_path / "frontend").mkdir()
    hook = hatch_build_module.FrontendBuildHook()
    hook.target_name = "wheel"
    hook.root = str(tmp_path)
    hook.directory = str(tmp_path / "dist")

    monkeypatch.setenv(hatch_build_module._BUILD_FRONTEND_ENV, "1")
    monkeypatch.setattr(hatch_build_module.shutil, "which", lambda _name: None)

    with pytest.raises(RuntimeError, match="MINDROOM_BUILD_FRONTEND=1"):
        hook.initialize("editable", {})


def test_run_command_retries_once_before_succeeding(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Transient subprocess failures should be retried before surfacing."""
    calls = {"count": 0}
    sleeps: list[float] = []

    def fake_run(cmd: list[str], *, check: bool, cwd: Path) -> None:
        assert check is True
        assert cwd == tmp_path
        calls["count"] += 1
        if calls["count"] == 1:
            raise subprocess.CalledProcessError(returncode=1, cmd=cmd)

    monkeypatch.setattr(hatch_build_module.subprocess, "run", fake_run)
    monkeypatch.setattr(hatch_build_module.time, "sleep", lambda seconds: sleeps.append(seconds))

    hatch_build_module._run_command(
        ["bun", "install", "--frozen-lockfile"],
        cwd=tmp_path,
        retries=2,
        retry_delay_seconds=0.25,
    )

    assert calls["count"] == 2
    assert sleeps == [0.25]


def test_build_frontend_retries_bun_install_only(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Only the network-dependent bun install step should use retries."""
    frontend_dir = tmp_path / "frontend"
    output_dir = tmp_path / "frontend-dist"
    frontend_dir.mkdir()

    calls: list[tuple[list[str], Path, int, float]] = []

    def fake_run_command(
        cmd: list[str],
        *,
        cwd: Path,
        retries: int = 1,
        retry_delay_seconds: float = 0.0,
    ) -> None:
        calls.append((cmd, cwd, retries, retry_delay_seconds))

    monkeypatch.setattr(hatch_build_module, "_run_command", fake_run_command)

    hatch_build_module._build_frontend(frontend_dir, output_dir, "/usr/local/bin/bun")

    assert calls == [
        (
            ["/usr/local/bin/bun", "install", "--frozen-lockfile"],
            frontend_dir,
            hatch_build_module._BUN_INSTALL_MAX_ATTEMPTS,
            hatch_build_module._BUN_INSTALL_RETRY_DELAY_SECONDS,
        ),
        (["/usr/local/bin/bun", "run", "tsc"], frontend_dir, 1, 0.0),
        (
            ["/usr/local/bin/bun", "run", "vite", "build", "--outDir", str(output_dir)],
            frontend_dir,
            1,
            0.0,
        ),
    ]


def test_build_frontend_rejects_git_lfs_pointer_assets(
    hatch_build_module: types.ModuleType,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Wheel builds must fail instead of bundling unresolved Git LFS pointers."""
    frontend_dir = tmp_path / "frontend"
    output_dir = tmp_path / "frontend-dist"
    frontend_dir.mkdir()

    def fake_run_command(
        cmd: list[str],
        **_run_kwargs: object,
    ) -> None:
        if cmd[-2:] == ["--outDir", str(output_dir)]:
            output_dir.mkdir(exist_ok=True)
            (output_dir / "logo.png").write_text(
                "version https://git-lfs.github.com/spec/v1\n"
                "oid sha256:4aab59a2761edf3b65c4f82cd0a0f13cb0ab782a4c1ef345034d1f9724cec4f1\n"
                "size 685151\n",
            )

    monkeypatch.setattr(hatch_build_module, "_run_command", fake_run_command)

    with pytest.raises(RuntimeError, match="Run git lfs pull before building"):
        hatch_build_module._build_frontend(frontend_dir, output_dir, "/usr/local/bin/bun")


def test_dashboard_shell_uses_canonical_png_logo() -> None:
    """The installed dashboard should use the canonical PNG logo assets."""
    repo_root = Path(__file__).resolve().parents[1]

    assert 'href="/favicon.png"' in (repo_root / "frontend/index.html").read_text()
    assert 'src="/logo.png"' in (repo_root / "frontend/src/App.tsx").read_text()


def test_wheel_force_include_does_not_bundle_avatar_assets() -> None:
    """Wheel builds should not ship repo avatar assets inside the package."""
    pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
    data = tomllib.loads(pyproject.read_text())

    force_include = data["tool"]["hatch"]["build"]["targets"]["wheel"]["force-include"]

    assert "avatars" not in force_include
