from __future__ import annotations

import subprocess
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from device_auth import DeviceAuthPrompt, terminate_process
from device_auth_dialog import (
    ClaudeCodeEntryDialog,
    DeviceAuthDialog,
    WARNING_COPY,
    _build_existing_accounts_lines,
)


class _SlowExitProcess:
    def __init__(self) -> None:
        self.terminate_calls = 0
        self.kill_calls = 0
        self.wait_calls: list[float | None] = []
        self.returncode = None

    def poll(self):
        return self.returncode

    def terminate(self) -> None:
        self.terminate_calls += 1

    def wait(self, timeout=None):
        self.wait_calls.append(timeout)
        if len(self.wait_calls) == 1:
            raise subprocess.TimeoutExpired(cmd="codex", timeout=timeout)
        self.returncode = -9
        return self.returncode

    def kill(self) -> None:
        self.kill_calls += 1
        self.returncode = -9


class _Clipboard:
    def __init__(self) -> None:
        self.text = ""

    def set_text(self, text: str, _length: int) -> None:
        self.text = text


def _prompt_fixture() -> DeviceAuthPrompt:
    return DeviceAuthPrompt(
        url="https://auth.openai.com/codex/device",
        code="ABCD-EFGH",
        raw_text="Visit https://auth.openai.com/codex/device and enter ABCD-EFGH",
    )


def _widget_text(widget: object) -> str:
    if hasattr(widget, "get_text"):
        return widget.get_text()
    if hasattr(widget, "get_label"):
        return widget.get_label()
    if hasattr(widget, "label"):
        return widget.label
    raise AssertionError(f"widget has no readable text API: {type(widget)!r}")


def _widget_visible(widget: object) -> bool:
    if hasattr(widget, "is_visible"):
        return widget.is_visible()
    if hasattr(widget, "get_visible"):
        return widget.get_visible()
    if hasattr(widget, "visible"):
        return widget.visible
    raise AssertionError(f"widget has no readable visibility API: {type(widget)!r}")


def _widget_sensitive(widget: object) -> bool:
    if hasattr(widget, "get_sensitive"):
        return widget.get_sensitive()
    if hasattr(widget, "sensitive"):
        return widget.sensitive
    raise AssertionError(f"widget has no readable sensitivity API: {type(widget)!r}")


def test_cancel_button_runs_cancel_handler_and_destroys_dialog() -> None:
    calls: list[str] = []
    dialog = DeviceAuthDialog(_prompt_fixture(), on_cancel=lambda: calls.append("cancel"))
    destroyed: list[bool] = []
    dialog.connect("destroy", lambda _w: destroyed.append(True))

    dialog.cancel_button.clicked()

    assert calls == ["cancel"]
    assert destroyed == [True]


def test_delete_event_uses_same_cancel_handler_once() -> None:
    calls: list[str] = []
    dialog = DeviceAuthDialog(_prompt_fixture(), on_cancel=lambda: calls.append("cancel"))
    destroyed: list[bool] = []
    dialog.connect("destroy", lambda _w: destroyed.append(True))

    result = dialog.emit("delete-event", None)
    dialog.cancel_button.clicked()

    assert result is True
    assert calls == ["cancel"]
    assert destroyed == [True]


def test_build_existing_accounts_lines_formats_alias_and_email() -> None:
    assert _build_existing_accounts_lines([("Work", "work@example.com")]) == [
        "Work — work@example.com"
    ]


def test_dialog_shows_warning_copy_only_for_add_account() -> None:
    prompt = _prompt_fixture()

    add_dialog = DeviceAuthDialog(
        prompt,
        existing_accounts=[("Work", "work@example.com"), ("Personal", "me@example.com")],
    )
    repair_dialog = DeviceAuthDialog(prompt, existing_accounts=None)

    assert _widget_text(add_dialog.warning_label) == WARNING_COPY
    assert "Work — work@example.com" in _widget_text(add_dialog.existing_accounts_label)
    assert add_dialog.modal is False
    assert _widget_text(repair_dialog.existing_accounts_label) == ""


def test_copy_buttons_write_code_and_full_text(monkeypatch: pytest.MonkeyPatch) -> None:
    prompt = _prompt_fixture()
    dialog = DeviceAuthDialog(prompt)

    clipboard = _Clipboard()
    monkeypatch.setattr(dialog, "_clipboard", lambda: clipboard)

    dialog._handle_copy_code(None)
    assert clipboard.text == prompt.code

    dialog._handle_copy_full_text(None)
    assert clipboard.text == prompt.raw_text


def test_show_failure_toggles_retry_visibility() -> None:
    prompt = _prompt_fixture()
    dialog = DeviceAuthDialog(prompt, on_retry=lambda: None)

    dialog.show_failure(retry_available=False)
    assert _widget_visible(dialog.failure_label) is True
    assert _widget_visible(dialog.retry_button) is False
    assert _widget_sensitive(dialog.retry_button) is False

    dialog.show_failure(retry_available=True)
    assert _widget_visible(dialog.retry_button) is True
    assert _widget_sensitive(dialog.retry_button) is True


def test_open_in_firefox_surfaces_inline_error(monkeypatch: pytest.MonkeyPatch) -> None:
    prompt = _prompt_fixture()
    dialog = DeviceAuthDialog(prompt)

    def fake_popen(_args):
        raise OSError("missing firefox")

    monkeypatch.setattr(subprocess, "Popen", fake_popen)
    dialog._handle_open_clicked(None)

    assert _widget_visible(dialog.firefox_error_label) is True
    assert "missing firefox" in _widget_text(dialog.firefox_error_label)


def test_retry_button_invokes_callback() -> None:
    prompt = _prompt_fixture()
    calls: list[str] = []
    dialog = DeviceAuthDialog(prompt, on_retry=lambda: calls.append("retry"))

    dialog._handle_retry(None)

    assert calls == ["retry"]


def test_claude_code_entry_dialog_builds_without_display() -> None:
    dialog = ClaudeCodeEntryDialog(
        "https://auth.anthropic.com/login",
        on_submit=lambda _code: None,
        on_cancel=lambda: None,
    )

    assert _widget_text(dialog.link_button) == "https://auth.anthropic.com/login"
    assert dialog.code_entry is not None
    assert dialog.submit_button is not None
    assert dialog.cancel_button is not None


def test_claude_code_entry_submit_uses_current_entry_text() -> None:
    calls: list[str] = []
    dialog = ClaudeCodeEntryDialog(
        "https://auth.anthropic.com/login",
        on_submit=lambda code: calls.append(code),
        on_cancel=lambda: None,
    )

    dialog.code_entry.set_text("  pasted-code  ")
    dialog.submit_button.clicked()

    assert calls == ["  pasted-code  "]


def test_claude_code_entry_cancel_invokes_callback() -> None:
    calls: list[str] = []
    dialog = ClaudeCodeEntryDialog(
        "https://auth.anthropic.com/login",
        on_submit=lambda _code: None,
        on_cancel=lambda: calls.append("cancel"),
    )

    dialog.cancel_button.clicked()

    assert calls == ["cancel"]


def test_terminate_process_escalates_to_kill_after_grace_timeout() -> None:
    process = _SlowExitProcess()

    terminate_process(process, wait_timeout_secs=5.0, kill_timeout_secs=1.0)

    assert process.terminate_calls == 1
    assert process.kill_calls == 1
    assert process.wait_calls == [5.0, 1.0]
