"""Run the real native preparation CLI and its Git lock fence offline."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

import native_pods
from test_native_dependency_guard import project

class PodsTests(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory(prefix="app-robot-native-pods-test-")
        self.addCleanup(self.temp.cleanup)
        self.root = Path(self.temp.name).resolve()
        project(self.root)
        self.env = dict(os.environ)

    def locked(self):
        (self.root / "Podfile").write_text("fixture")
        (self.root / "Podfile.lock").write_text("fixture-lock")
        commands = [["git", "init", "-q"], ["git", "add", "Podfile", "Podfile.lock"],
                    ["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
                     "commit", "-qm", "Own fixture lock"]]
        for command in commands:
            subprocess.run(command, cwd=self.root, check=True, capture_output=True)

    def run_cli(self):
        result = subprocess.run([sys.executable, native_pods.__file__, str(self.root),
                                 "--container", "App.xcodeproj"],
                                capture_output=True, text=True, env=self.env, timeout=20)
        return result.returncode, json.loads(result.stdout)

    def test_without_podfile_reports_not_configured(self):
        code, result = self.run_cli()
        self.assertEqual((code, result["pods"]), (0, "not_configured"))

    def test_missing_or_uncommitted_or_modified_lock_refuses(self):
        (self.root / "Podfile").write_text("fixture")
        self.assertEqual(self.run_cli()[1]["code"], "native_podfile_lock_required")
        (self.root / "Podfile.lock").write_text("uncommitted")
        self.assertEqual(self.run_cli()[1]["code"], "native_podfile_lock_commit_required")
        self.locked()
        (self.root / "Podfile.lock").write_text("changed")
        self.assertEqual(self.run_cli()[1]["code"], "native_podfile_lock_commit_required")

    def test_pod_runs_in_the_nested_project_folder(self):
        self.locked()
        folder = self.root / "mobile app/ios"
        folder.mkdir(parents=True)
        for name in ("App.xcodeproj", "Podfile", "Podfile.lock"):
            (self.root / name).rename(folder / name)
        for argv in (["git", "add", "-A"],
                     ["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
                      "commit", "-qm", "Move the native project into its app folder"]):
            subprocess.run(argv, cwd=self.root, check=True, capture_output=True)
        pod = self.root / "pod"
        pod.write_text('#!' + sys.executable + '\nimport json,os,sys\nfrom pathlib import Path\n'
                       'Path("child.json").write_text(json.dumps({"cwd":str(Path.cwd()),'
                       '"argv":sys.argv[1:]}))\n'
                       'workspace=Path("App.xcworkspace");workspace.mkdir()\n'
                       '(workspace/"contents.xcworkspacedata").write_text('
                       '\'<Workspace><FileRef location="group:App.xcodeproj"/></Workspace>\')\n')
        pod.chmod(0o700)
        self.env["PATH"] = str(self.root) + os.pathsep + self.env["PATH"]
        result = subprocess.run([sys.executable, native_pods.__file__, str(self.root),
                                 "--container", "mobile app/ios/App.xcodeproj"], cwd=self.root,
                                capture_output=True, text=True, env=self.env, timeout=20)
        self.assertEqual(result.returncode, 0, result.stdout)
        self.assertTrue(json.loads(result.stdout)["ok"])
        child = json.loads((folder / "child.json").read_text())
        self.assertEqual(child["cwd"], str(folder))
        self.assertEqual(child["argv"], ["install", "--deployment", "--no-repo-update"])
        self.assertEqual((folder / "Podfile.lock").read_text(), "fixture-lock")

    def test_declared_bundle_controls_the_actual_pod_child(self):
        self.locked()
        (self.root / "Gemfile").write_text('source "https://rubygems.org"\ngem "cocoapods", "1.16.2"\n')
        (self.root / "Gemfile.lock").write_text("fixture gem lock\n")
        for argv in (["git", "add", "Gemfile", "Gemfile.lock"],
                     ["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
                      "commit", "-qm", "Declare the app bundle"]):
            subprocess.run(argv, cwd=self.root, check=True, capture_output=True)
        bundle = self.root / "bundle"
        bundle.write_text('#!' + sys.executable + '\nimport os,sys\n'
                          'if sys.argv[1] == "install": raise SystemExit(0)\n'
                          'assert sys.argv[1] == "exec"\n'
                          'os.environ["FIXTURE_BUNDLE_ACTIVE"] = "yes"\n'
                          'os.execvp(sys.argv[2], sys.argv[2:])\n')
        bundle.chmod(0o700)
        pod = self.root / "pod"
        pod.write_text('#!' + sys.executable + '\nimport os,json\nfrom pathlib import Path\n'
                       'Path("bundle-child.json").write_text(json.dumps({k:os.environ.get(k) for k in '
                       '["FIXTURE_BUNDLE_ACTIVE","BUNDLE_GEMFILE","BUNDLE_FROZEN"]}))\n'
                       'raise SystemExit(7)\n')
        pod.chmod(0o700)
        self.env["PATH"] = str(self.root) + os.pathsep + self.env["PATH"]
        self.assertEqual(self.run_cli()[1]["code"], "native_pods_install_failed")
        child = json.loads((self.root / "bundle-child.json").read_text())
        self.assertEqual(child["FIXTURE_BUNDLE_ACTIVE"], "yes")
        self.assertEqual(child["BUNDLE_GEMFILE"], str(self.root / "Gemfile"))
        self.assertEqual(child["BUNDLE_FROZEN"], "true")

    def test_real_child_has_deployment_flags_and_preserves_failure(self):
        self.locked()
        script = self.root / "pod"
        script.write_text('#!' + sys.executable + '\nimport os,sys,json\n'
                          'from pathlib import Path\n'
                          'Path("child.json").write_text(json.dumps({"argv":sys.argv[1:]}))\n'
                          'raise SystemExit(7)\n')
        script.chmod(0o700)
        self.env["PATH"] = str(self.root) + os.pathsep + self.env["PATH"]
        code, result = self.run_cli()
        self.assertEqual((code, result["code"]), (1, "native_pods_install_failed"))
        child = json.loads((self.root / "child.json").read_text())
        self.assertEqual(child["argv"], ["install", "--deployment", "--no-repo-update"])

    @unittest.skipUnless(os.name == "posix", "owned POSIX process groups")
    def test_termination_reaps_the_owned_pod_child(self):
        import time
        self.locked()
        script = self.root / "pod"
        script.write_text('#!' + sys.executable + '\nimport os,time\nfrom pathlib import Path\n'
                          'print("Downloading dependencies\\nRead timed out", flush=True)\n'
                          'Path("owned-child").write_text(str(os.getpid()))\ntime.sleep(30)\n')
        script.chmod(0o700)
        self.env["PATH"] = str(self.root) + os.pathsep + self.env["PATH"]
        argv = [sys.executable, native_pods.__file__, str(self.root), "--container", "App.xcodeproj"]
        with subprocess.Popen(argv, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as child:
            deadline = time.monotonic() + 5
            while not (self.root / "owned-child").exists() and time.monotonic() < deadline:
                time.sleep(0.02)
            child.terminate()
            output, errors = child.communicate(timeout=10)
        self.assertTrue((self.root / "owned-child").exists())
        self.assertEqual(json.loads(output)["code"], "native_pods_operation_interrupted")
        self.assertIn("Downloading dependencies", json.loads(output).get("diagnostic", ""))
        self.assertTrue(errors.startswith("::error title=native_prepare_failed::"))
        self.assertEqual(json.loads(errors.strip().split("::", 2)[2])["code"], "native_pods_operation_interrupted")
        self.assertEqual(json.loads(errors.strip().split("::", 2)[2])["signals"], ["connection_timeout"])
        owned_pid = int((self.root / "owned-child").read_text())
        deadline = time.monotonic() + 5
        while time.monotonic() < deadline:
            try:
                os.kill(owned_pid, 0)
            except ProcessLookupError:
                return
            time.sleep(0.02)
        self.fail("the owned pod child survived helper termination")
