import hashlib
import json
import subprocess
from html.parser import HTMLParser
from pathlib import Path
from types import SimpleNamespace

import pytest

from hessian_recipes.hf_recipe_hub import (
    COLLECTION_ITEMS, NAVIGATION, RECIPES, build, publish, publish_collection,
    render_markdown, validate_links,
)
from hessian_recipes.hub_content import SFT, render_evidence


@pytest.fixture
def repository(tmp_path):
    root = tmp_path / "repository"
    root.mkdir()
    required = {target.split("#")[0] for _, target, _, _ in RECIPES}
    required |= {target for _, target in NAVIGATION if target.endswith(".md")}
    required |= {"README.md", "pyproject.toml", "src/hessian_recipes/__init__.py",
                 "benchmarks/starter/data.jsonl", "requirements/training.txt"}
    for name in required:
        path = root / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text("# Example\n\n## Distillation without a hosted teacher\n\n## Prime Intellect Verifiers\n")
    (root / "README.md").write_text("# Root\n\n[Code](src/hessian_recipes/__init__.py)\n\n## First run: evaluation without a GPU\n")
    (root / "src/hessian_recipes/__init__.py").write_text('VERSION = "0.1"\n')
    for name in ["runs/private.json", ".cache/token.json", ".venv/private.py", "unrelated.json"]:
        path = root / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text("private excluded bytes")
    subprocess.run(["git", "init", "-q", str(root)], check=True)
    subprocess.run(["git", "add", "."], cwd=root, check=True)
    subprocess.run(["git", "-c", "user.name=Tests", "-c", "user.email=tests@example.invalid",
                    "commit", "-qm", "fixture"], cwd=root, check=True)
    return root


def test_build_includes_runnable_source_and_excludes_unrelated_inputs(repository, tmp_path):
    output = tmp_path / "bundle"
    manifest = build(repository, output)
    assert manifest["intended_visibility"] == "private"
    assert manifest["recipe_count"] == 11
    assert manifest["link_validation"]["broken_local_links"] == 0
    assert manifest["uncommitted_source_paths"] == []
    assert "src/hessian_recipes/__init__.py" in manifest["source_sha256"]
    assert "benchmarks/starter/data.jsonl" in manifest["source_sha256"]
    assert all(not name.startswith(("runs/", ".cache/", ".venv/")) for name in manifest["source_sha256"])
    assert "unrelated.json" not in manifest["source_sha256"]
    assert "sdk: static" in (output / "README.md").read_text()
    for name, digest in manifest["files_sha256"].items():
        assert hashlib.sha256((output / name).read_bytes()).hexdigest() == digest
    assert 'href="sources/src/hessian_recipes/__init__.py"' in (output / "pages/README.html").read_text()
    with pytest.raises(ValueError, match="fresh"):
        build(repository, output)


def test_dirty_source_bytes_are_explicitly_recorded(repository, tmp_path):
    path = repository / "src/hessian_recipes/__init__.py"
    path.write_text('VERSION = "dirty"\n')
    manifest = build(repository, tmp_path / "bundle")
    assert manifest["uncommitted_source_paths"] == ["src/hessian_recipes/__init__.py"]
    assert manifest["source_sha256"]["src/hessian_recipes/__init__.py"] == hashlib.sha256(path.read_bytes()).hexdigest()


def test_generated_pages_allow_hf_image_cdn_without_expanding_script_or_network_access(repository, tmp_path):
    class PolicyParser(HTMLParser):
        def __init__(self):
            super().__init__()
            self.policies = []

        def handle_starttag(self, tag, attrs):
            attributes = dict(attrs)
            if tag == "meta" and attributes.get("http-equiv", "").lower() == "content-security-policy":
                self.policies.append(attributes["content"])

    output = tmp_path / "bundle"
    build(repository, output)
    image_origin = "https://us.aws.cdn.hf.co"
    for page in output.rglob("*.html"):
        parser = PolicyParser()
        parser.feed(page.read_text())
        assert len(parser.policies) == 1, page
        directives = dict((parts[0], parts[1:]) for directive in parser.policies[0].split(";")
                          if (parts := directive.split()))
        assert directives["img-src"] == ["'self'", "data:", image_origin], page
        assert directives["script-src"] == ["'self'"], page
        assert directives.get("connect-src", directives["default-src"]) == ["'self'"], page
        assert all(image_origin not in sources for name, sources in directives.items() if name != "img-src"), page


