"""S2: generated create uses runtime substrate + starter_assembly, not boilerplate copy."""

from __future__ import annotations

import asyncio
import json
import os
import shutil
from pathlib import Path

import pytest
from src.domain.codegen.port_adapter import install_catalog_adapters
from src.domain.codegen.repo_export import assert_export_clean, select_publishable_paths
from src.domain.projects.bootstrap_contract import (
    CONNECTED_REPO_MODE,
    NEW_PROJECT_BOOTSTRAP_MODE,
)
from src.domain.starter_library.runtime_substrate import (
    BOOTSTRAP_VERSION,
    assemble_generated_project,
    uses_starter_assembly,
    write_generated_project_tree,
)
from src.workers.project_setup_pipeline import (
    _materialize_generated_lockfile,
    _materialize_generated_source,
    _materialize_initial_repo_tree,
)
from src.workers.tasks.apply_boilerplate import _boilerplate_tree_present

ROOT = Path(__file__).resolve().parents[1]
PIPELINE = (ROOT / "src/workers/project_setup_pipeline.py").read_text(encoding="utf-8")
APPLY = (ROOT / "src/workers/tasks/apply_boilerplate.py").read_text(encoding="utf-8")

_PRODUCT_FILES = (
    "components/starter/RecordCreate.vue",
    "src/components/starter/RecordCreate.jsx",
    "src/pages/Documents.tsx",
    "pages/documents/index.vue",
    "p2hs.platform.json",
    "composables/useP2hsApi.ts",
)
_PUBLIC_MARKERS = (
    "NUXT_PUBLIC_P2HS_GATEWAY_URL",
    "VITE_P2HS_GATEWAY_URL",
    "NUXT_PUBLIC_P2HS_CLOUD_PUBLIC_URL",
    "VITE_P2HS_CLOUD_PUBLIC_URL",
    "NUXT_PUBLIC_P2HS_CORE_PUBLIC_URL",
    "VITE_P2HS_CORE_PUBLIC_URL",
    "gatewayPublicUrl",
    "cloudgate.p2hs.com",
)


class _Settings:
    boilerplate_snapshot_dir = ""
    github_template_installation_id = 0
    github_installation_id = 0
    hosting_platform_projects_created_after = ""
    hosting_provider = "cloudflare_pages"


def _relatives(root: Path) -> list[str]:
    relatives: list[str] = []
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [name for name in dirnames if name not in {".git", "node_modules"}]
        for name in filenames:
            relatives.append((Path(dirpath) / name).relative_to(root).as_posix())
    return relatives


def _assert_not_product(root: Path) -> None:
    for relative in _PRODUCT_FILES:
        assert not (root / relative).exists(), relative
    joined = "\n".join(
        (root / relative).read_text(encoding="utf-8")
        for relative in _relatives(root)
        if (root / relative).is_file()
    )
    for marker in _PUBLIC_MARKERS:
        assert marker not in joined, marker


def test_pages_types_use_starter_assembly_and_imports_do_not():
    assert NEW_PROJECT_BOOTSTRAP_MODE == "starter_assembly"
    assert CONNECTED_REPO_MODE == "adopt_and_patch"
    assert uses_starter_assembly("nuxt_app") is True
    assert uses_starter_assembly("react_app") is True
    assert uses_starter_assembly("html") is True
    assert uses_starter_assembly("shopify_app") is False
    assert uses_starter_assembly("ai_assist_wordpress") is False


def test_substrate_is_not_live_boilerplate_copy():
    files = assemble_generated_project("nuxt_app")
    assert "package.json" in files
    assert "nuxt.config.ts" in files
    assert "pages/index.vue" in files
    assert "p2hs.platform.json" not in files
    assert all("components/starter/" not in name for name in files)
    joined = "\n".join(files.values())
    for marker in _PUBLIC_MARKERS:
        assert marker not in joined


@pytest.mark.parametrize("project_type", ["nuxt_app", "react_app", "html"])
def test_write_generated_tree_is_publishable(tmp_path, project_type):
    version = write_generated_project_tree(str(tmp_path), project_type)
    assert version == BOOTSTRAP_VERSION
    _assert_not_product(tmp_path)
    staged = select_publishable_paths(_relatives(tmp_path))
    assert_export_clean(tmp_path, staged)


def test_optional_starter_overlay_stays_thin(tmp_path):
    write_generated_project_tree(str(tmp_path), "nuxt_app", starter_id="case_operations")
    assert (tmp_path / "layouts/starter.vue").is_file()
    assert (tmp_path / "public/starter-shell.json").is_file()
    assert not (tmp_path / "server/api").exists()
    assert not (tmp_path / "functions/api").exists()
    shell = (tmp_path / "public/starter-shell.json").read_text(encoding="utf-8")
    assert '"bff": "/api"' in shell
    _assert_not_product(tmp_path)


def test_generated_source_helper_does_not_copy_snapshot(tmp_path):
    version = _materialize_generated_source(
        str(tmp_path), "nuxt_app", _Settings(), {}
    )
    assert version == "starter_assembly"
    assert (tmp_path / "nuxt.config.ts").is_file()
    _assert_not_product(tmp_path)


