"""Regression tests for issue #31015 — Termux:API app detection.

`/voice on` was reporting "Termux:API Android app is not installed"
even on devices where the app *is* installed and the
`termux-microphone-record` binary works fine. The cause was a single
brittle probe (`pm list packages com.termux.api`) that returns a
false negative on some Android versions / Termux configurations.

These tests pin the new probe ladder:

  1. `pm list packages` confirms the app → True (back-compat).
  2. `pm` missing or non-zero → fall back to
     `cmd package list packages` (modern Android API 28+).
  3. Both probes inconclusive but `termux-microphone-record` on PATH
     → trust the binary, return True.
  4. At least one probe ran cleanly and definitively did not list
     the package → return False (the genuine "CLI without app"
     case the existing warning was written for).
"""

from __future__ import annotations

import subprocess
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest


# ── Helpers ────────────────────────────────────────────────────────────────


def _make_run_dispatcher(behaviors):
    """Build a fake `subprocess.run` that returns / raises per-command.

    `behaviors` maps the first arg of each invocation to either a
    SimpleNamespace (returncode, stdout, stderr) or an Exception class
    instance to raise.
    """
    def _fake_run(cmd, **kwargs):
        key = cmd[0] if isinstance(cmd, (list, tuple)) and cmd else cmd
        action = behaviors.get(key)
        if action is None:
            raise AssertionError(f"Unexpected probe command: {cmd!r}")
        if isinstance(action, BaseException):
            raise action
        return action
    return _fake_run


def _force_termux(monkeypatch):
    monkeypatch.setattr("tools.voice_mode._is_termux_environment", lambda: True)


# ── _termux_api_app_installed: probe ladder ───────────────────────────────


