"""Unit tests for the bootstrap scaffold generator."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Dict

from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.bootstrap import (
    _parse_entry,
    _resolve_deps,
    _resolve_version,
    _semver_key,
    bootstrap,
    build_context,
    render,
    scaffold_one,
)


# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------

_MC_NAMES = {
    "dir_name": "multi_choice",
    "pip_name": "byteb4rb1e.sphinxcontrib.h5p.multi_choice",
    "vendor_name": "h5p-multi-choice",
}


# -------------------------------------------------------------------
# render()
# -------------------------------------------------------------------

class TestRenderPyproject:
    """Tests for pyproject.toml rendering."""

    def test_build_system(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert 'requires = ["setuptools", "wheel"' in out

    def test_project_name(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert (
            'name = "byteb4rb1e.sphinxcontrib.h5p.multi_choice"'
            in out
        )

    def test_core_dep(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert '"byteb4rb1e.sphinxcontrib.h5p.core"' in out

    def test_h5p_manifest(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert 'machine_name = "H5P.MultiChoice"' in out

    def test_vendor_map(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert (
            '"h5p-multi-choice" = "vendor/h5p-multi-choice"' in out
        )

    def test_package_data(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert '[tool.setuptools.package-data]' in out
        assert '"_static/**/*"' in out


# -------------------------------------------------------------------
# __init__.py rendering
# -------------------------------------------------------------------

class TestRenderInit:
    """Tests for __init__.py rendering."""

    def test_core_extension(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("__init__.py.j2", ctx)
        assert (
            "app.setup_extension("
            "'byteb4rb1e.sphinxcontrib.h5p.core')"
            in out
        )

    def test_content_hook(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("__init__.py.j2", ctx)
        assert "register_assets(app)" in out
        assert "directives.register(app)" in out
        assert "directive.register(app)" in out


# -------------------------------------------------------------------
# Pipfile rendering
# -------------------------------------------------------------------

class TestRenderPipfile:
    """Tests for Pipfile rendering."""

    def test_self_editable(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("Pipfile.j2", ctx)
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.multi_choice" = '
            '{file = ".", editable = true}'
            in out
        )

    def test_core_editable(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("Pipfile.j2", ctx)
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.core" = '
            '{file = "../core", editable = true}'
            in out
        )


# -------------------------------------------------------------------
# Makefile / README / conftest rendering
# -------------------------------------------------------------------

class TestRenderMakefile:
    def test_standard_targets(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("Makefile.j2", ctx)
        for target in ("fmt", "lint", "check", "test", "dist",
                        "clean"):
            assert "%s:" % target in out


class TestRenderReadme:
    def test_pip_name(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("README.md.j2", ctx)
        assert "byteb4rb1e.sphinxcontrib.h5p.multi_choice" in out

    def test_machine_name(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("README.md.j2", ctx)
        assert "H5P.MultiChoice" in out


class TestRenderConftest:
    def test_sphinx_testing_fixtures(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("conftest.py.j2", ctx)
        assert "sphinx.testing.fixtures" in out


# -------------------------------------------------------------------
# tox.ini rendering
# -------------------------------------------------------------------

class TestRenderToxIni:
    """Tests for tox.ini rendering."""

    def test_core_dep(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("tox.ini.j2", ctx)
        assert "../core" in out

    def test_env_names(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("tox.ini.j2", ctx)
        assert "unit-py3" in out
        assert "lint" in out
        assert "format" in out

    def test_pytest_command(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("tox.ini.j2", ctx)
        assert "pytest" in out


class TestRenderToxIniWithDeps:
    """Tests for tox.ini rendering with dependencies."""

    def test_dep_dirs(self) -> None:
        ctx = build_context(
            "H5P.MultiChoice", _MC_NAMES, deps=_SAMPLE_DEPS,
        )
        out = render("tox.ini.j2", ctx)
        assert "../font_awesome" in out
        assert "../joubel_ui" in out


# -------------------------------------------------------------------
# scaffold_one()
# -------------------------------------------------------------------

class TestScaffoldOne:
    """Tests for scaffold_one()."""

    def test_directory_structure(self, tmp_path: Path) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        scaffold_one("H5P.MultiChoice", _MC_NAMES, target)
        pkg = target / "multi_choice"
        assert pkg.is_dir()
        assert (pkg / "vendor").is_dir()
        assert (pkg / "tests" / "unit").is_dir()
        assert (
            pkg / "src"
            / "byteb4rb1e/sphinxcontrib/h5p/multi_choice"
        ).is_dir()

    def test_all_files_created(self, tmp_path: Path) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        scaffold_one("H5P.MultiChoice", _MC_NAMES, target)
        pkg = target / "multi_choice"
        assert (pkg / "pyproject.toml").is_file()
        assert (pkg / "Pipfile").is_file()
        assert (pkg / "Makefile").is_file()
        assert (pkg / "README.md").is_file()
        assert (pkg / "tox.ini").is_file()
        assert (pkg / "tests" / "unit" / "conftest.py").is_file()
        assert (
            pkg / "src"
            / "byteb4rb1e/sphinxcontrib/h5p/multi_choice"
            / "__init__.py"
        ).is_file()

    def test_idempotent_rerun(self, tmp_path: Path) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        scaffold_one("H5P.MultiChoice", _MC_NAMES, target)
        pkg = target / "multi_choice"
        first = (pkg / "pyproject.toml").read_text()
        scaffold_one("H5P.MultiChoice", _MC_NAMES, target)
        second = (pkg / "pyproject.toml").read_text()
        assert first == second


# -------------------------------------------------------------------
# bootstrap()
# -------------------------------------------------------------------

_GIT_MODULE = (
    "byteb4rb1e.sphinxcontrib.h5p.utils.pkg.bootstrap"
)


class TestParseEntry:
    """Tests for _parse_entry()."""

    def test_machine_name_only(self) -> None:
        assert _parse_entry("H5P.MultiChoice") == (
            "H5P.MultiChoice", "",
        )

    def test_machine_name_with_version(self) -> None:
        assert _parse_entry("H5P.MultiChoice@1.16") == (
            "H5P.MultiChoice", "1.16",
        )

    def test_version_with_dots(self) -> None:
        assert _parse_entry("H5P.MultiChoice@1.16.3") == (
            "H5P.MultiChoice", "1.16.3",
        )


class TestBootstrap:
    """Tests for the bootstrap() orchestrator."""

    def test_single_machine_name(
        self,
        pkg_index_file: Path,
        tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.MultiChoice"],
            target_dir=target,
            index=pkg_index_file,
        )
        assert rc == 0
        assert (
            target / "multi_choice" / "pyproject.toml"
        ).is_file()

    def test_machine_name_with_version(
        self,
        pkg_index_file: Path,
        tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.MultiChoice@1.16"],
            target_dir=target,
            index=pkg_index_file,
        )
        assert rc == 0

    def test_all_entries(
        self,
        pkg_index_file: Path,
        tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=[],
            all_entries=True,
            target_dir=target,
            index=pkg_index_file,
        )
        assert rc == 0
        assert (
            target / "multi_choice" / "pyproject.toml"
        ).is_file()
        assert (
            target / "joubel_ui" / "pyproject.toml"
        ).is_file()
        assert (
            target / "font_awesome" / "pyproject.toml"
        ).is_file()

    def test_missing_target(
        self, pkg_index_file: Path, tmp_path: Path,
    ) -> None:
        rc = bootstrap(
            entries=["H5P.MultiChoice"],
            target_dir=tmp_path / "nonexistent",
            index=pkg_index_file,
        )
        assert rc == 1

    def test_unknown_machine_name(
        self, pkg_index_file: Path, tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.Nonexistent"],
            target_dir=target,
            index=pkg_index_file,
        )
        assert rc == 1

    def test_missing_pkg_index(self, tmp_path: Path) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.MultiChoice"],
            target_dir=target,
            index=tmp_path / "missing.json",
        )
        assert rc == 1

    def test_with_lib_index_resolves_deps(
        self,
        pkg_index_file: Path,
        lib_index_file: Path,
        tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.MultiChoice@1.16"],
            target_dir=target,
            index=pkg_index_file,
            lib_index=lib_index_file,
        )
        assert rc == 0
        pyproject = (
            target / "multi_choice" / "pyproject.toml"
        ).read_text()
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.font_awesome"'
            in pyproject
        )
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.joubel_ui"'
            in pyproject
        )

    def test_without_lib_index_still_scaffolds(
        self,
        pkg_index_file: Path,
        tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        rc = bootstrap(
            entries=["H5P.MultiChoice@1.16"],
            target_dir=target,
            index=pkg_index_file,
            lib_index=tmp_path / "missing.json",
        )
        assert rc == 0
        assert (
            target / "multi_choice" / "pyproject.toml"
        ).is_file()


# -------------------------------------------------------------------
# _semver_key()
# -------------------------------------------------------------------

class TestSemverKey:
    """Tests for _semver_key()."""

    def test_three_parts(self) -> None:
        assert _semver_key("1.16.22") == (1, 16, 22)

    def test_two_parts(self) -> None:
        assert _semver_key("4.5") == (4, 5)

    def test_sorting(self) -> None:
        versions = ["1.16.0", "1.16.22", "1.15.3"]
        assert sorted(versions, key=_semver_key) == [
            "1.15.3", "1.16.0", "1.16.22",
        ]


# -------------------------------------------------------------------
# _resolve_version()
# -------------------------------------------------------------------

class TestResolveVersion:
    """Tests for _resolve_version()."""

    def test_exact_match(
        self, lib_index_data: Dict[str, Any],
    ) -> None:
        result = _resolve_version(
            "H5P.MultiChoice", "1.16.22", lib_index_data,
        )
        assert result is not None
        assert result["ref"] == (
            "aaa1111111111111111111111111111111111111"
        )

    def test_prefix_match_picks_highest_patch(
        self, lib_index_data: Dict[str, Any],
    ) -> None:
        result = _resolve_version(
            "H5P.MultiChoice", "1.16", lib_index_data,
        )
        assert result is not None
        # 1.16.22 > 1.16.0
        assert result["ref"] == (
            "aaa1111111111111111111111111111111111111"
        )

    def test_latest_when_no_version(
        self, lib_index_data: Dict[str, Any],
    ) -> None:
        result = _resolve_version(
            "H5P.MultiChoice", "", lib_index_data,
        )
        assert result is not None
        # 1.16.22 is the highest
        assert result["ref"] == (
            "aaa1111111111111111111111111111111111111"
        )

    def test_missing_machine_name(
        self, lib_index_data: Dict[str, Any],
    ) -> None:
        result = _resolve_version(
            "H5P.Nonexistent", "1.0", lib_index_data,
        )
        assert result is None

    def test_missing_version(
        self, lib_index_data: Dict[str, Any],
    ) -> None:
        result = _resolve_version(
            "H5P.MultiChoice", "9.99", lib_index_data,
        )
        assert result is None


# -------------------------------------------------------------------
# _resolve_deps()
# -------------------------------------------------------------------

class TestResolveDeps:
    """Tests for _resolve_deps()."""

    def test_resolves_known_deps(
        self, multi_choice_pkg_index: Dict[str, Any],
    ) -> None:
        dependencies = {
            "FontAwesome": "4.5",
            "H5P.JoubelUI": "1.3",
        }
        result = _resolve_deps(
            dependencies, multi_choice_pkg_index,
        )
        pip_names = [d["pip_name"] for d in result]
        assert (
            "byteb4rb1e.sphinxcontrib.h5p.font_awesome"
            in pip_names
        )
        assert (
            "byteb4rb1e.sphinxcontrib.h5p.joubel_ui"
            in pip_names
        )

    def test_skips_unknown_deps(
        self, multi_choice_pkg_index: Dict[str, Any],
    ) -> None:
        dependencies = {
            "FontAwesome": "4.5",
            "H5P.Unknown": "1.0",
        }
        result = _resolve_deps(
            dependencies, multi_choice_pkg_index,
        )
        assert len(result) == 1
        assert result[0]["pip_name"] == (
            "byteb4rb1e.sphinxcontrib.h5p.font_awesome"
        )

    def test_empty_deps(
        self, multi_choice_pkg_index: Dict[str, Any],
    ) -> None:
        result = _resolve_deps({}, multi_choice_pkg_index)
        assert result == []

    def test_includes_dir_name(
        self, multi_choice_pkg_index: Dict[str, Any],
    ) -> None:
        dependencies = {"FontAwesome": "4.5"}
        result = _resolve_deps(
            dependencies, multi_choice_pkg_index,
        )
        assert result[0]["dir_name"] == "font_awesome"


# -------------------------------------------------------------------
# Template rendering with deps
# -------------------------------------------------------------------

_SAMPLE_DEPS = [
    {
        "pip_name": "byteb4rb1e.sphinxcontrib.h5p.font_awesome",
        "dir_name": "font_awesome",
    },
    {
        "pip_name": "byteb4rb1e.sphinxcontrib.h5p.joubel_ui",
        "dir_name": "joubel_ui",
    },
]


class TestRenderPyprojectWithDeps:
    """Tests for pyproject.toml rendering with dependencies."""

    def test_deps_in_dependencies(self) -> None:
        ctx = build_context(
            "H5P.MultiChoice", _MC_NAMES, deps=_SAMPLE_DEPS,
        )
        out = render("pyproject.toml.j2", ctx)
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.font_awesome"' in out
        )
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.joubel_ui"' in out
        )

    def test_no_deps_no_extra_lines(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("pyproject.toml.j2", ctx)
        assert "font_awesome" not in out


class TestRenderPipfileWithDeps:
    """Tests for Pipfile rendering with dependencies."""

    def test_deps_editable(self) -> None:
        ctx = build_context(
            "H5P.MultiChoice", _MC_NAMES, deps=_SAMPLE_DEPS,
        )
        out = render("Pipfile.j2", ctx)
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.font_awesome" = '
            '{file = "../font_awesome", editable = true}'
            in out
        )
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.joubel_ui" = '
            '{file = "../joubel_ui", editable = true}'
            in out
        )


class TestRenderInitWithDeps:
    """Tests for __init__.py rendering with dependencies."""

    def test_deps_setup_extension(self) -> None:
        ctx = build_context(
            "H5P.MultiChoice", _MC_NAMES, deps=_SAMPLE_DEPS,
        )
        out = render("__init__.py.j2", ctx)
        assert (
            "app.setup_extension("
            "'byteb4rb1e.sphinxcontrib.h5p.font_awesome')"
            in out
        )
        assert (
            "app.setup_extension("
            "'byteb4rb1e.sphinxcontrib.h5p.joubel_ui')"
            in out
        )

    def test_no_deps_no_extra_setup(self) -> None:
        ctx = build_context("H5P.MultiChoice", _MC_NAMES)
        out = render("__init__.py.j2", ctx)
        assert out.count("setup_extension") == 1


# -------------------------------------------------------------------
# scaffold_one() with deps
# -------------------------------------------------------------------

class TestScaffoldOneWithDeps:
    """Tests for scaffold_one() with dependency data."""

    def test_deps_in_rendered_files(
        self, tmp_path: Path,
    ) -> None:
        target = tmp_path / "monorepo"
        target.mkdir()
        scaffold_one(
            "H5P.MultiChoice", _MC_NAMES, target,
            deps=_SAMPLE_DEPS,
        )
        pkg = target / "multi_choice"
        pyproject = (pkg / "pyproject.toml").read_text()
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.font_awesome"'
            in pyproject
        )
        pipfile = (pkg / "Pipfile").read_text()
        assert (
            '"byteb4rb1e.sphinxcontrib.h5p.joubel_ui"'
            in pipfile
        )
        init_py = (
            pkg / "src"
            / "byteb4rb1e/sphinxcontrib/h5p/multi_choice"
            / "__init__.py"
        ).read_text()
        assert "register_assets" in init_py
        assert (
            "app.setup_extension("
            "'byteb4rb1e.sphinxcontrib.h5p.font_awesome')"
            in init_py
        )
