import shutil
import hashlib
import json
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest
from src.domain.codegen.auth_users_adapter import install_auth_users_adapter
from src.domain.codegen.backend_guard import migrate_legacy_app_paths, validate_backend_repo
from src.domain.codegen.prompt_wrapper import build_platform_backend_capabilities_section


@pytest.mark.parametrize(
    "source",
    [
        "const users = []; users.push(body); return {success: true};",
        "const usersStore = new Map(); usersStore.set(id, body);",
        "const users = useStorage('users');",
        "const orders = useStorage('db:orders');",
        "await writeFile('data/users.json', JSON.stringify(body));",
        "import Database from 'better-sqlite3';",
        "import { JSONFilePreset } from 'lowdb/node';",
        "return { id: 'usr_seed_owner', email: 'private@example.com' };",
        "if (!process.env.CLOUD_GATEWAY_URL) { return localUsers; }",
        "const SEED_USERS = [{ email: 'private@example.com' }];",
        "const STORAGE_KEY = 'auth:users';",
        "console.warn('durable write skipped — serving from in-memory cache');",
    ],
)
def test_rejects_shadow_backend_without_leaking_source(source):
    with tempfile.TemporaryDirectory() as raw:
        route = Path(raw) / "server/api/admin/users.post.ts"
        route.parent.mkdir(parents=True)
        route.write_text(source, encoding="utf-8")
        with pytest.raises(ValueError, match="Cloud persistence contract failed") as error:
            validate_backend_repo(raw)
    assert "server/api/admin/users.post.ts" in str(error.value)
    assert "private@example.com" not in str(error.value)


