#!/usr/bin/env python3
"""Integration tests for installed-layout wrappers and import boundaries."""

from __future__ import annotations

import os
import pwd
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest


MODULE_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
REPO_ROOT = os.path.abspath(os.path.join(MODULE_ROOT, "..", "..", ".."))
LIB = os.path.join(MODULE_ROOT, "lib")
BIN = os.path.join(MODULE_ROOT, "bin")

SEAT_PROVISION_SHEBANG_ENTRYPOINTS = (
    "overdeck-seat-scope-entry",
    "overdeck-seat-implementer-exec",
    "overdeck-seat-tmux-mediator",
    "overdeck-seat-proxy-validate",
    "install-seat-guard",
    "install-headless-guard",
)

ROOT_INSTALLED_WRAPPER_MODULE_PATHS = (
    ("overdeck-seat-scope-entry", "/usr/local/lib/overdeck/seat_scope_entry.py"),
    ("overdeck-seat-implementer-exec", "/usr/local/lib/overdeck/seat_implementer_exec.py"),
    ("overdeck-seat-tmux-mediator", "/usr/local/lib/overdeck/seat_tmux_mediator.py"),
)


class SeatProvisionSourceLayoutTests(unittest.TestCase):
    def test_shebang_entrypoints_executable_in_git_and_worktree(self) -> None:
        for name in SEAT_PROVISION_SHEBANG_ENTRYPOINTS:
            rel = f"modules/workstation/claude/bin/{name}"
            proc = subprocess.run(
                ["git", "-C", REPO_ROOT, "ls-files", "-s", "--", rel],
                text=True,
                capture_output=True,
                check=False,
            )
            self.assertEqual(proc.returncode, 0, proc.stderr)
            mode = proc.stdout.strip().split()[0]
            self.assertEqual(mode, "100755", rel)
            path = os.path.join(BIN, name)
            st = os.stat(path)
            self.assertTrue(stat.S_ISREG(st.st_mode), rel)
            self.assertEqual(st.st_mode & stat.S_IXUSR, stat.S_IXUSR, rel)
            with open(path, encoding="utf-8") as fh:
                self.assertTrue(fh.readline().startswith("#!"), rel)

    def test_root_wrappers_embed_literal_canonical_module_paths(self) -> None:
        # Wrappers resolve their module through a root-owned, version-pinned activation
        # pointer (immutable version dir + validated 64-hex activation file), not a fixed
        # mutable path — so the module filename is checked directly, and the base dir plus
        # activation-file read must be present as fixed literals, never environment-derived.
        for name, module_path in ROOT_INSTALLED_WRAPPER_MODULE_PATHS:
            module_name = os.path.basename(module_path)
            path = os.path.join(BIN, name)
            with open(path, encoding="utf-8") as fh:
                text = fh.read()
            self.assertIn('_BASE = "/usr/local/lib/overdeck"', text, name)
            self.assertIn('seat-authority-active', text, name)
            self.assertIn(module_name, text, name)
            self.assertNotIn('f"{_OVERDECK_LIB}', text, name)
            self.assertNotIn("os.environ.get", text, name)
            self.assertNotIn("sys.argv[1:][0]", text, name)
    def test_scope_entry_pins_install_check_to_active_version(self) -> None:
        with tempfile.TemporaryDirectory(prefix="seat-wrapper-active-version-") as tmp:
            base = os.path.join(tmp, "usr/local/lib/overdeck")
            version = "a" * 64
            active = os.path.join(base, "versions", version)
            os.makedirs(active)
            with open(os.path.join(base, "seat-authority-active"), "w", encoding="ascii") as fh:
                fh.write(f"{version}\n")
            with open(os.path.join(active, "seat_scope_entry.py"), "w", encoding="utf-8") as fh:
                fh.write(
                    "import os\n"
                    "def main(args):\n"
                    "    print(os.environ.get('OVERDECK_SEAT_INSTALL_CHECK_ROOT', 'missing'))\n"
                )
            wrapper = os.path.join(tmp, "overdeck-seat-scope-entry")
            with open(os.path.join(BIN, "overdeck-seat-scope-entry"), encoding="utf-8") as fh:
                text = fh.read().replace("/usr/local/lib/overdeck", base)
            with open(wrapper, "w", encoding="utf-8") as fh:
                fh.write(text)
            os.chmod(wrapper, 0o755)
            env = os.environ.copy()
            env["OVERDECK_SEAT_INSTALL_CHECK_ROOT"] = os.path.join(tmp, "caller-controlled")

            proc = subprocess.run(
                [wrapper, "--install-check"],
                cwd=tmp,
                env=env,
                text=True,
                capture_output=True,
                check=False,
            )

            self.assertEqual(proc.returncode, 0, proc.stderr)
            self.assertEqual(proc.stdout.strip(), active)


