#!/usr/bin/env python3

import errno
import os
import sys
import uuid
from os.path import join

from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import mergerfs_get_option
from posix_parity import mergerfs_mount
from posix_parity import mergerfs_set_option
from posix_parity import touch


SKIP = 77


def get_errno(func):
    try:
        func()
        return 0
    except OSError as exc:
        return exc.errno


def require_symlink(path, expected, label):
    # Inspect the backend: rename/link can leave a cached regular pool inode.
    if not os.path.islink(path):
        return f"{label}: expected backend symlink fallback"
    target = os.readlink(path)
    if target != expected:
        return f"{label}: target {target!r} != {expected!r}"
    return None


def require_payload(path, label):
    with open(path, "rb") as fp:
        if fp.read() != b"src":
            return f"{label}: backend payload mismatch"
    return None


def setup_paths(branches, mount, rel_base, name, root=False):
    src_rel = f"{rel_base}_{name}.src" if root else join(rel_base, f"{name}.src")
    dst_rel = join(rel_base, "dst", "nested", name)
    src = join(mount, src_rel)
    dst = join(mount, dst_rel)

    cleanup_dir(join(branches[0], rel_base))
    cleanup_dir(join(branches[1], rel_base))
    os.makedirs(join(branches[0], rel_base), exist_ok=True)
    os.makedirs(join(branches[1], rel_base, "dst", "nested"), exist_ok=True)
    touch(join(branches[0], src_rel), b"src")

    return src, dst, src_rel, dst_rel


def main():
    try:
        with mergerfs_mount(num_branches=2) as (mount, branches):
            rel_base = f".mergerfs_exdev_test_{os.getpid()}_{uuid.uuid4().hex}"
            root_sources = []
            if len(branches) < 2:
                print("EXDEV fallback test requires at least 2 branches", end="")
                return SKIP
            branches = branches[:2]
            try:
                orig_create = mergerfs_get_option(mount, "category.create")
                orig_link = mergerfs_get_option(mount, "link-exdev")
                orig_rename = mergerfs_get_option(mount, "rename-exdev")
                try:
                    mergerfs_set_option(mount, "category.create", "epmfs")

                    src, dst, src_rel, _ = setup_paths(branches, mount, rel_base, "link_pass")
                    mergerfs_set_option(mount, "link-exdev", "passthrough")
                    err = get_errno(lambda: os.link(src, dst + ".link.pass"))
                    if err != errno.EXDEV:
                        return fail(f"link-exdev passthrough expected EXDEV, errno={err}")

                    for mode in ("rel-symlink", "abs-base-symlink", "abs-pool-symlink"):
                        src, dst, src_rel, dst_rel = setup_paths(branches, mount, rel_base, f"link_{mode}")
                        mergerfs_set_option(mount, "link-exdev", mode)
                        err = get_errno(lambda: os.link(src, dst))
                        if err != 0:
                            return fail(f"link-exdev={mode} returned errno={err}")
                        src_base = join(branches[0], src_rel)
                        if mode == "rel-symlink":
                            expected = os.path.relpath(src, os.path.dirname(dst))
                        elif mode == "abs-base-symlink":
                            expected = src_base
                        else:
                            expected = src
                        err_msg = require_symlink(join(branches[1], dst_rel), expected, f"link-exdev={mode}")
                        if err_msg:
                            return fail(err_msg)
                        err_msg = require_payload(src_base, f"link-exdev={mode}")
                        if err_msg:
                            return fail(err_msg)
                        if os.path.lexists(join(branches[0], dst_rel)):
                            return fail(f"link-exdev={mode}: destination unexpectedly created on source branch")

                    src, dst, _, _ = setup_paths(branches, mount, rel_base, "rename_pass")
                    mergerfs_set_option(mount, "rename-exdev", "passthrough")
                    err = get_errno(lambda: os.rename(src, dst))
                    if err != errno.EXDEV:
                        return fail(f"rename-exdev passthrough expected EXDEV, errno={err}")

                    # Run root sources first, before any fallback creates staging.
                    for root in (True, False):
                        for mode in ("rel-symlink", "abs-symlink"):
                            # Each root case must start without a staging directory.
                            if root:
                                cleanup_dir(join(branches[0], ".mergerfs_rename_exdev"))
                            name = f"rename_{mode}_{root}"
                            if root:
                                root_sources.append(f"{rel_base}_{name}.src")
                            src, dst, src_rel, dst_rel = setup_paths(branches, mount, rel_base, name, root=root)
                            mergerfs_set_option(mount, "rename-exdev", mode)
                            err = get_errno(lambda: os.rename(src, dst))
                            if err != 0:
                                return fail(f"rename-exdev={mode}, root={root} returned errno={err}")
                            staged_rel = join(".mergerfs_rename_exdev", src_rel)
                            pool_target = join(mount, staged_rel)
                            expected = os.path.relpath(pool_target, os.path.dirname(dst)) if mode == "rel-symlink" else pool_target
                            label = f"rename-exdev={mode}, root={root}"
                            err_msg = require_symlink(join(branches[1], dst_rel), expected, label)
                            if err_msg:
                                return fail(err_msg)
                            if os.path.lexists(join(branches[0], src_rel)):
                                return fail(f"{label}: source was not moved")
                            err_msg = require_payload(join(branches[0], staged_rel), label)
                            if err_msg:
                                return fail(err_msg)
                finally:
                    mergerfs_set_option(mount, "category.create", orig_create)
                    mergerfs_set_option(mount, "link-exdev", orig_link)
                    mergerfs_set_option(mount, "rename-exdev", orig_rename)
                return 0
            finally:
                for branch in branches:
                    cleanup_dir(join(branch, rel_base))
                    cleanup_dir(join(branch, ".mergerfs_rename_exdev", rel_base))
                    for src_rel in root_sources:
                        for path in (join(branch, src_rel), join(branch, ".mergerfs_rename_exdev", src_rel)):
                            if os.path.lexists(path):
                                os.unlink(path)
    except RuntimeError as exc:
        if str(exc) in ("mergerfs binary not found", "fusermount3/fusermount not found"):
            print(str(exc), end="")
            return SKIP
        return fail(f"EXDEV fallback test failed: {exc}")
    except OSError as exc:
        return fail(f"EXDEV fallback test failed: {exc}")


if __name__ == "__main__":
    raise SystemExit(main())