@pytest.mark.parametrize("project_type", ["html", "react_app", "nuxt_app"])
def test_platform_adapters_and_ui_state_pass(project_type):
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        install_auth_users_adapter(raw, project_type)
        (root / "Users.vue").write_text(
            "const users = []; users.push(await createCloudUser(body)); "
            "localStorage.setItem('theme', 'dark');",
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_skips_dependencies_build_output_and_tests():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        for directory in ["node_modules", ".output", "tests"]:
            target = root / directory / "db.js"
            target.parent.mkdir()
            target.write_text("import db from 'better-sqlite3';", encoding="utf-8")
        validate_backend_repo(raw)


def test_skips_embedded_codegen_runner_and_p2hs_context():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        scripts = root / "scripts"
        scripts.mkdir()
        (scripts / "p2hs-codegen-action-runner.mjs").write_text(
            "const id = 'usr_seed_owner';\n"
            "if (!process.env.CLOUD_GATEWAY_URL) { return localUsers; }\n"
            "console.warn('durable write skipped — serving from in-memory cache');\n",
            encoding="utf-8",
        )
        context = root / ".p2hs" / "ai-context"
        context.mkdir(parents=True)
        (context / "notes.js").write_text("const id = 'usr_seed_owner';\n", encoding="utf-8")
        validate_backend_repo(raw)


def test_generated_actions_runner_passes_guard():
    from src.domain.codegen.actions_pipeline import build_codegen_actions_runner

    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        scripts = root / "scripts"
        scripts.mkdir()
        (scripts / "p2hs-codegen-action-runner.mjs").write_text(
            build_codegen_actions_runner(),
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_authoritative_form_block_requires_app_wiring():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "lib").mkdir()
        helper = "function submitContact() {}\n"
        (root / "lib" / "p2hs-app.js").write_bytes(helper.encode("utf-8"))
        digest = hashlib.sha256(helper.encode()).hexdigest()
        (root / "p2hs" / "platform-ports.lock.json").write_text(
            json.dumps({"files": {"lib/p2hs-app.js": digest}, "routes": []}),
            encoding="utf-8",
        )
        (root / "p2hs" / "app-blocks-plan.json").write_text(
            json.dumps(
                {
                    "adoption": "authoritative",
                    "blocks": [{"id": "forms.contact", "version": "1", "reason": "x"}],
                }
            ),
            encoding="utf-8",
        )
        (root / "p2hs" / "generation-plan.json").write_text(
            json.dumps({"runtime": {"authentication_required": False}}),
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="app_block_binding"):
            validate_backend_repo(raw)
        (root / "pages").mkdir()
        (root / "pages" / "contact.vue").write_text(
            "<script>submitGeneratedForm({ formKey: 'contact', values: { name: 'A' } })</script>",
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_canonical_generated_project_missing_generation_plan_fails_closed():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "app-blocks-plan.json").write_text(
            json.dumps({"adoption": "authoritative", "blocks": []}),
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="AUTH-GEN-000.*generation-plan.json"):
            validate_backend_repo(raw)


def test_legacy_project_without_generation_plan_remains_classifiable():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        validate_backend_repo(raw)
        (root / "pages").mkdir()
        (root / "pages" / "contact.vue").write_text(
            "<script>submitContact({ name: 'A' })</script>",
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_advisory_form_block_does_not_require_rewire():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "app-blocks-plan.json").write_text(
            json.dumps(
                {
                    "adoption": "advisory",
                    "blocks": [{"id": "forms.contact", "version": "1", "reason": "x"}],
                }
            ),
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_standalone_guard_fails_even_without_provision_manifest():
    import src.domain.codegen.backend_guard as guard

    with tempfile.TemporaryDirectory() as raw:
        (Path(raw) / "db.ts").write_text("const db = useStorage('users');", encoding="utf-8")
        result = subprocess.run(
            [sys.executable, "-c", Path(guard.__file__).read_text(encoding="utf-8"), raw],
            capture_output=True,
            text=True,
        )
    assert result.returncode == 1
    assert "Cloud persistence contract failed" in result.stderr


def test_standalone_guard_prepare_does_not_import_core_package():
    import src.domain.codegen.backend_guard as guard

    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "platform-ports.lock.json").write_text(
            json.dumps({"files": {}, "routes": []}), encoding="utf-8"
        )
        result = subprocess.run(
            [
                sys.executable,
                "-c",
                Path(guard.__file__).read_text(encoding="utf-8"),
                raw,
                "-",
                "--prepare",
            ],
            input=json.dumps({"files": {}, "routes": []}),
            capture_output=True,
            text=True,
        )
    assert result.returncode == 0, result.stderr


def test_missing_contract_never_authorizes_local_fallback():
    prompt = build_platform_backend_capabilities_section(None)
    assert "must still use Cloud records" in prompt
    assert "never return fake success" in prompt
    assert "HTTP 503" in prompt
    assert "generated Worker may only render UI and thin BFF routes" in prompt
    assert "If no customer backend is provided, use P2HS Cloud" in prompt


def test_actions_embeds_guard_before_bundle_delivery_and_push():
    from src.domain.codegen.actions_pipeline import build_codegen_actions_runner

    runner = build_codegen_actions_runner()
    assert "__P2HS_BACKEND_GUARD__" not in runner
    assert runner.index("const backendCheck =") < runner.index("if (String(payload.result_delivery")
    guard = runner.index("const backendCheck =")
    # everything that publishes the agent's work must come after the guard: the legacy push to
    # preview and, with a work branch, the checkpoint pushes and the promotion (call sites, not the
    # helper definitions, which sit above the main flow)
    assert guard < runner.index('log("Pushing updated changes to preview")')
    assert guard < runner.index('await pushCheckpoint(payload, workBranch, "agent output")')
    assert guard < runner.index("await promoteToPreview(payload)")
    assert "__P2HS_SHADOW_STUBS__" not in runner
    if shutil.which("node"):
        result = subprocess.run(
            ["node", "--input-type=module", "--check"],
            input=runner,
            capture_output=True,
            text=True,
        )
        assert result.returncode == 0, result.stderr


def test_rejects_dual_store_helper_module():
    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "server/utils/user-db.ts"
        path.parent.mkdir(parents=True)
        path.write_text("export async function listDbUsers() { return [] }", encoding="utf-8")
        with pytest.raises(ValueError, match="local dual-store module"):
            validate_backend_repo(raw)


def test_retired_dual_store_stub_is_allowed():
    from src.domain.codegen.legacy_store_retire import retire_shadow_store_files

    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "server/utils/user-db.ts"
        path.parent.mkdir(parents=True)
        path.write_text(
            "const STORAGE_KEY = 'auth:users'\n"
            "const SEED_USERS = [{ email: 'private@example.com' }]\n"
            "return { id: 'usr_seed_owner' }\n",
            encoding="utf-8",
        )
        retired = retire_shadow_store_files(raw)
        validate_backend_repo(raw)
    assert retired == ["server/utils/user-db.ts"]


def test_refresh_retired_local_auth_exports_session_helpers():
    from src.domain.codegen.legacy_store_retire import retire_shadow_store_files

    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "server/utils/local-auth.ts"
        path.parent.mkdir(parents=True)
        path.write_text("/* __P2HS_RETIRED_LOCAL_STORE__ */\nexport async function localSignIn() {}\n")
        assert retire_shadow_store_files(raw) == []
        refreshed = retire_shadow_store_files(raw, refresh_stubs=True)
        source = path.read_text(encoding="utf-8")
    assert refreshed == ["server/utils/local-auth.ts"]
    assert "localGetCurrentUser" in source
    assert "localSignOut" in source
    assert "getPlatformUserById" in source


def test_prompt_forbids_dual_store_helpers():
    prompt = build_platform_backend_capabilities_section({"rules": []})
    assert "user-db.ts" in prompt
    assert "merge Cloud lists" in prompt
    assert "HTTP 503" in prompt
    assert "customer_backend is omitted or none" in prompt
    assert "workspace_owner_email" in prompt


def test_rejects_homemade_magic_link_landing():
    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "js" / "be-admin.js"
        path.parent.mkdir(parents=True)
        path.write_text(
            "const token = new URLSearchParams(location.search).get('token');\n"
            "await fetch('/api/auth/magic-link/verify', { method: 'POST', body: JSON.stringify({ token }) });\n",
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="client-side magic-link consume"):
            validate_backend_repo(raw)


def test_rejects_split_client_side_magic_link_consume():
    """Real bug shape: token read and verify() call in separate functions, far apart."""
    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "js" / "be-admin.js"
        path.parent.mkdir(parents=True)
        filler = "\n".join(f"  // unrelated dashboard logic line {i}" for i in range(60))
        path.write_text(
            "async function verifyMagicLinkToken(token) {\n"
            "  const resp = await fetch('/api/auth/magic-link/verify', {\n"
            "    method: 'POST', body: JSON.stringify({ token }),\n"
            "  });\n"
            "  return resp.json();\n"
            "}\n"
            f"{filler}\n"
            "async function init() {\n"
            "  const params = new URLSearchParams(window.location.search);\n"
            "  const token = params.get('token');\n"
            "  if (token) await verifyMagicLinkToken(token);\n"
            "}\n",
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="client-side magic-link consume"):
            validate_backend_repo(raw)


def test_migrates_legacy_staff_dashboard_onto_consume_route():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        path = root / "js" / "be-admin.js"
        path.parent.mkdir(parents=True)
        path.write_text(
            "const token = new URLSearchParams(location.search).get('token');\n"
            "await fetch('/api/auth/magic-link/verify', { method: 'POST', body: JSON.stringify({ token }) });\n",
            encoding="utf-8",
        )
        html = root / "be-admin.html"
        html.write_text('<a href="/admin/magic">sign in</a>\n', encoding="utf-8")
        (root / "p2hs").mkdir()
        (root / "p2hs" / "runtime-contract.json").write_text(
            '{"routes": {"admin_panel_path": "/be-admin"}}', encoding="utf-8"
        )
        changed = migrate_legacy_app_paths(raw)
        assert "js/be-admin.js" in changed
        assert "be-admin.html" in changed
        source = path.read_text(encoding="utf-8")
        assert "magic-link/verify" not in source
        assert "/api/auth/magic-link/consume" in source
        assert "__P2HS_MAGIC_CONSUME_MIGRATED__" in source
        assert "/admin/magic" not in html.read_text(encoding="utf-8")
        assert "/be-admin" in html.read_text(encoding="utf-8")
        validate_backend_repo(raw)


def test_migrates_leftover_contact_post_and_inbox_urls():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        leftover = root / "functions" / "api" / "contact.js"
        leftover.parent.mkdir(parents=True)
        leftover.write_text("// leftover platform contact submit\n", encoding="utf-8")
        js = root / "js" / "main.js"
        js.parent.mkdir(parents=True)
        js.write_text(
            "await fetch('https://api.resend.com/emails', { method: 'POST' });\n"
            "await fetch('/api/send-email', { method: 'POST', body: JSON.stringify(payload) });\n",
            encoding="utf-8",
        )
        admin = root / "js" / "be-admin.js"
        admin.write_text(
            "const rows = await fetch('/api/submissions').then((r) => r.json());\n",
            encoding="utf-8",
        )
        changed = migrate_legacy_app_paths(raw)
        assert "js/main.js" in changed
        assert "js/be-admin.js" in changed
        main = js.read_text(encoding="utf-8")
        assert "api.resend.com" not in main
        assert "/api/send-email" not in main
        assert main.count("/api/contact") == 2
        inbox = admin.read_text(encoding="utf-8")
        assert "/api/submissions" not in inbox
        assert "/api/contact/inbox" in inbox
        validate_backend_repo(raw)


def test_does_not_rewrite_platform_contact_adapter():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        path = root / "functions" / "_lib" / "p2hs-contact.mjs"
        path.parent.mkdir(parents=True)
        original = "export async function post() { return fetch('/api/send-email'); } // p2hs-contact\n"
        path.write_text(original, encoding="utf-8")
        assert migrate_legacy_app_paths(raw) == []
        assert path.read_text(encoding="utf-8") == original


def test_rejects_browser_cloud_magic_link_verify():
    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "js" / "be-admin.js"
        path.parent.mkdir(parents=True)
        path.write_text(
            "const token = new URLSearchParams(location.search).get('token');\n"
            "await fetch(cloudGatewayUrl + '/v1/projects/' + projectId + '/auth/magic-link/verify', {\n"
            "  method: 'POST', body: JSON.stringify({ token }),\n"
            "});\n",
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="client-side magic-link consume"):
            validate_backend_repo(raw)


def _write_contract(root: Path, admin_panel_path: str) -> None:
    (root / "p2hs").mkdir(parents=True, exist_ok=True)
    (root / "p2hs" / "runtime-contract.json").write_text(
        f'{{"routes": {{"admin_panel_path": "{admin_panel_path}"}}}}', encoding="utf-8"
    )


@pytest.mark.parametrize(
    "source",
    [
        "location.replace('/admin');",
        'location.assign("/admin");',
        "location.href = '/admin';",
        'return json({ next: "/admin" });',
        "redirect('/admin')",
        '<a href="/admin">Staff</a>',
    ],
)
def test_rejects_hardcoded_admin_redirect_when_contract_names_custom_path(source):
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        _write_contract(root, "/be-admin")
        path = root / "js" / "dashboard.js"
        path.parent.mkdir(parents=True)
        path.write_text(source, encoding="utf-8")
        with pytest.raises(ValueError, match="hardcoded /admin redirect"):
            validate_backend_repo(raw)


def test_allows_hardcoded_admin_redirect_when_contract_absent():
    """Older project that hasn't regenerated since Phase 4 -- no contract file,
    check is skipped rather than treated as a violation."""
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        path = root / "js" / "dashboard.js"
        path.parent.mkdir(parents=True)
        path.write_text("location.replace('/admin');", encoding="utf-8")
        validate_backend_repo(raw)


def test_allows_hardcoded_admin_redirect_when_contract_names_default_path():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        _write_contract(root, "/admin")
        path = root / "js" / "dashboard.js"
        path.parent.mkdir(parents=True)
        path.write_text("location.replace('/admin');", encoding="utf-8")
        validate_backend_repo(raw)


def test_allows_longer_admin_scoped_path_even_with_custom_contract():
    """/admin/reset-password is a real, still-valid platform path -- must not be
    confused with the bare "/admin" literal this check targets."""
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        _write_contract(root, "/be-admin")
        path = root / "js" / "dashboard.js"
        path.parent.mkdir(parents=True)
        path.write_text("location.replace('/admin/reset-password');", encoding="utf-8")
        validate_backend_repo(raw)


@pytest.mark.parametrize(
    "source",
    [
        'import { Resend } from "resend";',
        'const nodemailer = require("nodemailer");',
        'const transport = "smtp://user:pass@smtp.example.com:587";',
    ],
)
def test_rejects_vendor_email_client_regardless_of_contract(source):
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        path = root / "js" / "mailer.js"
        path.parent.mkdir(parents=True)
        path.write_text(source, encoding="utf-8")
        with pytest.raises(ValueError, match="vendor email client"):
            validate_backend_repo(raw)


def test_rejects_admin_magic_path():
    with tempfile.TemporaryDirectory() as raw:
        path = Path(raw) / "be-admin.html"
        path.write_text('<a href="/admin/magic">sign in</a>', encoding="utf-8")
        with pytest.raises(ValueError, match="homemade magic-link landing"):
            validate_backend_repo(raw)


def test_platform_magic_consume_adapter_passes_guard():
    from src.domain.codegen.contact_adapter import contact_adapter_files

    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        for rel, content in contact_adapter_files("html").items():
            target = root / rel
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content, encoding="utf-8")
        validate_backend_repo(raw)


