from __future__ import annotations

import json

from typer.testing import CliRunner

from autosana_cli import doctor, runtime, tunnel
from autosana_cli.cli import app
from autosana_cli.models import DoctorCheck, DoctorReport, Platform

runner = CliRunner()


# ---------------------------------------------------------------------------
# run_fix
# ---------------------------------------------------------------------------


class TestRunFix:
    def test_calls_ensure_chain_in_order(self, monkeypatch) -> None:
        call_order: list[str] = []
        monkeypatch.setattr(runtime, "ensure_node", lambda version=None: call_order.append("node"))
        monkeypatch.setattr(runtime, "ensure_appium", lambda version=None: call_order.append("appium"))
        monkeypatch.setattr(runtime, "ensure_drivers", lambda p: call_order.append("drivers"))
        monkeypatch.setattr(tunnel, "ensure_cloudflared", lambda version=None: call_order.append("cloudflared"))
        monkeypatch.setattr(
            "autosana_cli.doctor.run_preflight",
            lambda p: DoctorReport(p, [DoctorCheck("ok", True, "ok")]),
        )

        doctor.run_fix(Platform.IOS)
        assert call_order == ["node", "appium", "drivers", "cloudflared"]

    def test_emits_steps(self, monkeypatch) -> None:
        monkeypatch.setattr(runtime, "ensure_node", lambda version=None: None)
        monkeypatch.setattr(runtime, "ensure_appium", lambda version=None: None)
        monkeypatch.setattr(runtime, "ensure_drivers", lambda p: None)
        monkeypatch.setattr(tunnel, "ensure_cloudflared", lambda version=None: None)
        monkeypatch.setattr(
            "autosana_cli.doctor.run_preflight",
            lambda p: DoctorReport(p, [DoctorCheck("ok", True, "ok")]),
        )

        steps: list[tuple[str, str]] = []
        doctor.run_fix(Platform.IOS, on_step=lambda n, s: steps.append((n, s)))

        step_names = [name for name, _ in steps]
        assert "node" in step_names
        assert "appium" in step_names
        assert "drivers" in step_names
        assert "cloudflared" in step_names

    def test_continues_on_error(self, monkeypatch) -> None:
        """If node install fails, appium/drivers/cloudflared should still be attempted."""
        call_order: list[str] = []

        def fail_node(version=None):
            call_order.append("node")
            raise RuntimeError("download failed")

        monkeypatch.setattr(runtime, "ensure_node", fail_node)
        monkeypatch.setattr(runtime, "ensure_appium", lambda version=None: call_order.append("appium"))
        monkeypatch.setattr(runtime, "ensure_drivers", lambda p: call_order.append("drivers"))
        monkeypatch.setattr(tunnel, "ensure_cloudflared", lambda version=None: call_order.append("cloudflared"))
        monkeypatch.setattr(
            "autosana_cli.doctor.run_preflight",
            lambda p: DoctorReport(p, [DoctorCheck("node", False, "failed")]),
        )

        doctor.run_fix(Platform.IOS)
        assert call_order == ["node", "appium", "drivers", "cloudflared"]


# ---------------------------------------------------------------------------
# Doctor CLI
# ---------------------------------------------------------------------------


def _mock_all_ok(monkeypatch) -> None:
    monkeypatch.setattr(
        "autosana_cli.cli.doctor.run_preflight",
        lambda p: DoctorReport(p, [DoctorCheck("all", True, "ok")]),
    )


def _mock_fixable_failure(monkeypatch) -> None:
    monkeypatch.setattr(
        "autosana_cli.cli.doctor.run_preflight",
        lambda p: DoctorReport(p, [DoctorCheck("node", False, "missing", fixable=True)]),
    )


class TestDoctorCLI:
    def test_doctor_ok_exits_zero(self, monkeypatch) -> None:
        _mock_all_ok(monkeypatch)
        result = runner.invoke(app, ["doctor", "--platform", "ios"])
        assert result.exit_code == 0

    def test_doctor_failure_exits_nonzero(self, monkeypatch) -> None:
        _mock_fixable_failure(monkeypatch)
        result = runner.invoke(app, ["doctor", "--platform", "ios"])
        assert result.exit_code == 1

    def test_doctor_json_output(self, monkeypatch) -> None:
        _mock_all_ok(monkeypatch)
        result = runner.invoke(app, ["doctor", "--platform", "ios", "--json"])
        assert result.exit_code == 0
        payload = json.loads(result.stdout)
        assert payload["status"] == "ok"

    def test_doctor_fix_flag_triggers_fix(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "autosana_cli.cli.doctor.run_fix",
            lambda p, on_step=None: DoctorReport(p, [DoctorCheck("ok", True, "ok")]),
        )
        result = runner.invoke(app, ["doctor", "--fix", "--platform", "ios"])
        assert result.exit_code == 0

    def test_doctor_fix_json(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "autosana_cli.cli.doctor.run_fix",
            lambda p, on_step=None: DoctorReport(p, [DoctorCheck("ok", True, "ok")]),
        )
        result = runner.invoke(app, ["doctor", "--fix", "--json", "--platform", "ios"])
        assert result.exit_code == 0
        # Should contain JSON output
        lines = [line for line in result.stdout.strip().split("\n") if line.strip()]
        assert len(lines) >= 1
        payload = json.loads(lines[-1])
        assert payload["status"] == "ok"

    def test_doctor_shows_fixable_annotation(self, monkeypatch) -> None:
        _mock_fixable_failure(monkeypatch)
        result = runner.invoke(app, ["doctor", "--platform", "ios"])
        assert "auto-fixable" in result.stdout


