# retoor <retoor@molodetz.nl>

import asyncio
from datetime import datetime, timezone
from pathlib import Path
import pytest
from devplacepy.database import init_db, get_table, refresh_snapshot
from devplacepy import project_files
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.fork_service import ForkService
from tests.conftest import run_async
@pytest.fixture(autouse=True)
def _init_db_fork_jobs():
    init_db()
    yield
@pytest.fixture
def fork_env(tmp_path, monkeypatch):
    monkeypatch.setattr(
        "devplacepy.services.jobs.fork_service.FORK_STAGING_DIR", tmp_path / "staging"
    )
    monkeypatch.setattr("devplacepy.project_files.PROJECT_FILES_DIR", tmp_path / "pf")
    yield tmp_path
    jobs = get_table("jobs")
    for row in list(jobs.find(kind="fork")):
        jobs.delete(uid=row["uid"])
    projects = get_table("projects")
    files = get_table("project_files")
    for project in list(projects.find()):
        if str(project.get("user_uid", "")).startswith("forktest-owner"):
            for node in list(files.find(project_uid=project["uid"])):
                files.delete(uid=node["uid"])
            projects.delete(uid=project["uid"])
    for node in list(files.find()):
        if str(node.get("project_uid", "")).startswith("forktest"):
            files.delete(uid=node["uid"])
    forks = get_table("project_forks")
    for relation in list(forks.find()):
        if str(relation.get("forked_by_uid", "")).startswith("forktest-owner"):
            forks.delete(uid=relation["uid"])
    users = get_table("users")
    for user in list(users.find()):
        if str(user.get("uid", "")).startswith("forktest-owner"):
            users.delete(uid=user["uid"])
_counter_fork_jobs = [0]
def _make_source_project(*, is_private=False, binary=False):
    _counter_fork_jobs[0] += 1
    pid = f"forktest-{_counter_fork_jobs[0]}"
    owner_uid = f"forktest-owner-{_counter_fork_jobs[0]}"
    user = {"uid": owner_uid, "username": f"forktester{_counter_fork_jobs[0]}"}
    get_table("users").insert(
        {"uid": owner_uid, "username": user["username"], "xp": 0, "level": 1}
    )
    get_table("projects").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": pid,
            "user_uid": owner_uid,
            "slug": f"{pid}-source",
            "title": "Source Project",
            "description": "the original",
            "project_type": "software",
            "platforms": "linux",
            "status": "Released",
            "is_private": 1 if is_private else 0,
            "read_only": 0,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    project_files.write_text_file(pid, user, "README.md", "# hello\nworld")
    project_files.write_text_file(pid, user, "src/app.py", "print(1)\n")
    if binary:
        project_files.store_upload(pid, user, "assets", "logo.bin", bytes(range(256)))
    return pid, owner_uid
def _tree(directory):
    root = Path(directory)
    out = {}
    for path in sorted(root.rglob("*")):
        if path.is_file():
            out[path.relative_to(root).as_posix()] = path.read_bytes()
    return out
def _process_fork_jobs():
    async def drive():
        svc = ForkService()
        for _ in range(400):
            await svc.run_once()
            refresh_snapshot()
            pending = [
                r
                for r in get_table("jobs").find(kind="fork")
                if r["status"] in ("pending", "running")
            ]
            if not pending and not svc._inflight:
                return
            await asyncio.sleep(0.05)

    run_async(drive())
def _enqueue(source_uid, owner_uid, title="My Fork"):
    return queue.enqueue(
        "fork",
        {"source_project_uid": source_uid, "title": title, "forked_by_uid": owner_uid},
        "user",
        owner_uid,
        title,
    )


def test_enqueue_creates_pending_job(fork_env):
    uid = _enqueue("p", "forktest-owner-x")
    job = queue.get_job(uid)
    assert job["status"] == "pending"
    assert job["kind"] == "fork"
    assert job["payload"]["source_project_uid"] == "p"