def test_student_consulting_checkout_uses_retired_local_stores():
    root = Path(
        r"E:\workroom\2026\sites.p2hs.com\public"
        r"\5dd80e82-04a8-47f5-8fed-7e533b75de1b"
        r"\296357f8-07a3-4085-b136-e9e2967b8356"
    )
    if not root.is_dir():
        pytest.skip("downloaded generated checkout is not present")
    validate_backend_repo(str(root))
    user_db = root / "server/utils/user-db.ts"
    if user_db.is_file():
        source = user_db.read_text(encoding="utf-8")
        assert "__P2HS_RETIRED_LOCAL_STORE__" in source
        assert "private@example.com" not in source
        assert "pbkdf2:" not in source


def test_rejects_missing_cloud_write_when_persistence_required():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "runtime-contract.json").write_text(
            json.dumps({"requirements": {"persistence_required": True}}),
            encoding="utf-8",
        )
        (root / "js").mkdir()
        (root / "js" / "app.js").write_text("console.log('ui only');\n", encoding="utf-8")
        with pytest.raises(ValueError, match="persistence_required"):
            validate_backend_repo(raw)


def test_allows_cloud_write_marker_when_persistence_required():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "runtime-contract.json").write_text(
            json.dumps({"requirements": {"persistence_required": True}}),
            encoding="utf-8",
        )
        (root / "js").mkdir()
        (root / "js" / "app.js").write_text(
            "export function submit() { return submitContact({ name: 'Ada' }); }\n",
            encoding="utf-8",
        )
        validate_backend_repo(raw)


def test_rejects_missing_staff_read_when_admin_must_read_persisted():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "runtime-contract.json").write_text(
            json.dumps(
                {
                    "requirements": {
                        "persistence_required": True,
                        "admin_must_read_persisted": True,
                    }
                }
            ),
            encoding="utf-8",
        )
        (root / "js").mkdir()
        (root / "js" / "app.js").write_text(
            "export function submit() { return submitContact({ name: 'Ada' }); }\n",
            encoding="utf-8",
        )
        with pytest.raises(ValueError, match="admin_must_read_persisted"):
            validate_backend_repo(raw)


def test_allows_inbox_read_when_admin_must_read_persisted():
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "runtime-contract.json").write_text(
            json.dumps(
                {
                    "requirements": {
                        "persistence_required": True,
                        "admin_must_read_persisted": True,
                    }
                }
            ),
            encoding="utf-8",
        )
        (root / "js").mkdir()
        (root / "js" / "app.js").write_text(
            "export async function boot() {\n"
            "  await submitContact({ name: 'Ada' });\n"
            "  return listInbox();\n"
            "}\n",
            encoding="utf-8",
        )
        validate_backend_repo(raw)