class InstalledLayoutIntegrationTests(unittest.TestCase):
    def setUp(self) -> None:
        self.tmp = tempfile.mkdtemp(prefix="seat-installed-layout-")
        self._saved_env = {
            key: os.environ[key]
            for key in ("OVERDECK_SEAT_RUNTIME_BASE", "OVERDECK_SEAT_CONTROL_BASE", "OVERDECK_SEAT_TEST_MODE")
            if key in os.environ
        }
        for key in self._saved_env:
            os.environ.pop(key, None)
        self.overdeck_lib = os.path.join(self.tmp, "usr/local/lib/overdeck")
        self.overdeck_bin = os.path.join(self.tmp, "usr/local/bin")
        os.makedirs(self.overdeck_lib, exist_ok=True)
        os.makedirs(self.overdeck_bin, exist_ok=True)
        for name in (
            "seat_common.py",
            "seat_scope_entry.py",
            "seat_implementer_exec.py",
            "seat_implementer_identity.py",
            "seat_tmux_mediator.py",
        ):
            shutil.copy2(os.path.join(LIB, name), os.path.join(self.overdeck_lib, name))
        for name in (
            "overdeck-seat-scope-entry",
            "overdeck-seat-implementer-exec",
            "overdeck-seat-tmux-mediator",
        ):
            src = os.path.join(BIN, name)
            dst = os.path.join(self.overdeck_bin, name)
            with open(src, encoding="utf-8") as src_fh:
                text = src_fh.read().replace(
                    "/usr/local/lib/overdeck",
                    self.overdeck_lib,
                )
            with open(dst, "w", encoding="utf-8") as fh:
                fh.write(text)
            os.chmod(dst, 0o755)

    def tearDown(self) -> None:
        shutil.rmtree(self.tmp, ignore_errors=True)
        for key, value in self._saved_env.items():
            os.environ[key] = value

    def _run_wrapper(self, wrapper_name: str, args: list[str]) -> subprocess.CompletedProcess[str]:
        wrapper = os.path.join(self.overdeck_bin, wrapper_name)
        env = os.environ.copy()
        env["PYTHONPATH"] = ""
        return subprocess.run(
            [wrapper, *args],
            cwd=self.tmp,
            env=env,
            text=True,
            capture_output=True,
        )

    def test_implementer_exec_imports_without_cycle(self) -> None:
        proc = self._run_wrapper("overdeck-seat-implementer-exec", ["--help"])
        self.assertNotIn("ModuleNotFoundError", proc.stderr)
        self.assertNotIn("module-missing", proc.stderr)

    def test_scope_entry_imports_identity_module(self) -> None:
        if os.geteuid() != 0:
            self.skipTest("requires root for install-check")
        proc = self._run_wrapper("overdeck-seat-scope-entry", ["--install-check"])
        self.assertEqual(proc.returncode, 0, proc.stderr)
        self.assertIn("scope-entry-install-check-ok", proc.stdout)
        self.assertNotIn("ModuleNotFoundError", proc.stderr)
        self.assertNotIn("module-missing", proc.stderr)

    def test_implementer_exec_shebang_not_setuid(self) -> None:
        wrapper = os.path.join(self.overdeck_bin, "overdeck-seat-implementer-exec")
        st = os.stat(wrapper)
        self.assertEqual(stat.S_IMODE(st.st_mode), 0o755)
        self.assertEqual(st.st_mode & stat.S_ISUID, 0)

    def test_tmux_mediator_shebang_not_setuid(self) -> None:
        wrapper = os.path.join(self.overdeck_bin, "overdeck-seat-tmux-mediator")
        st = os.stat(wrapper)
        self.assertEqual(stat.S_IMODE(st.st_mode), 0o755)
        self.assertEqual(st.st_mode & stat.S_ISUID, 0)

    def test_implementer_exec_non_root_without_sudo_fails(self) -> None:
        if os.geteuid() == 0:
            self.skipTest("need non-root subject")
        home = pwd.getpwuid(os.getuid()).pw_dir
        wrapper = os.path.join(self.overdeck_bin, "overdeck-seat-implementer-exec")
        env = os.environ.copy()
        env["PYTHONPATH"] = ""
        env["OVERDECK_SEAT_SSH_USER"] = pwd.getpwuid(os.getuid()).pw_name
        proc = subprocess.run(
            [
                wrapper,
                "--seat-id", "seat-x",
                "--seat-host", "debian1",
                "--seat-model", "gpt-5.6-terra",
                "--socket", "/tmp/s.sock",
                "--launcher", "/usr/local/bin/overdeck-seat-launcher",
                "--claude-bin", f"{home}/.local/bin/claude",
                "--guard-bin", f"{home}/.local/share/overdeck/seat-guard/current/bin",
                "--checkout", f"{home}/seats/seat-x/repo",
                "--state-dir", f"{home}/.local/state/overdeck/seats/seat-x",
                "--implementer-home", "/var/lib/overdeck/seat-runtime/seat-x/home",
                "--operator-home", home,
                "--account-slug", "roy",
                "--profile-dir", f"{home}/.claudex-accounts/roy/seat-x",
                "--secure-storage-dir", f"{home}/.claudex-accounts/roy/seat-x/secure-storage",
                "--netns-path", "/run/netns/seat-x",
                "--", "tmux",
            ],
            cwd=self.tmp,
            env=env,
            text=True,
            capture_output=True,
        )
        # The wrapper resolves through a root-owned activation pointer (finding #6) before it
        # ever reaches root-check/arg-parse — with no live install present, that gate fails
        # first as "activation-invalid". Either failure mode proves the wrapper never runs
        # unprivileged code as the caller; "requires-root" additionally proves the deeper gate
        # once a real activation exists (installed-layout smoke, not exercised here).
        self.assertRegex(proc.stderr, r"requires-root|activation-invalid")

    def test_implementer_exec_sudo_transition_not_setuid_bits(self) -> None:
        wrapper = os.path.join(self.overdeck_bin, "overdeck-seat-implementer-exec")
        st = os.stat(wrapper)
        self.assertEqual(st.st_mode & stat.S_ISUID, 0)
        if os.geteuid() != 0:
            self.skipTest("requires root to simulate sudo transition")
        operator_uid = os.getuid()
        operator_user = os.environ.get("USER", "root")
        code = (
            "import os, sys; "
            f"sys.path.insert(0, {self.overdeck_lib!r}); "
            f"os.environ['SUDO_UID'] = {str(operator_uid)!r}; "
            f"os.environ['SUDO_USER'] = {operator_user!r}; "
            "from seat_implementer_exec import require_root_transition; "
            f"require_root_transition({operator_uid}); "
            "print('sudo-transition-ok')"
        )
        probe = subprocess.run(
            ["sudo", "-n", "env", f"SUDO_UID={operator_uid}", f"SUDO_USER={operator_user}", "python3", "-c", code],
            cwd=self.tmp,
            text=True,
            capture_output=True,
        )
        self.assertEqual(probe.returncode, 0, probe.stderr)
        self.assertIn("sudo-transition-ok", probe.stdout)

    def test_implementer_exec_direct_root_without_sudo_env_fails(self) -> None:
        if os.geteuid() != 0:
            self.skipTest("requires root subject")
        code = (
            f"import sys; sys.path.insert(0, {self.overdeck_lib!r}); "
            "from seat_implementer_exec import require_root_transition; "
            "require_root_transition(1000)"
        )
        env = os.environ.copy()
        env.pop("SUDO_UID", None)
        proc = subprocess.run(
            ["python3", "-c", code],
            cwd=self.tmp,
            env=env,
            text=True,
            capture_output=True,
        )
        self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr)
        self.assertIn("operator-invocation-required", proc.stderr)


def _load_suite(suite_name: str) -> unittest.TestSuite:
    loader = unittest.TestLoader()
    if suite_name == "source":
        return loader.loadTestsFromTestCase(SeatProvisionSourceLayoutTests)
    if suite_name == "deployed":
        return loader.loadTestsFromTestCase(InstalledLayoutIntegrationTests)
    return loader.loadTestsFromModule(sys.modules[__name__])


def main(argv: list[str] | None = None) -> None:
    import argparse

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--suite",
        choices=("all", "source", "deployed"),
        default="all",
        help="source: git-index layout checks; deployed: installed-runtime integration only",
    )
    args, remainder = parser.parse_known_args(argv)
    if remainder and remainder[0] == "--":
        remainder = remainder[1:]
    result = unittest.TextTestRunner(verbosity=2).run(_load_suite(args.suite))
    raise SystemExit(0 if result.wasSuccessful() else 1)


if __name__ == "__main__":
    main()