def test_brand_assets_are_exact_and_other_binary_files_are_excluded(repository, tmp_path):
    brand = repository / "examples/huggingface/brand"
    brand.mkdir(parents=True)
    assets = {"hessian-ai-logo.svg": b'<svg xmlns="http://www.w3.org/2000/svg"/>',
              "Poppins-Regular.woff2": b"wOF2\x00\xfffixture",
              "favicon-32x32.png": b"\x89PNG\r\n\x1a\nfixture",
              "jaif-logo.png": b"\x89PNG\r\n\x1a\nJAIF fixture"}
    for name, raw in assets.items():
        (brand / name).write_bytes(raw)
    (brand / "unreviewed.svg").write_bytes(b"excluded")
    (repository / "examples/unrelated.woff2").write_bytes(b"excluded")
    subprocess.run(["git", "add", "examples"], cwd=repository, check=True)
    output = tmp_path / "bundle"
    manifest = build(repository, output)
    for name, raw in assets.items():
        assert (output / "assets/brand" / name).read_bytes() == raw
        assert (output / "sources/examples/huggingface/brand" / name).read_bytes() == raw
    assert "examples/huggingface/brand/unreviewed.svg" not in manifest["source_sha256"]
    assert "examples/unrelated.woff2" not in manifest["source_sha256"]
    assert 'src="assets/brand/hessian-ai-logo.svg"' in (output / "index.html").read_text()
    assert 'href="assets/brand/favicon-32x32.png"' in (output / "pages/README.html").read_text()


def test_rejects_selected_symlink_and_credential_file(repository, tmp_path):
    code = repository / "src/hessian_recipes/__init__.py"
    code.unlink()
    code.symlink_to(repository / "README.md")
    with pytest.raises(ValueError, match="regular"):
        build(repository, tmp_path / "symlink-bundle")
    code.unlink()
    code.write_text("")
    secret = repository / "examples/credentials.json"
    secret.parent.mkdir(exist_ok=True)
    secret.write_text("do not upload")
    subprocess.run(["git", "add", str(secret)], cwd=repository, check=True)
    with pytest.raises(ValueError, match="allowlisted"):
        build(repository, tmp_path / "secret-bundle")


def test_renderer_escapes_html_rejects_unsafe_urls_and_links_same_page_anchors():
    rendered = render_markdown('# Title\n\n<script>alert(1)</script> [bad](javascript:alert) [local](#title)',
                               "docs/page.md", {"docs/page.md"}, "abc")
    assert "<script>" not in rendered and "javascript:" not in rendered
    assert '&lt;script&gt;' in rendered
    assert 'href="pages/docs--page.html#title"' in rendered


def test_article_navigation_targets_current_document_with_base_url(repository, tmp_path):
    source = repository / "docs/training.md"
    source.write_text("# Training\n\n## Distillation without a hosted teacher\n\n```python\nprint('example')\n```\n")
    output = tmp_path / "bundle"
    build(repository, output)
    article = (output / "pages/docs--training.html").read_text()
    # A fragment-only link would target the home page because of <base href="../">.
    assert 'href="pages/docs--training.html#distillation-without-a-hosted-teacher"' in article
    assert 'href="pages/docs--training.html#main"' in article
    assert 'href="#distillation-without-a-hosted-teacher"' not in article
    assert 'id="code-1"' in article and 'data-copy-target="code-1"' in article
    assert validate_links(output)["broken_local_links"] == 0


def test_result_chart_refuses_inconsistent_aggregate_counts():
    raw = (Path(__file__).resolve().parents[1] / SFT).read_bytes()
    def link(path):
        return "https://example.invalid/" + path
    rendered = render_evidence({SFT: raw}, link)
    assert 'id="sft-mgsm-before" min="0" max="64" value="28"' in rendered
    assert 'id="sft-mgsm-after" min="0" max="64" value="30"' in rendered
    assert 'Inconclusive' in rendered
    data = json.loads(raw)
    data["candidate"]["correct"] = 65
    fallback = render_evidence({SFT: json.dumps(data).encode()}, link)
    assert 'id="sft-mgsm-before"' not in fallback and 'id="sft-mgsm-after"' not in fallback
    assert 'Scores are shown only when complete aggregate counts are available.' in fallback


def test_rejects_credential_literal_inside_an_otherwise_allowlisted_file(repository, tmp_path):
    (repository / "src/hessian_recipes/__init__.py").write_text('TOKEN = "hf_' + "x" * 32 + '"\n')
    with pytest.raises(ValueError, match="Credential-like"):
        build(repository, tmp_path / "bundle")


def test_links_check_anchors_not_only_file_existence(repository, tmp_path):
    output = tmp_path / "bundle"
    build(repository, output)
    path = output / "index.html"
    path.write_text(path.read_text().replace('href="index.html"', 'href="index.html#missing"', 1))
    with pytest.raises(ValueError, match="anchor"):
        validate_links(output)