# ---------------------------------------------------------------------------
# Preflight checks
# ---------------------------------------------------------------------------


class TestPreflight:
    def test_env_checks_come_first(self, monkeypatch) -> None:
        monkeypatch.setattr(runtime, "find_node", lambda version=None: None)
        monkeypatch.setattr(runtime, "find_appium", lambda version=None: None)
        monkeypatch.setattr(runtime, "check_appium_version", lambda bin, env=None: None)
        monkeypatch.setattr(runtime, "check_driver_installed", lambda name: False)
        monkeypatch.setattr(tunnel, "find_cloudflared_binary", lambda: None)
        monkeypatch.setattr(tunnel, "cloudflared_download_info", lambda: None)
        monkeypatch.setattr("autosana_cli.doctor._has_command", lambda cmd: False)
        monkeypatch.setattr("autosana_cli.doctor.device_service.list_available_ios_simulators", lambda: [])

        report = doctor.run_preflight(Platform.IOS)
        names = [c.name for c in report.checks]
        assert names[0] == "Disk Space"
        assert "Node.js" in names

    def test_android_checks_java_home(self, monkeypatch) -> None:
        monkeypatch.setattr(runtime, "find_node", lambda version=None: None)
        monkeypatch.setattr(runtime, "find_appium", lambda version=None: None)
        monkeypatch.setattr(runtime, "check_appium_version", lambda bin, env=None: None)
        monkeypatch.setattr(runtime, "check_driver_installed", lambda name: False)
        monkeypatch.setattr(tunnel, "find_cloudflared_binary", lambda: None)
        monkeypatch.setattr(tunnel, "cloudflared_download_info", lambda: None)
        monkeypatch.setattr("autosana_cli.doctor._has_command", lambda cmd: False)
        monkeypatch.delenv("JAVA_HOME", raising=False)
        monkeypatch.delenv("ANDROID_HOME", raising=False)
        monkeypatch.delenv("ANDROID_SDK_ROOT", raising=False)

        report = doctor.run_preflight(Platform.ANDROID)
        check_names = [c.name for c in report.checks]
        assert "JAVA_HOME" in check_names
        assert "ANDROID_HOME" in check_names


# ---------------------------------------------------------------------------
# Up command auto-fix
# ---------------------------------------------------------------------------


class TestUpAutoFix:
    def _mock_up_deps(self, monkeypatch) -> None:
        """Mock all dependencies for the up command after preflight passes."""
        from autosana_cli.models import Device, Platform

        device = Device(platform=Platform.IOS, name="iPhone 15", udid="TEST-UDID")
        # Mock the new device detection flow (list_booted returns one device)
        monkeypatch.setattr("autosana_cli.cli.devices.list_booted_ios_simulators", lambda: [device])
        monkeypatch.setattr("autosana_cli.cli.devices.list_installed_apps", lambda d: ([], True))
        monkeypatch.setattr("autosana_cli.cli.allocate_session_ports", lambda s, p, port: (4723, 8100))

        class FakeProc:
            pid = 12345

            def poll(self):
                return None

            def wait(self):
                return 0

        monkeypatch.setattr("autosana_cli.cli.appium.start_appium", lambda **kw: FakeProc())
        monkeypatch.setattr("autosana_cli.cli.appium.wait_for_appium_ready", lambda port, proc: None)
        monkeypatch.setattr("autosana_cli.cli.tunnel.ensure_cloudflared", lambda: "/usr/bin/cloudflared")
        monkeypatch.setattr(
            "autosana_cli.cli.tunnel.start_tunnel", lambda **kw: (FakeProc(), "https://test.trycloudflare.com")
        )
        monkeypatch.setattr("autosana_cli.cli.register_cleanup", lambda fn: None)

    def test_up_auto_fixes_when_fixable(self, monkeypatch) -> None:
        call_count = {"preflight": 0, "fix": 0}

        def mock_preflight(p):
            call_count["preflight"] += 1
            if call_count["preflight"] == 1:
                return DoctorReport(p, [DoctorCheck("node", False, "missing", fixable=True)])
            return DoctorReport(p, [DoctorCheck("node", True, "ok")])

        def mock_fix(p, on_step=None):
            call_count["fix"] += 1
            return DoctorReport(p, [DoctorCheck("node", True, "ok")])

        monkeypatch.setattr("autosana_cli.cli.doctor.run_preflight", mock_preflight)
        monkeypatch.setattr("autosana_cli.cli.doctor.run_fix", mock_fix)
        self._mock_up_deps(monkeypatch)

        runner.invoke(app, ["up", "--platform", "ios", "--detach"])
        assert call_count["fix"] == 1

    def test_up_no_fix_skips_auto_fix(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "autosana_cli.cli.doctor.run_preflight",
            lambda p: DoctorReport(p, [DoctorCheck("node", False, "missing", fixable=True)]),
        )
        fix_called = {"value": False}
        monkeypatch.setattr(
            "autosana_cli.cli.doctor.run_fix",
            lambda p, on_step=None: (fix_called.update(value=True), DoctorReport(p, []))[1],
        )

        result = runner.invoke(app, ["up", "--platform", "ios", "--no-fix"])
        assert result.exit_code == 1
        assert not fix_called["value"]
