from __future__ import annotations

from pathlib import Path
import platform
import plistlib
import subprocess

import pytest

from a2a_agent import autostart


class RecordingRunner:
    def __init__(self, returncode: int = 0) -> None:
        self.returncode = returncode
        self.calls: list[tuple[list[str], dict[str, object]]] = []

    def __call__(self, command: list[str], **kwargs):
        self.calls.append((list(command), dict(kwargs)))
        return subprocess.CompletedProcess(command, self.returncode, "", "")


def _finder(paths: dict[str, Path | str]):
    def find(program: str) -> str | None:
        value = paths.get(program)
        return None if value is None else str(value)

    return find


def test_install_macos_writes_valid_plist_backup_and_only_lints(tmp_path) -> None:
    agent = tmp_path / "venv bin" / "a2a-agent"
    cursor = tmp_path / "cursor bin" / "cursor-agent"
    agent.parent.mkdir()
    cursor.parent.mkdir()
    agent.write_text("agent", encoding="utf-8")
    cursor.write_text("cursor", encoding="utf-8")

    plist_path = (
        tmp_path
        / "Library"
        / "LaunchAgents"
        / f"{autostart.MACOS_LABEL}.plist"
    )
    plist_path.parent.mkdir(parents=True)
    plist_path.write_bytes(b"old plist")
    calls: list[tuple[list[str], dict[str, object]]] = []

    def lint_runner(command: list[str], **kwargs):
        calls.append((list(command), dict(kwargs)))
        with Path(command[2]).open("rb") as file:
            plistlib.load(file)
        return subprocess.CompletedProcess(command, 0, "OK", "")

    result = autostart.install_autostart(
        "cursor",
        system_name="Darwin",
        home=tmp_path,
        executable=agent,
        environ={"PATH": "/usr/bin:/bin"},
        which=_finder({"cursor-agent": cursor, "plutil": "/usr/bin/plutil"}),
        run_command=lint_runner,
    )

    payload = plistlib.loads(plist_path.read_bytes())
    assert payload["Label"] == autostart.MACOS_LABEL
    assert payload["ProgramArguments"] == [str(agent.resolve()), "run"]
    assert payload["RunAtLoad"] is True
    assert payload["KeepAlive"] is True
    assert payload["EnvironmentVariables"] == {
        "A2A_EXECUTOR": "cursor",
        "PATH": f"{cursor.resolve().parent}:/usr/bin:/bin",
    }
    assert payload["StandardOutPath"] == str(
        tmp_path / ".a2a-agent" / "worker.out.log"
    )
    assert payload["StandardErrorPath"] == str(
        tmp_path / ".a2a-agent" / "worker.err.log"
    )
    assert (plist_path.with_name(f"{plist_path.name}.bak")).read_bytes() == b"old plist"
    assert result.enabled
    assert not result.started
    assert result.backup_path == plist_path.with_name(f"{plist_path.name}.bak")
    assert len(calls) == 1
    assert calls[0][0][0:2] == ["/usr/bin/plutil", "-lint"]
    assert calls[0][1]["check"] is True
    assert all("launchctl" not in part for call, _ in calls for part in call)


def test_macos_lint_failure_preserves_existing_plist(tmp_path) -> None:
    agent = tmp_path / "a2a-agent"
    opencode = tmp_path / "bin" / "opencode"
    agent.write_text("agent", encoding="utf-8")
    opencode.parent.mkdir()
    opencode.write_text("opencode", encoding="utf-8")
    plist_path = (
        tmp_path
        / "Library"
        / "LaunchAgents"
        / f"{autostart.MACOS_LABEL}.plist"
    )
    plist_path.parent.mkdir(parents=True)
    plist_path.write_bytes(b"keep me")

    def failing_lint(command: list[str], **kwargs):
        raise subprocess.CalledProcessError(1, command, stderr="invalid")

    with pytest.raises(
        autostart.AutostartError, match="plutil validation failed"
    ) as raised:
        autostart.install_autostart(
            "opencode",
            system_name="Darwin",
            home=tmp_path,
            executable=agent,
            environ={"PATH": "/usr/bin"},
            which=_finder({"opencode": opencode, "plutil": "/usr/bin/plutil"}),
            run_command=failing_lint,
        )

    assert "invalid" in str(raised.value)
    assert plist_path.read_bytes() == b"keep me"
    assert not plist_path.with_name(f"{plist_path.name}.bak").exists()