class FakeAPI:
    def __init__(self, *, private=True, owner="ctauchmann", exists=False, visibility="private",
                 token=None, remote_files=None):
        self.private, self.owner, self.exists = private, owner, exists
        self.visibility = visibility
        self.created, self.uploaded = [], []
        self.token, self.remote_files = token, remote_files or []

    def whoami(self):
        return {"name": self.owner}

    def repo_exists(self, *args, **kwargs):
        return self.exists

    def create_repo(self, repo_id, **kwargs):
        self.created.append((repo_id, kwargs))
        self.exists = True

    def repo_info(self, repo_id, **kwargs):
        return SimpleNamespace(id=repo_id, private=self.private, visibility=self.visibility, sdk="static",
                               sha=kwargs.get("revision", "parent"))

    def upload_folder(self, **kwargs):
        self.uploaded.append(kwargs)
        return SimpleNamespace(oid="published-commit")

    def list_repo_files(self, repo_id, **kwargs):
        assert repo_id == "ctauchmann/hessian-recipes" and kwargs["revision"] == "parent"
        return self.remote_files


def test_publish_checks_owner_private_and_parent_commit(repository, tmp_path):
    output = tmp_path / "bundle"
    build(repository, output)
    api = FakeAPI()
    receipt = publish(output, "ctauchmann/hessian-recipes", api=api)
    assert api.created[0][1] == {"repo_type": "space", "space_sdk": "static", "private": True, "exist_ok": False}
    assert api.uploaded[0]["parent_commit"] == "parent"
    assert receipt["private"] and receipt["commit"] == "published-commit"
    for rejected in [FakeAPI(owner="other"), FakeAPI(private=False, exists=True),
                     FakeAPI(visibility="protected", exists=True)]:
        with pytest.raises(ValueError):
            publish(output, "ctauchmann/hessian-recipes", api=rejected)
        assert not rejected.uploaded
    with pytest.raises(ValueError, match="approved"):
        publish(output, "other/hessian-recipes", api=api)


def test_repository_scoped_publisher_uses_synthetic_identity_and_same_private_target(repository, tmp_path):
    output = tmp_path / "bundle"
    build(repository, output)
    token = "hf_jwt_fixture"
    api = FakeAPI(owner="synthetic-publisher", exists=True, token=token)
    assert publish(output, "ctauchmann/hessian-recipes", api=api, publisher_token=token)["private"]
    assert not api.created
    for candidate in [FakeAPI(exists=True, token="different"),
                      FakeAPI(private=False, exists=True, token=token),
                      FakeAPI(exists=True, token=token, visibility="protected"),
                      FakeAPI(exists=False, token=token)]:
        with pytest.raises(ValueError):
            publish(output, "ctauchmann/hessian-recipes", api=candidate, publisher_token=token)
        assert not candidate.uploaded
    with pytest.raises(ValueError, match="OIDC"):
        publish(output, "ctauchmann/hessian-recipes", api=api, publisher_token="personal-token")


def test_mirror_removes_only_obsolete_generated_files_in_the_same_commit(repository, tmp_path):
    output = tmp_path / "bundle"
    build(repository, output)
    files = [str(path.relative_to(output)) for path in output.rglob("*") if path.is_file()]
    stale = ["style.css", "assets/favicon.svg", "sources/docs/removed.md", "pages/removed.html"]
    api = FakeAPI(exists=True, remote_files=files + stale + [".gitattributes"])
    publish(output, "ctauchmann/hessian-recipes", api=api, mirror=True)
    assert api.uploaded[0]["delete_patterns"] == sorted(stale)
    assert api.uploaded[0]["parent_commit"] == "parent"
    for unexpected in ["unrelated.txt", "sources/wild*.py", "sources/../escape.txt"]:
        rejected = FakeAPI(exists=True, remote_files=files + [unexpected])
        with pytest.raises(ValueError, match="unrelated"):
            publish(output, "ctauchmann/hessian-recipes", api=rejected, mirror=True)
        assert not rejected.uploaded


@pytest.mark.parametrize("mutation", ["changed", "extra", "symlink"])
def test_publish_rejects_bundle_inventory_or_byte_changes(repository, tmp_path, mutation):
    output = tmp_path / "bundle"
    build(repository, output)
    if mutation == "changed":
        (output / "index.html").write_text("changed")
    elif mutation == "extra":
        (output / "token.txt").write_text("unexpected")
    else:
        path = output / "assets/site.js"
        raw = path.read_bytes()
        outside = tmp_path / "outside.js"
        outside.write_bytes(raw)
        path.unlink()
        path.symlink_to(outside)
    api = FakeAPI()
    with pytest.raises(ValueError):
        publish(output, "ctauchmann/hessian-recipes", api=api)
    assert not api.created and not api.uploaded