class TestTermuxApiAppInstalledProbeLadder:
    """The probe ladder is the heart of the #31015 fix — keep its truth
    table pinned so future "simplifications" can't silently regress it."""

    def test_returns_false_outside_termux(self, monkeypatch):
        monkeypatch.setattr("tools.voice_mode._is_termux_environment", lambda: False)
        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is False

    def test_pm_confirms_package_returns_true(self, monkeypatch):
        _force_termux(monkeypatch)
        run = _make_run_dispatcher({
            "pm": SimpleNamespace(
                returncode=0,
                stdout="package:com.termux.api\n",
                stderr="",
            ),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)

        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is True


    def test_pm_timeout_then_cmd_confirms(self, monkeypatch):
        """A hung `pm` (5s timeout) must not block detection."""
        _force_termux(monkeypatch)
        run = _make_run_dispatcher({
            "pm": subprocess.TimeoutExpired(cmd="pm", timeout=5),
            "cmd": SimpleNamespace(
                returncode=0,
                stdout="package:com.termux.api\n",
                stderr="",
            ),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)

        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is True

    def test_pm_nonzero_exit_then_cmd_confirms(self, monkeypatch):
        """Non-zero exit (e.g. permission denied for the calling user)
        is treated as inconclusive, not as a definitive "no"."""
        _force_termux(monkeypatch)
        run = _make_run_dispatcher({
            "pm": SimpleNamespace(returncode=1, stdout="", stderr="permission denied"),
            "cmd": SimpleNamespace(
                returncode=0,
                stdout="package:com.termux.api\n",
                stderr="",
            ),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)

        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is True

    def test_both_probes_inconclusive_but_binary_present_returns_true(self, monkeypatch):
        """Core #31015 case: both probes fail or are unavailable, but the
        `termux-microphone-record` binary is on PATH. Trust the binary —
        a false positive only surfaces a precise runtime error, while a
        false negative blocks /voice on entirely (the user-reported
        symptom)."""
        _force_termux(monkeypatch)
        run = _make_run_dispatcher({
            "pm": FileNotFoundError("pm: command not found"),
            "cmd": FileNotFoundError("cmd: command not found"),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)
        monkeypatch.setattr(
            "tools.voice_mode.shutil.which",
            lambda name: "/data/data/com.termux/files/usr/bin/termux-microphone-record"
            if name == "termux-microphone-record" else None,
        )

        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is True


    def test_match_is_case_insensitive(self, monkeypatch):
        """Defensive against ROMs that capitalise the prefix differently."""
        _force_termux(monkeypatch)
        run = _make_run_dispatcher({
            "pm": SimpleNamespace(
                returncode=0,
                stdout="Package:com.termux.api\n",
                stderr="",
            ),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)

        from tools.voice_mode import _termux_api_app_installed
        assert _termux_api_app_installed() is True


# ── End-to-end through detect_audio_environment ───────────────────────────


class TestDetectAudioEnvironmentTermuxFallback:
    """The point of the fix is that #31015 users with the binary on PATH
    no longer see the misleading 'Termux:API Android app is not installed'
    warning when the package-manager probe is inconclusive."""

    def test_inconclusive_probes_with_binary_does_not_emit_app_warning(
        self, monkeypatch
    ):
        monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
        monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
        monkeypatch.delenv("SSH_CLIENT", raising=False)
        monkeypatch.delenv("SSH_TTY", raising=False)
        monkeypatch.delenv("SSH_CONNECTION", raising=False)

        # No sounddevice — we go down the Termux:API branch.
        monkeypatch.setattr(
            "tools.voice_mode._import_audio",
            lambda: (_ for _ in ()).throw(ImportError("no audio libs")),
        )
        monkeypatch.setattr(
            "tools.voice_mode._termux_microphone_command",
            lambda: "/data/data/com.termux/files/usr/bin/termux-microphone-record",
        )
        # Both probes fail (the #31015 reproduction).
        run = _make_run_dispatcher({
            "pm": FileNotFoundError("pm: command not found"),
            "cmd": FileNotFoundError("cmd: command not found"),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)
        monkeypatch.setattr(
            "tools.voice_mode.shutil.which",
            lambda name: "/data/data/com.termux/files/usr/bin/termux-microphone-record"
            if name == "termux-microphone-record" else None,
        )

        from tools.voice_mode import detect_audio_environment
        result = detect_audio_environment()

        assert result["available"] is True, (
            f"Voice mode should be available when the binary is on PATH "
            f"and probes are inconclusive (issue #31015). Got: {result}"
        )
        assert not any(
            "Termux:API Android app is not installed" in w
            for w in result["warnings"]
        ), (
            "The misleading 'app is not installed' warning must not fire "
            "when probes are inconclusive but the binary works (issue "
            f"#31015). warnings={result['warnings']!r}"
        )
        assert any(
            "Termux:API microphone recording available" in n
            for n in result.get("notices", [])
        )

    def test_clean_probes_no_match_still_blocks(self, monkeypatch):
        """The genuine "CLI installed without the app" case still blocks
        with the existing warning — important so users don't lose the
        install hint when the package manager *can* tell us the truth."""
        monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
        monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
        monkeypatch.delenv("SSH_CLIENT", raising=False)
        monkeypatch.delenv("SSH_TTY", raising=False)
        monkeypatch.delenv("SSH_CONNECTION", raising=False)

        monkeypatch.setattr(
            "tools.voice_mode._import_audio",
            lambda: (_ for _ in ()).throw(ImportError("no audio libs")),
        )
        monkeypatch.setattr(
            "tools.voice_mode._termux_microphone_command",
            lambda: "/data/data/com.termux/files/usr/bin/termux-microphone-record",
        )
        run = _make_run_dispatcher({
            "pm": SimpleNamespace(returncode=0, stdout="", stderr=""),
            "cmd": SimpleNamespace(returncode=0, stdout="", stderr=""),
        })
        monkeypatch.setattr("tools.voice_mode.subprocess.run", run)

        from tools.voice_mode import detect_audio_environment
        result = detect_audio_environment()

        assert result["available"] is False
        assert any(
            "Termux:API Android app is not installed" in w
            for w in result["warnings"]
        )
