from __future__ import annotations

import os
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from autosana_cli import runtime
from autosana_cli.models import DEFAULT_NODE_VERSION, Platform


# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------


class TestNodeDownloadInfo:
    def test_darwin_arm64(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Darwin")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "arm64")
        asset = runtime.node_download_info("24.0.0")
        assert "darwin-arm64" in asset.url
        assert asset.url.startswith("https://nodejs.org/dist/v24.0.0/")
        assert asset.filename == "node-v24.0.0-darwin-arm64.tar.gz"

    def test_darwin_x86_64(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Darwin")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "x86_64")
        asset = runtime.node_download_info("24.0.0")
        assert "darwin-x64" in asset.url

    def test_linux_x86_64(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Linux")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "x86_64")
        asset = runtime.node_download_info("24.0.0")
        assert "linux-x64" in asset.url

    def test_linux_aarch64(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Linux")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "aarch64")
        asset = runtime.node_download_info("24.0.0")
        assert "linux-arm64" in asset.url

    def test_windows_amd64(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Windows")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "AMD64")
        asset = runtime.node_download_info("24.0.0")
        assert "win-x64" in asset.url
        assert asset.filename.endswith(".zip")
        assert asset.archive_format == "zip"

    def test_unsupported_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "FreeBSD")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "amd64")
        with pytest.raises(RuntimeError, match="Unsupported"):
            runtime.node_download_info("24.0.0")


# ---------------------------------------------------------------------------
# Find functions
# ---------------------------------------------------------------------------


class TestFindNode:
    def test_prefers_managed(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        # Create a fake managed node binary
        node_dir = tmp_path / ".autosana" / "node" / f"v{DEFAULT_NODE_VERSION}" / "bin"
        node_dir.mkdir(parents=True)
        node_bin = node_dir / "node"
        node_bin.write_text("#!/bin/sh\necho v24.0.0")
        node_bin.chmod(0o755)

        monkeypatch.setattr(runtime, "check_node_version", lambda _: "24.0.0")

        result = runtime.find_node()
        assert result == node_bin

    def test_rejects_wrong_version(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        # System node exists but wrong version
        monkeypatch.setattr("shutil.which", lambda cmd: "/usr/bin/node" if cmd == "node" else None)
        monkeypatch.setattr(runtime, "check_node_version", lambda _: "16.0.0")

        result = runtime.find_node()
        assert result is None

    def test_returns_none_when_not_installed(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("shutil.which", lambda cmd: None)
        result = runtime.find_node()
        assert result is None


class TestFindAppium:
    def test_returns_none_when_missing(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr("shutil.which", lambda cmd: None)
        result = runtime.find_appium()
        assert result is None


# ---------------------------------------------------------------------------
# Runtime env
# ---------------------------------------------------------------------------


class TestRuntimeEnv:
    def test_prepends_managed_paths(self, tmp_path: Path) -> None:
        # AUTOSANA_HOME is set by conftest.py to tmp_path/.autosana
        # Create the directories so they get prepended
        node_bin_dir = tmp_path / ".autosana" / "node" / f"v{DEFAULT_NODE_VERSION}" / "bin"
        node_bin_dir.mkdir(parents=True)
        appium_bin_dir = tmp_path / ".autosana" / "appium" / "node_modules" / ".bin"
        appium_bin_dir.mkdir(parents=True)

        env = runtime.runtime_env()
        path_parts = env["PATH"].split(os.pathsep)
        assert str(node_bin_dir) in path_parts[:3]
        assert str(appium_bin_dir) in path_parts[:3]

    def test_sets_appium_home(self, tmp_path: Path) -> None:
        env = runtime.runtime_env()
        assert "APPIUM_HOME" in env
        assert ".autosana/appium" in env["APPIUM_HOME"] or "autosana" in env["APPIUM_HOME"]


# ---------------------------------------------------------------------------
# Ensure functions (mocked I/O)
# ---------------------------------------------------------------------------


class TestEnsureNode:
    def test_skips_if_present(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        node_dir = tmp_path / ".autosana" / "node" / f"v{DEFAULT_NODE_VERSION}" / "bin"
        node_dir.mkdir(parents=True)
        node_bin = node_dir / "node"
        node_bin.write_text("#!/bin/sh\necho v24.0.0")
        node_bin.chmod(0o755)

        monkeypatch.setattr(runtime, "check_node_version", lambda _: "24.0.0")

        # Should return without downloading
        result = runtime.ensure_node()
        assert result == node_bin

    def test_downloads_if_missing(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        import io
        import tarfile as tf

        # Create a fake tarball in memory
        buf = io.BytesIO()
        with tf.open(fileobj=buf, mode="w:gz") as tar:
            # Add bin/node
            node_content = b"#!/bin/sh\necho v24.0.0\n"
            info = tf.TarInfo(name="node-v24.0.0-darwin-arm64/bin/node")
            info.size = len(node_content)
            info.mode = 0o755
            tar.addfile(info, io.BytesIO(node_content))
            # Add bin/npm
            npm_content = b"#!/bin/sh\necho npm\n"
            info2 = tf.TarInfo(name="node-v24.0.0-darwin-arm64/bin/npm")
            info2.size = len(npm_content)
            info2.mode = 0o755
            tar.addfile(info2, io.BytesIO(npm_content))
        tar_bytes = buf.getvalue()

        # Mock urlopen to return our fake tarball
        mock_response = MagicMock()
        mock_response.read.return_value = tar_bytes
        mock_response.__enter__ = lambda s: s
        mock_response.__exit__ = lambda s, *a: None
        monkeypatch.setattr("autosana_cli.runtime.urlopen", lambda url, timeout=None: mock_response)

        # Mock platform detection
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.system", lambda: "Darwin")
        monkeypatch.setattr("autosana_cli.runtime.platform_lib.machine", lambda: "arm64")
        monkeypatch.setattr(runtime, "check_node_version", lambda _: "24.0.0")

        result = runtime.ensure_node()
        assert result.exists()
        assert "node" in result.name


class TestEnsureAppium:
    def test_skips_if_present(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        appium_dir = tmp_path / ".autosana" / "appium" / "node_modules" / ".bin"
        appium_dir.mkdir(parents=True)
        appium_bin = appium_dir / "appium"
        appium_bin.write_text("#!/bin/sh\necho 3.1.0")
        appium_bin.chmod(0o755)

        monkeypatch.setattr(runtime, "check_appium_version", lambda bin, env=None: "3.1.0")

        result = runtime.ensure_appium()
        assert result == appium_bin

    def test_uses_managed_npm(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        # Create fake npm binary
        node_dir = tmp_path / ".autosana" / "node" / f"v{DEFAULT_NODE_VERSION}" / "bin"
        node_dir.mkdir(parents=True)
        npm_bin = node_dir / "npm"
        npm_bin.write_text("#!/bin/sh\necho npm")
        npm_bin.chmod(0o755)

        # find_appium returns None (not installed yet)
        monkeypatch.setattr(runtime, "find_appium", lambda version=None: None)

        # Mock subprocess.run to capture the npm call
        calls: list[list[str]] = []
        fake_result = MagicMock()
        fake_result.returncode = 0

        def fake_run(cmd, **kwargs):
            calls.append(list(cmd))
            return fake_result

        monkeypatch.setattr("autosana_cli.runtime.subprocess.run", fake_run)

        # Create the expected appium binary so ensure_appium can verify
        appium_dir = tmp_path / ".autosana" / "appium" / "node_modules" / ".bin"
        appium_dir.mkdir(parents=True)
        appium_bin = appium_dir / "appium"
        appium_bin.write_text("#!/bin/sh\necho 3.1.0")
        appium_bin.chmod(0o755)
        monkeypatch.setattr(runtime, "check_appium_version", lambda bin, env=None: "3.1.0")

        runtime.ensure_appium()

        assert len(calls) == 1
        assert "--prefix" in calls[0]
        assert "appium@3.1.0" in calls[0]


class TestEnsureDriver:
    def test_uses_appium_driver_install(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        monkeypatch.setattr(runtime, "check_driver_installed", lambda name: False)

        # Create fake appium binary
        appium_dir = tmp_path / ".autosana" / "appium" / "node_modules" / ".bin"
        appium_dir.mkdir(parents=True)
        appium_bin = appium_dir / "appium"
        appium_bin.write_text("#!/bin/sh\necho 3.1.0")
        appium_bin.chmod(0o755)
        monkeypatch.setattr(runtime, "find_appium", lambda version=None: appium_bin)

        calls: list[list[str]] = []
        fake_result = MagicMock()
        fake_result.returncode = 0

        def fake_run(cmd, **kwargs):
            calls.append(list(cmd))
            return fake_result

        monkeypatch.setattr("autosana_cli.runtime.subprocess.run", fake_run)

        runtime.ensure_driver("xcuitest", "10.2.2")

        assert len(calls) == 1
        assert "driver" in calls[0]
        assert "install" in calls[0]
        assert "xcuitest@10.2.2" in calls[0]

    def test_skips_if_already_installed(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setattr(runtime, "check_driver_installed", lambda name: True)

        calls: list = []
        monkeypatch.setattr(
            "autosana_cli.runtime.subprocess.run", lambda cmd, **kw: calls.append(cmd) or MagicMock(returncode=0)
        )

        runtime.ensure_driver("xcuitest", "10.2.2")
        assert len(calls) == 0  # No subprocess call made


class TestEnsureDrivers:
    def test_ios_installs_xcuitest(self, monkeypatch: pytest.MonkeyPatch) -> None:
        installed: list[str] = []
        monkeypatch.setattr(runtime, "ensure_driver", lambda name, version: installed.append(name))
        runtime.ensure_drivers(Platform.IOS)
        assert installed == ["xcuitest"]

    def test_android_installs_uiautomator2(self, monkeypatch: pytest.MonkeyPatch) -> None:
        installed: list[str] = []
        monkeypatch.setattr(runtime, "ensure_driver", lambda name, version: installed.append(name))
        runtime.ensure_drivers(Platform.ANDROID)
        assert installed == ["uiautomator2"]

    def test_none_installs_both(self, monkeypatch: pytest.MonkeyPatch) -> None:
        installed: list[str] = []
        monkeypatch.setattr(runtime, "ensure_driver", lambda name, version: installed.append(name))
        runtime.ensure_drivers(None)
        assert installed == ["xcuitest", "uiautomator2"]