@pytest.mark.asyncio
@pytest.mark.parametrize("project_type", ["nuxt_app", "react_app", "html"])
async def test_materialize_create_path_assembles_then_installs_adapters(tmp_path, project_type):
    result = await _materialize_initial_repo_tree(
        str(tmp_path),
        project_type,
        "pabcdef12",
        _Settings(),
        template_source={"kind": "generated"},
    )
    assert result["version"] == "starter_assembly"
    _assert_not_product(tmp_path)
    if project_type == "nuxt_app":
        assert (tmp_path / "server/api").is_dir() or (tmp_path / "functions").is_dir()
        assert (tmp_path / "package.json").is_file()
    elif project_type == "react_app":
        assert (tmp_path / "functions").is_dir()
        assert not (tmp_path / "src/pages/Documents.tsx").exists()
    else:
        assert (tmp_path / "index.html").is_file()
        html = (tmp_path / "index.html").read_text(encoding="utf-8")
        assert "p2hs-core-public-url" not in html
        assert "p2hs-cloud-public-url" not in html
    staged = select_publishable_paths(_relatives(tmp_path))
    assert_export_clean(tmp_path, staged)


@pytest.mark.asyncio
async def test_generated_lockfile_repairs_stale_boilerplate_lock(tmp_path):
    npm = shutil.which("npm.cmd" if os.name == "nt" else "npm")
    if not npm:
        pytest.skip("npm is required for generated lockfile validation")
    (tmp_path / "package.json").write_text(
        json.dumps(
            {
                "name": "p2hs-lockfile-fixture",
                "version": "0.0.1",
                "private": True,
                "dependencies": {"is-number": "^7.0.0"},
            }
        ),
        encoding="utf-8",
    )
    # This shape mirrors the stale v2 lock that caused the W4P workflow to
    # fail: the package tree advertises a dependency, but the legacy tree does
    # not contain it.
    (tmp_path / "package-lock.json").write_text(
        json.dumps(
            {
                "name": "p2hs-lockfile-fixture",
                "version": "0.0.1",
                "lockfileVersion": 2,
                "requires": True,
                "packages": {
                    "": {
                        "dependencies": {"is-number": "^7.0.0"}
                    }
                },
                "dependencies": {},
            }
        ),
        encoding="utf-8",
    )

    await _materialize_generated_lockfile(str(tmp_path))

    check = await asyncio.create_subprocess_exec(
        npm,
        "ci",
        "--dry-run",
        "--ignore-scripts",
        "--no-audit",
        "--no-fund",
        cwd=tmp_path,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    await check.communicate()
    assert check.returncode == 0


def test_create_path_then_adapters_do_not_reintroduce_boilerplate(tmp_path):
    write_generated_project_tree(str(tmp_path), "nuxt_app")
    install_catalog_adapters(str(tmp_path), "nuxt_app", {"legacy_contact_submit": False})
    _assert_not_product(tmp_path)
    staged = select_publishable_paths(_relatives(tmp_path))
    assert_export_clean(tmp_path, staged)


def test_pipeline_keeps_import_and_template_adopt_paths():
    assert 'source_kind == "github_import"' in PIPELINE
    assert "_copy_import_tree" in PIPELINE
    assert "await clone_repo(template_clone_url, repo_path" in PIPELINE
    assert "write_generated_project_tree" in PIPELINE
    assert "apply_live_boilerplate(repo_path, project_type)" in PIPELINE
    generated_helper = (
        ROOT / "src/workers/project_setup_pipeline.py"
    ).read_text(encoding="utf-8")
    assert "def _materialize_generated_source" in generated_helper
    assert "uses_starter_assembly(project_type)" in generated_helper


def test_setup_pipeline_initializes_optional_framework_label_before_resume_path():
    declaration = "detected_framework_label: Optional[str] = None"
    assignment = 'detected_framework_label = materialized.get("detected_framework")'
    use = "if detected_framework_label:"
    assert PIPELINE.index(declaration) < PIPELINE.index(use)
    assert PIPELINE.index(assignment) < PIPELINE.index(use)


def test_legacy_heal_skips_existing_pages_tree():
    assert "uses_starter_assembly(project_type)" in APPLY
    assert "write_generated_project_tree" in APPLY
    assert "_boilerplate_tree_present" in APPLY
    assert "apply_boilerplate_skip_existing_pages_tree" in APPLY


def test_generated_create_source_has_no_github_repo():
    src = Path(__file__).resolve().parents[1] / "src/domain/projects/service.py"
    text = src.read_text(encoding="utf-8")
    assert "suggest: bool = False" in text
    assert "if not suggest:" in text


def test_recommend_still_suggests_without_binding_create():
    src = Path(__file__).resolve().parents[1] / "src/domain/projects/service.py"
    text = src.read_text(encoding="utf-8")
    assert "suggest=True" in text
    assert "suggest=False" in text


def test_resume_heal_does_not_overwrite_existing_nuxt(tmp_path):
    existing = tmp_path / "nuxt.config.ts"
    existing.write_text("export default defineNuxtConfig({ ssr: true })\n", encoding="utf-8")

    assert uses_starter_assembly("nuxt_app") is True
    assert _boilerplate_tree_present(str(tmp_path), "nuxt_app") is True
    assert not (tmp_path / "pages/index.vue").exists()