@pytest.mark.skipif(
    platform.system() != "Darwin" or not Path("/usr/bin/plutil").exists(),
    reason="the system plutil check only runs on macOS",
)
def test_build_macos_plist_passes_system_plutil(tmp_path) -> None:
    plist_path = tmp_path / "worker.plist"
    plist_path.write_bytes(
        autostart.build_macos_plist(
            "cursor",
            Path("/opt/a2a/bin/a2a-agent"),
            "/opt/cursor/bin:/usr/bin:/bin",
            tmp_path / ".a2a-agent",
        )
    )

    completed = subprocess.run(
        ["/usr/bin/plutil", "-lint", str(plist_path)],
        check=False,
        capture_output=True,
        text=True,
    )

    assert completed.returncode == 0, completed.stderr or completed.stdout


def test_install_windows_writes_startup_entry_without_schtasks_or_running(tmp_path) -> None:
    home = tmp_path / "user home"
    appdata = tmp_path / "app data"
    agent = tmp_path / "Program Files" / "a2a-agent.exe"
    grok = tmp_path / "npm bin" / "grok.cmd"
    agent.parent.mkdir()
    grok.parent.mkdir()
    agent.write_text("agent", encoding="utf-8")
    grok.write_text("grok", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.install_autostart(
        "grok",
        system_name="Windows",
        home=home,
        executable=agent,
        environ={
            "APPDATA": str(appdata),
            "PATH": r"C:\Windows\System32",
        },
        which=_finder({"grok": grok}),
        run_command=runner,
    )

    command_path = home / ".a2a-agent" / "start-worker.cmd"
    startup_path = (
        appdata
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / autostart.WINDOWS_STARTUP_FILE_NAME
    )
    content = command_path.read_text(encoding="utf-8")
    assert '@echo off\nset "A2A_EXECUTOR=grok"\n' in content
    assert f'set "PATH={grok.resolve().parent};C:\\Windows\\System32"' in content
    assert f'"{agent.resolve()}" run' in content
    assert startup_path.read_text(encoding="utf-8") == (
        "@echo off\n"
        f'start "" /min "{command_path}"\n'
    )
    assert runner.calls == []
    assert result.path == startup_path
    assert result.enabled
    assert not result.started


def test_install_windows_falls_back_to_user_roaming_startup(tmp_path) -> None:
    agent = tmp_path / "a2a-agent.exe"
    agent.write_text("agent", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.install_autostart(
        "echo",
        system_name="Windows",
        home=tmp_path,
        executable=agent,
        environ={"PATH": r"C:\Windows\System32"},
        run_command=runner,
    )

    startup_path = (
        tmp_path
        / "AppData"
        / "Roaming"
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / autostart.WINDOWS_STARTUP_FILE_NAME
    )
    assert startup_path.exists()
    assert result.path == startup_path
    assert result.enabled
    assert runner.calls == []


def test_install_windows_backs_up_both_existing_wrappers(tmp_path) -> None:
    appdata = tmp_path / "app data"
    agent = tmp_path / "a2a-agent.exe"
    worker_path = tmp_path / ".a2a-agent" / "start-worker.cmd"
    startup_path = (
        appdata
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / autostart.WINDOWS_STARTUP_FILE_NAME
    )
    agent.write_text("agent", encoding="utf-8")
    worker_path.parent.mkdir()
    worker_path.write_text("old worker", encoding="utf-8")
    startup_path.parent.mkdir(parents=True)
    startup_path.write_text("old startup", encoding="utf-8")

    result = autostart.install_autostart(
        "echo",
        system_name="Windows",
        home=tmp_path,
        executable=agent,
        environ={"APPDATA": str(appdata), "PATH": r"C:\Windows\System32"},
        run_command=RecordingRunner(),
    )

    assert worker_path.with_name("start-worker.cmd.bak").read_text(
        encoding="utf-8"
    ) == "old worker"
    assert startup_path.with_name(f"{startup_path.name}.bak").read_text(
        encoding="utf-8"
    ) == "old startup"
    assert result.backup_path == startup_path.with_name(f"{startup_path.name}.bak")


def test_install_linux_enables_without_starting(tmp_path) -> None:
    agent = tmp_path / "a2a-agent"
    opencode = tmp_path / "opencode bin" / "opencode"
    agent.write_text("agent", encoding="utf-8")
    opencode.parent.mkdir()
    opencode.write_text("opencode", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.install_autostart(
        "opencode",
        system_name="Linux",
        home=tmp_path,
        executable=agent,
        environ={"PATH": "/usr/local/bin:/usr/bin"},
        which=_finder(
            {
                "opencode": opencode,
                "systemctl": "/usr/bin/systemctl",
            }
        ),
        run_command=runner,
    )

    service_path = (
        tmp_path / ".config" / "systemd" / "user" / autostart.LINUX_UNIT_NAME
    )
    content = service_path.read_text(encoding="utf-8")
    assert "Environment=\"A2A_EXECUTOR=opencode\"" in content
    assert (
        f'Environment="PATH={opencode.resolve().parent}:'
        '/usr/local/bin:/usr/bin"'
    ) in content
    assert f'ExecStart="{agent.resolve()}" run' in content
    assert "Restart=always" in content
    assert "WantedBy=default.target" in content
    assert runner.calls[0][0] == [
        "/usr/bin/systemctl",
        "--user",
        "enable",
        autostart.LINUX_UNIT_NAME,
    ]
    assert "start" not in runner.calls[0][0]
    assert "--now" not in runner.calls[0][0]
    assert result.enabled
    assert result.manual_command is None


@pytest.mark.parametrize("returncode", [1, 5])
def test_install_linux_keeps_file_and_prints_manual_enable_on_failure(
    tmp_path, returncode: int
) -> None:
    agent = tmp_path / "a2a-agent"
    cursor = tmp_path / "bin" / "cursor-agent"
    agent.write_text("agent", encoding="utf-8")
    cursor.parent.mkdir()
    cursor.write_text("cursor", encoding="utf-8")
    runner = RecordingRunner(returncode=returncode)

    result = autostart.install_autostart(
        "cursor",
        system_name="Linux",
        home=tmp_path,
        executable=agent,
        environ={"PATH": "/usr/bin"},
        which=_finder(
            {"cursor-agent": cursor, "systemctl": "/usr/bin/systemctl"}
        ),
        run_command=runner,
    )

    assert result.path.exists()
    assert not result.enabled
    assert result.manual_command == (
        f"systemctl --user enable {autostart.LINUX_UNIT_NAME}"
    )
    assert "start" not in runner.calls[0][0]
    assert "--now" not in runner.calls[0][0]


def test_install_linux_without_systemctl_only_writes_file(tmp_path) -> None:
    agent = tmp_path / "a2a-agent"
    cursor = tmp_path / "bin" / "cursor-agent"
    agent.write_text("agent", encoding="utf-8")
    cursor.parent.mkdir()
    cursor.write_text("cursor", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.install_autostart(
        "cursor",
        system_name="Linux",
        home=tmp_path,
        executable=agent,
        environ={"PATH": "/usr/bin"},
        which=_finder({"cursor-agent": cursor}),
        run_command=runner,
    )

    assert result.path.exists()
    assert not result.enabled
    assert result.manual_command == (
        f"systemctl --user enable {autostart.LINUX_UNIT_NAME}"
    )
    assert runner.calls == []


def test_uninstall_macos_only_removes_plist(tmp_path) -> None:
    path = (
        tmp_path
        / "Library"
        / "LaunchAgents"
        / f"{autostart.MACOS_LABEL}.plist"
    )
    path.parent.mkdir(parents=True)
    path.write_text("plist", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.uninstall_autostart(
        system_name="Darwin",
        home=tmp_path,
        run_command=runner,
    )

    assert not path.exists()
    assert runner.calls == []
    assert not result.enabled
    assert not result.started


def test_uninstall_windows_deletes_startup_and_worker_without_schtasks(tmp_path) -> None:
    appdata = tmp_path / "app data"
    worker_path = tmp_path / ".a2a-agent" / "start-worker.cmd"
    startup_path = (
        appdata
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / autostart.WINDOWS_STARTUP_FILE_NAME
    )
    worker_path.parent.mkdir()
    worker_path.write_text("worker", encoding="utf-8")
    startup_path.parent.mkdir(parents=True)
    startup_path.write_text("startup", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.uninstall_autostart(
        system_name="Windows",
        home=tmp_path,
        environ={"APPDATA": str(appdata)},
        run_command=runner,
    )

    assert not worker_path.exists()
    assert not startup_path.exists()
    assert runner.calls == []
    assert result.path == startup_path
    assert not result.enabled


def test_uninstall_windows_ignores_absent_wrappers(tmp_path) -> None:
    appdata = tmp_path / "app data"
    runner = RecordingRunner(returncode=1)

    result = autostart.uninstall_autostart(
        system_name="Windows",
        home=tmp_path,
        environ={"APPDATA": str(appdata)},
        run_command=runner,
    )

    startup_path = (
        appdata
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / autostart.WINDOWS_STARTUP_FILE_NAME
    )
    assert result.path == startup_path
    assert not result.enabled
    assert runner.calls == []


def test_uninstall_linux_disables_without_stopping(tmp_path) -> None:
    path = tmp_path / ".config" / "systemd" / "user" / autostart.LINUX_UNIT_NAME
    path.parent.mkdir(parents=True)
    path.write_text("service", encoding="utf-8")
    runner = RecordingRunner()

    result = autostart.uninstall_autostart(
        system_name="Linux",
        home=tmp_path,
        which=_finder({"systemctl": "/usr/bin/systemctl"}),
        run_command=runner,
    )

    assert not path.exists()
    assert runner.calls[0][0] == [
        "/usr/bin/systemctl",
        "--user",
        "disable",
        autostart.LINUX_UNIT_NAME,
    ]
    assert "stop" not in runner.calls[0][0]
    assert "--now" not in runner.calls[0][0]
    assert not result.enabled


def test_uninstall_linux_failure_keeps_unit_for_manual_recovery(tmp_path) -> None:
    path = tmp_path / ".config" / "systemd" / "user" / autostart.LINUX_UNIT_NAME
    path.parent.mkdir(parents=True)
    path.write_text("service", encoding="utf-8")
    runner = RecordingRunner(returncode=1)

    result = autostart.uninstall_autostart(
        system_name="Linux",
        home=tmp_path,
        which=_finder({"systemctl": "/usr/bin/systemctl"}),
        run_command=runner,
    )

    assert path.exists()
    assert result.enabled
    assert result.manual_command == (
        f"systemctl --user disable {autostart.LINUX_UNIT_NAME}"
    )


def test_executor_binary_must_be_resolvable(tmp_path) -> None:
    agent = tmp_path / "a2a-agent"
    agent.write_text("agent", encoding="utf-8")

    with pytest.raises(autostart.AutostartError, match="cursor-agent"):
        autostart.install_autostart(
            "cursor",
            system_name="Linux",
            home=tmp_path,
            executable=agent,
            environ={"PATH": "/usr/bin"},
            which=lambda program: None,
            run_command=RecordingRunner(),
        )


def test_resolve_agent_executable_falls_back_from_argv0_to_which(tmp_path) -> None:
    installed = tmp_path / "venv" / "bin" / "a2a-agent"
    installed.parent.mkdir(parents=True)
    installed.write_text("agent", encoding="utf-8")
    lookups: list[str] = []

    def find(program: str) -> str | None:
        lookups.append(program)
        if program == "a2a-agent":
            return str(installed)
        return None

    resolved = autostart.resolve_agent_executable(
        argv0=str(tmp_path / "src" / "a2a_agent" / "cli.py"),
        which=find,
    )

    assert resolved == installed.resolve()
    assert lookups == [
        str(tmp_path / "src" / "a2a_agent" / "cli.py"),
        "a2a-agent",
    ]


def test_replace_with_retry_retries_permission_errors_on_windows(tmp_path) -> None:
    source = tmp_path / "temporary.cmd"
    destination = tmp_path / "start-worker.cmd"
    calls: list[tuple[Path, Path]] = []
    sleeps: list[float] = []

    def flaky_replace(current_source: Path, current_destination: Path) -> None:
        calls.append((current_source, current_destination))
        if len(calls) <= 2:
            raise PermissionError(5, "Access is denied")

    autostart._replace_with_retry(
        source,
        destination,
        replace=flaky_replace,
        sleep=sleeps.append,
        is_windows=True,
    )

    assert calls == [(source, destination)] * 3
    assert sleeps == [0.1, 0.2]


def test_replace_with_retry_raises_final_windows_error_and_warns(
    tmp_path, capsys
) -> None:
    source = tmp_path / "temporary.cmd"
    destination = tmp_path / "start-worker.cmd"
    expected = PermissionError(5, "Access is denied")
    calls: list[tuple[Path, Path]] = []
    sleeps: list[float] = []

    def always_locked(current_source: Path, current_destination: Path) -> None:
        calls.append((current_source, current_destination))
        raise expected

    with pytest.raises(PermissionError) as raised:
        autostart._replace_with_retry(
            source,
            destination,
            replace=always_locked,
            sleep=sleeps.append,
            is_windows=True,
        )

    assert raised.value is expected
    assert calls == [(source, destination)] * 6
    assert sleeps == [0.1, 0.2, 0.4, 0.8, 1.6]
    assert "antivirus real-time scanning" in capsys.readouterr().err


def test_replace_with_retry_does_not_retry_off_windows(tmp_path) -> None:
    source = tmp_path / "temporary.cmd"
    destination = tmp_path / "start-worker.cmd"
    expected = OSError("replace failed")
    calls: list[tuple[Path, Path]] = []
    sleeps: list[float] = []

    def failing_replace(current_source: Path, current_destination: Path) -> None:
        calls.append((current_source, current_destination))
        raise expected

    with pytest.raises(OSError) as raised:
        autostart._replace_with_retry(
            source,
            destination,
            replace=failing_replace,
            sleep=sleeps.append,
            is_windows=False,
        )

    assert raised.value is expected
    assert calls == [(source, destination)]
    assert sleeps == []


def test_copy_with_retry_retries_permission_errors_on_windows(tmp_path) -> None:
    source = tmp_path / "start-worker.cmd"
    destination = tmp_path / "start-worker.cmd.bak"
    calls: list[tuple[Path, Path]] = []
    sleeps: list[float] = []

    def flaky_copy(current_source: Path, current_destination: Path) -> None:
        calls.append((current_source, current_destination))
        if len(calls) == 1:
            raise PermissionError(5, "Access is denied")

    autostart._copy_with_retry(
        source,
        destination,
        copy=flaky_copy,
        sleep=sleeps.append,
        is_windows=True,
    )

    assert calls == [(source, destination)] * 2
    assert sleeps == [0.1]