def test_manifest_unsafe_paths_rejected_even_if_inventory_matches(repository, tmp_path):
    output = tmp_path / "bundle"
    build(repository, output)
    manifest = json.loads((output / "hub-manifest.json").read_text())
    manifest["files_sha256"]["../outside"] = "0" * 64
    (output / "hub-manifest.json").write_text(json.dumps(manifest))
    with pytest.raises(ValueError):
        publish(output, "ctauchmann/hessian-recipes", api=FakeAPI())


class CollectionAPI(FakeAPI):
    def __init__(self, *, collection_private=True, collection_owner="ctauchmann", **kwargs):
        super().__init__(**kwargs)
        self.collection = SimpleNamespace(owner=collection_owner, private=collection_private,
                                          slug="ctauchmann/hessian-recipes-123", items=[])
        self.added = []

    def create_collection(self, title, **kwargs):
        self.created.append((title, kwargs))
        return self.collection

    def get_collection(self, slug):
        return self.collection

    def add_collection_item(self, slug, **kwargs):
        self.added.append(kwargs)
        self.collection.items.append(SimpleNamespace(item_id=kwargs["item_id"], item_type=kwargs["item_type"], note=kwargs["note"]))


def test_companion_collection_is_private_references_only_and_requires_verified_space():
    receipt = {"repo_id": "ctauchmann/hessian-recipes", "private": True, "commit": "space-commit"}
    api = CollectionAPI()
    result = publish_collection(receipt, api=api)
    assert api.created[0][1]["private"] is True
    assert [(item["item_id"], item["item_type"]) for item in api.added] == [row[:2] for row in COLLECTION_ITEMS]
    assert all(len(row[2]) <= 500 for row in COLLECTION_ITEMS)
    assert result["private"] and not result["copies_models_or_datasets"]
    for denied in [CollectionAPI(private=False), CollectionAPI(owner="other"),
                   CollectionAPI(collection_private=False), CollectionAPI(collection_owner="other")]:
        with pytest.raises(ValueError):
            publish_collection(receipt, api=denied)
        assert not denied.added
    with pytest.raises(ValueError, match="verified"):
        publish_collection(receipt | {"private": False}, api=api)


def test_collection_api_accepts_live_owner_object_and_still_rejects_wrong_owner():
    receipt = {"repo_id": "ctauchmann/hessian-recipes", "private": True, "commit": "space-commit"}
    api = CollectionAPI(collection_owner={"name": "ctauchmann", "type": "user"})
    result = publish_collection(receipt, collection_slug=api.collection.slug, api=api)
    assert result["private"] and len(result["items"]) == 4
    wrong = CollectionAPI(collection_owner={"name": "other", "type": "user"})
    with pytest.raises(ValueError, match="owned"):
        publish_collection(receipt, collection_slug=wrong.collection.slug, api=wrong)
    assert not wrong.added


def test_protected_build_and_publish_preserve_hidden_existing_repository(repository, tmp_path):
    output = tmp_path / "protected-bundle"
    manifest = build(repository, output, visibility="protected")
    assert manifest["intended_visibility"] == "protected"
    api = FakeAPI(exists=True, visibility="protected")
    receipt = publish(output, "ctauchmann/hessian-recipes", api=api, visibility="protected")
    assert receipt["private"] is True and receipt["visibility"] == "protected"
    assert not api.created
    assert api.uploaded[0]["parent_commit"] == "parent"
    # Default-private publication cannot accidentally accept a Protected bundle.
    with pytest.raises(ValueError, match="visibility"):
        publish(output, "ctauchmann/hessian-recipes", api=api)


@pytest.mark.parametrize("change", ["missing", "public", "wrong-mode"])
def test_protected_publication_refuses_repository_creation_or_wrong_target(repository, tmp_path, change):
    output = tmp_path / "protected-bundle"
    build(repository, output, visibility="private" if change == "wrong-mode" else "protected")
    api = FakeAPI(exists=change != "missing", private=change != "public", visibility="protected")
    with pytest.raises(ValueError):
        publish(output, "ctauchmann/hessian-recipes", api=api, visibility="protected")
    assert not api.created and not api.uploaded


@pytest.mark.parametrize("visibility", ["public", "Protected", None])
def test_build_rejects_unapproved_visibility(repository, tmp_path, visibility):
    with pytest.raises(ValueError, match="visibility"):
        build(repository, tmp_path / "invalid-visibility", visibility=visibility)