def test_process_creates_fork_with_files(fork_env, tmp_path):
    pid, owner_uid = _make_source_project(binary=True)
    uid = _enqueue(pid, owner_uid, title="Forked Copy")
    _process_fork_jobs()
    job = queue.get_job(uid)
    assert job["status"] == "done"
    result = job["result"]
    new_uid = result["project_uid"]
    forked = get_table("projects").find_one(uid=new_uid)
    assert forked is not None
    assert forked["user_uid"] == owner_uid
    assert forked["title"] == "Forked Copy"
    assert result["project_url"] == f"/projects/{forked['slug']}"

    source_dir = tmp_path / "exp_source"
    fork_dir = tmp_path / "exp_fork"
    project_files.export_to_dir(pid, "", source_dir)
    project_files.export_to_dir(new_uid, "", fork_dir)
    assert _tree(fork_dir) == _tree(source_dir)


def test_fork_relation_direction(fork_env):
    pid, owner_uid = _make_source_project()
    uid = _enqueue(pid, owner_uid)
    _process_fork_jobs()
    new_uid = queue.get_job(uid)["result"]["project_uid"]
    relation = get_table("project_forks").find_one(forked_project_uid=new_uid)
    assert relation is not None
    assert relation["source_project_uid"] == pid
    assert relation["forked_project_uid"] == new_uid
    assert relation["forked_by_uid"] == owner_uid


def test_fork_copies_binary_file(fork_env, tmp_path):
    pid, owner_uid = _make_source_project(binary=True)
    uid = _enqueue(pid, owner_uid)
    _process_fork_jobs()
    new_uid = queue.get_job(uid)["result"]["project_uid"]
    fork_dir = tmp_path / "binfork"
    project_files.export_to_dir(new_uid, "", fork_dir)
    assert (fork_dir / "assets" / "logo.bin").read_bytes() == bytes(range(256))


def test_fork_preserves_private_flag(fork_env):
    pid, owner_uid = _make_source_project(is_private=True)
    uid = _enqueue(pid, owner_uid)
    _process_fork_jobs()
    new_uid = queue.get_job(uid)["result"]["project_uid"]
    assert get_table("projects").find_one(uid=new_uid)["is_private"] == 1


def test_missing_source_fails_job_without_orphan(fork_env):
    before = {p["uid"] for p in get_table("projects").find()}
    uid = _enqueue("does-not-exist", "forktest-owner-missing")
    _process_fork_jobs()
    job = queue.get_job(uid)
    assert job["status"] == "failed"
    assert "source project not found" in job["error"].lower()
    after = {p["uid"] for p in get_table("projects").find()}
    assert before == after


def test_cleanup_keeps_project(fork_env):
    pid, owner_uid = _make_source_project()
    uid = _enqueue(pid, owner_uid)
    _process_fork_jobs()
    job = queue.get_job(uid)
    new_uid = job["result"]["project_uid"]
    ForkService().cleanup(job)
    assert get_table("projects").find_one(uid=new_uid) is not None


def test_retention_sweep_keeps_project(fork_env):
    pid, owner_uid = _make_source_project()
    uid = _enqueue(pid, owner_uid)
    _process_fork_jobs()
    new_uid = queue.get_job(uid)["result"]["project_uid"]
    get_table("jobs").update(
        {"uid": uid, "expires_at": "2000-01-01T00:00:00+00:00"}, ["uid"]
    )
    svc = ForkService()
    run_async(svc.run_once())
    refresh_snapshot()
    assert queue.get_job(uid) is None
    assert get_table("projects").find_one(uid=new_uid) is not None


def test_orphan_running_recovered_on_enable(fork_env):
    uid = _enqueue("p", "forktest-owner-o")
    get_table("jobs").update(
        {"uid": uid, "status": "running", "started_at": "2020-01-01T00:00:00+00:00"},
        ["uid"],
    )
    svc = ForkService()
    run_async(svc.on_enable())
    job = queue.get_job(uid)
    assert job["status"] == "pending"
    assert job["retry_count"] == 1
