import importlib.util
import json
import socket
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("vphone_local", ROOT / "vphone_local.py")
mod = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(mod)


class VphoneLocalTests(unittest.TestCase):
    def test_name_validation_rejects_path_traversal(self):
        for value in ("../outside", "", "a/b", "name with space"):
            with self.assertRaises(mod.KitError):
                mod.require_name(value)
        self.assertEqual(mod.require_name("dev_17.3"), "dev_17.3")

    def test_socket_request_uses_current_json_line_protocol(self):
        class SocketMock:
            sent = b""
            def __enter__(self): return self
            def __exit__(self, *unused): return False
            def settimeout(self, unused): pass
            def connect(self, unused): pass
            def sendall(self, data): self.sent += data
            def recv(self, unused): return b'{"ok":true,"path":"/tmp/shot.png"}\n'
        mock = SocketMock()
        with patch.object(Path, "exists", return_value=True), patch.object(mod.socket, "socket", return_value=mock):
            result = mod.socket_request(Path("/mock/vphone.sock"), {"t": "screenshot", "screen": False}, 2)
        self.assertEqual(json.loads(mock.sent.split(b"\n", 1)[0]), {"t": "screenshot", "screen": False})
        self.assertTrue(result["ok"])

    def test_diagnose_keeps_sigkill_cause_unknown_without_evidence(self):
        with tempfile.TemporaryDirectory() as directory:
            log = Path(directory) / "run.log"; log.write_text("process Killed (signal 9)")
            # Direct function output is intentionally JSON for UI/HTML consumers.
            import contextlib, io
            stream = io.StringIO()
            with contextlib.redirect_stdout(stream):
                mod.cmd_diagnose(type("Args", (), {"log": str(log)})())
            finding = json.loads(stream.getvalue())["findings"][0]
            self.assertEqual(finding["kind"], "SIGKILL")
            self.assertEqual(finding["confidence"], "unknown cause")

    def test_diagnose_normal_amfi_and_memory_pressure_are_not_candidates(self):
        import contextlib, io
        with tempfile.TemporaryDirectory() as directory:
            log = Path(directory) / "run.log"
            log.write_text("AMFI status: ready\nmemory pressure: normal")
            stream = io.StringIO()
            with contextlib.redirect_stdout(stream):
                mod.cmd_diagnose(type("Args", (), {"log": str(log)})())
            kinds = [item["kind"] for item in json.loads(stream.getvalue())["findings"]]
            self.assertEqual(kinds, ["no candidate signature"])

    def test_create_runs_preflight_and_defaults_root_popup(self):
        with tempfile.TemporaryDirectory() as directory:
            firmware = Path(directory) / "firmware.ipsw"; firmware.write_bytes(b"x")
            args = mod.parser().parse_args([
                "--cli", "/bin/echo", "create", "dev17", "--iphone-source", str(firmware),
                "--cloudos-source", str(firmware), "--dry-run",
            ])
            calls = []
            def fake_run(args, action, argv, **unused):
                calls.append((action, argv)); return 0
            with patch.object(mod, "host_conditions", return_value=({}, [])), patch.object(mod, "run", side_effect=fake_run):
                mod.cmd_create(args)
            self.assertEqual(calls[0][0], "host-preflight")
            self.assertIn("--root-popup", calls[1][1])

    def test_health_uses_loopback_http_json(self):
        class Response:
            status = 200
            def read(self): return b'{"status":"ok","name":"vphoned"}'
            def __enter__(self): return self
            def __exit__(self, *unused): return False
        args = mod.parser().parse_args(["health", "--api-listen", "127.0.0.1:8765"])
        with patch.object(mod.urllib.request, "urlopen", return_value=Response()):
            self.assertEqual(mod.cmd_health(args), 0)

    def test_bootstrap_rejects_dirty_pinned_source(self):
        args = mod.parser().parse_args(["bootstrap", "--source", "/private/tmp/vphone-source"])
        with patch.object(mod, "tracked_dirty", return_value=True):
            with self.assertRaisesRegex(mod.KitError, "tracked changes"):
                mod.cmd_bootstrap(args)

    def test_fetch_source_without_execute_only_plans_git_commands(self):
        args = mod.parser().parse_args(["bootstrap", "--fetch-source", "--source", "/private/tmp/not-real-vphone-source"])
        calls = []
        with patch.object(mod, "run", side_effect=lambda *items, **unused: calls.append(items[1]) or 0):
            self.assertEqual(mod.cmd_bootstrap(args), 0)
        self.assertEqual(calls, ["source-clone", "source-checkout", "source-submodules"])

    def test_launch_rejects_non_loopback_listener(self):
        args = mod.parser().parse_args(["--cli", "/bin/echo", "launch", "dev17", "--api-listen", "0.0.0.0:8765", "--dry-run"])
        with self.assertRaisesRegex(mod.KitError, "local-only"):
            mod.cmd_launch(args)
