import importlib.util
import json
import stat
import zipfile
from pathlib import Path

import pytest

MODULE_PATH = Path(__file__).parents[1] / "tools" / "theme_factory.py"
SPEC = importlib.util.spec_from_file_location("theme_factory", MODULE_PATH)
factory = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(factory)


def write_zip(path, members):
    with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
        for name, body in members.items():
            archive.writestr(name, body)


def valid_zip(tmp_path, root="theme"):
    path = tmp_path / "theme.zip"
    write_zip(path, {f"{root}/style.css": "Theme Name: Test", f"{root}/index.php": "<?php"})
    return path


def test_install_pack_and_audit_round_trip(tmp_path):
    source = valid_zip(tmp_path, "real-theme")
    container = tmp_path / "published"

    assert factory.install_archive(source, container) == "real-theme"
    (container / "response.md").write_text("done\n")
    (container / "generation.json").write_text("{}\n")
    dist = tmp_path / "dist.zip"
    assert factory.pack_theme(container, dist) == "real-theme"
    factory.audit(container, dist)
    assert factory.inspect_archive(dist) == "real-theme"


@pytest.mark.parametrize(
    "members,match",
    [
        ({"../style.css": "x", "theme/index.php": "x"}, "unsafe ZIP path"),
        ({"one/style.css": "x", "two/index.php": "x"}, "exactly one root"),
        ({"theme/style.css": "x"}, "missing"),
        ({"theme/style.css": "x", "theme/STYLE.CSS": "x", "theme/index.php": "x"}, "duplicate"),
    ],
)
def test_rejects_malformed_archives(tmp_path, members, match):
    path = tmp_path / "bad.zip"
    write_zip(path, members)
    with pytest.raises(factory.FactoryError, match=match):
        factory.inspect_archive(path)


def test_rejects_symlink(tmp_path):
    path = tmp_path / "bad.zip"
    with zipfile.ZipFile(path, "w") as archive:
        archive.writestr("theme/style.css", "x")
        archive.writestr("theme/index.php", "x")
        link = zipfile.ZipInfo("theme/link")
        link.external_attr = (stat.S_IFLNK | 0o777) << 16
        archive.writestr(link, "target")
    with pytest.raises(factory.FactoryError, match="symlink"):
        factory.inspect_archive(path)


def test_result_requires_one_zip_inside_output(tmp_path):
    output = tmp_path / "output"
    output.mkdir()
    generated = valid_zip(output)
    result = tmp_path / "result.json"
    result.write_text(json.dumps({"files": [str(generated)]}))
    assert factory.generated_zip(result, output) == generated.resolve()

    outside = valid_zip(tmp_path, "other")
    result.write_text(json.dumps({"files": [str(outside)]}))
    with pytest.raises(factory.FactoryError, match="escaped output"):
        factory.generated_zip(result, output)


def test_pack_rejects_unexpected_container_file(tmp_path):
    source = valid_zip(tmp_path)
    container = tmp_path / "published"
    factory.install_archive(source, container)
    (container / "unexpected.txt").write_text("x")
    with pytest.raises(factory.FactoryError, match="one root plus generation metadata"):
        factory.pack_theme(container, tmp_path / "dist.zip")
