from __future__ import annotations

import sys
from pathlib import Path

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

from account_registry import Account, AccountRef
from device_auth import DeviceAuthPrompt, DeviceAuthSession
from device_auth_operation import DeviceAuthOperation


def _account(
    slug: str,
    alias: str,
    *,
    account_id: str | None = None,
) -> Account:
    return Account(
        ref=AccountRef("codex", slug),
        alias=alias,
        account_home=Path("/tmp") / slug,
        email=f"{slug}@example.com",
        plan="plus",
        account_id=account_id or f"acct-{slug}",
    )


class _FakeStream:
    def __init__(self, chunks) -> None:
        self._chunks = list(chunks)

    def read(self, _size=-1):
        if not self._chunks:
            return ""
        return self._chunks.pop(0)


class _FakeProcess:
    def __init__(self) -> None:
        self.stdout = _FakeStream([])
        self.stderr = _FakeStream([])
        self.terminated = False
        self.killed = False
        self.wait_calls: list[float | None] = []
        self.returncode = None

    def poll(self):
        return self.returncode

    def terminate(self) -> None:
        self.terminated = True

    def wait(self, timeout=None):
        self.wait_calls.append(timeout)
        return self.returncode

    def kill(self) -> None:
        self.killed = True
        self.returncode = -9


class _FakeDeviceAuthFlow:
    def __init__(
        self,
        *,
        prompt: DeviceAuthPrompt | None = None,
        completion_result: bool = True,
        start_error: Exception | None = None,
        prompt_error: Exception | None = None,
    ) -> None:
        self.prompt = prompt
        self.completion_result = completion_result
        self.start_error = start_error
        self.prompt_error = prompt_error
        self.start_calls: list[tuple[Path, bool]] = []
        self.read_prompt_calls: list[DeviceAuthSession] = []
        self.await_completion_calls: list[DeviceAuthSession] = []
        self.commit_calls: list[Path] = []
        self.rollback_calls: list[Path] = []
        self.abort_calls: list[tuple[DeviceAuthSession, Path]] = []

    def start(self, codex_home: Path, backup_existing: bool):
        self.start_calls.append((codex_home, backup_existing))
        if self.start_error is not None:
            raise self.start_error
        return DeviceAuthSession(
            process=_FakeProcess(),
            codex_home=codex_home,
            had_backup=backup_existing,
        )

    def read_prompt(self, session: DeviceAuthSession):
        self.read_prompt_calls.append(session)
        if self.prompt_error is not None:
            raise self.prompt_error
        assert self.prompt is not None
        return self.prompt

    def await_completion(self, session: DeviceAuthSession) -> bool:
        self.await_completion_calls.append(session)
        return self.completion_result

    def commit(self, codex_home: Path) -> None:
        self.commit_calls.append(codex_home)

    def rollback(self, codex_home: Path) -> None:
        self.rollback_calls.append(codex_home)

    def abort(self, session: DeviceAuthSession, codex_home: Path) -> None:
        self.abort_calls.append((session, codex_home))


class _FakeRegistry:
    def __init__(self, accounts: list[Account], default_slug: str | None) -> None:
        self._accounts = list(accounts)
        self._default_slug = default_slug
        self.add_dir_calls: list[tuple[str, str]] = []
        self.remove_calls: list[str] = []
        self.set_default_calls: list[str] = []
        self.new_slug_calls: list[str] = []

    def list(self) -> list[Account]:
        return list(self._accounts)

    def default_slug(self) -> str | None:
        return self._default_slug

    def set_default(self, account: Account) -> None:
        self.set_default_calls.append(account.slug)
        self._default_slug = account.slug

    def new_slug(self, alias: str) -> str:
        self.new_slug_calls.append(alias)
        return alias.lower().replace(" ", "-")

    def add_dir(self, slug: str, alias: str) -> Path:
        self.add_dir_calls.append((slug, alias))
        account = Account(
            ref=AccountRef("codex", slug),
            alias=alias,
            account_home=Path("/tmp") / slug,
            email=f"{slug}@example.com",
            plan="plus",
            account_id=f"acct-{slug}",
        )
        self._accounts.append(account)
        return account.account_home

    def remove(self, slug: str) -> None:
        self.remove_calls.append(slug)
        self._accounts = [account for account in self._accounts if account.slug != slug]


def test_repair_emits_process_prompt_commit_and_success_events() -> None:
    account = _account("rafa", "Rafa")
    prompt = DeviceAuthPrompt(
        url="https://auth.openai.com/codex/device",
        code="ABCD-EFGH",
        raw_text="Visit https://auth.openai.com/codex/device and enter ABCD-EFGH",
    )
    flow = _FakeDeviceAuthFlow(prompt=prompt, completion_result=True)
    registry = _FakeRegistry([account], default_slug="rafa")

    events = list(DeviceAuthOperation(registry, flow).repair(account))

    assert [event.kind for event in events] == [
        "process_started",
        "prompt_ready",
        "commit",
        "success",
    ]
    assert events[1].prompt == prompt
    assert flow.commit_calls == [account.account_home]
    assert flow.rollback_calls == []
    assert registry.set_default_calls == ["rafa"]


def test_repair_rolls_back_and_emits_failure_when_completion_fails() -> None:
    account = _account("rafa", "Rafa")
    prompt = DeviceAuthPrompt(
        url="https://auth.openai.com/codex/device",
        code="ABCD-EFGH",
        raw_text="Visit https://auth.openai.com/codex/device and enter ABCD-EFGH",
    )
    flow = _FakeDeviceAuthFlow(prompt=prompt, completion_result=False)
    registry = _FakeRegistry([account], default_slug="rafa")

    events = list(DeviceAuthOperation(registry, flow).repair(account))

    assert [event.kind for event in events] == [
        "process_started",
        "prompt_ready",
        "rollback",
        "failure",
    ]
    assert events[-1].retry_available is True
    assert flow.commit_calls == []
    assert flow.rollback_calls == [account.account_home]
    assert registry.set_default_calls == []


def test_add_collision_removes_new_account_and_emits_collision_event() -> None:
    existing = _account("rafa", "Rafa", account_id="acct-shared")
    flow = _FakeDeviceAuthFlow(
        prompt=DeviceAuthPrompt(
            url="https://auth.openai.com/codex/device",
            code="ABCD-EFGH",
            raw_text="Visit https://auth.openai.com/codex/device and enter ABCD-EFGH",
        ),
        completion_result=True,
    )
    registry = _FakeRegistry([existing], default_slug="rafa")

    original_add_dir = registry.add_dir

    def colliding_add_dir(slug: str, alias: str) -> Path:
        codex_home = original_add_dir(slug, alias)
        registry._accounts[-1] = Account(
            ref=AccountRef("codex", slug),
            alias=alias,
            account_home=codex_home,
            email=f"{slug}@example.com",
            plan="plus",
            account_id="acct-shared",
        )
        return codex_home

    registry.add_dir = colliding_add_dir

    events = list(DeviceAuthOperation(registry, flow).add("Team Account"))

    assert [event.kind for event in events] == [
        "process_started",
        "prompt_ready",
        "collision",
    ]
    assert events[-1].collision == existing
    assert registry.remove_calls == ["team-account"]
    assert flow.commit_calls == []
    assert flow.rollback_calls == []


def test_add_completion_failure_removes_new_account_and_rolls_back() -> None:
    flow = _FakeDeviceAuthFlow(
        prompt=DeviceAuthPrompt(
            url="https://auth.openai.com/codex/device",
            code="ABCD-EFGH",
            raw_text="Visit https://auth.openai.com/codex/device and enter ABCD-EFGH",
        ),
        completion_result=False,
    )
    registry = _FakeRegistry([], default_slug=None)

    events = list(DeviceAuthOperation(registry, flow).add("Broken Add"))

    assert [event.kind for event in events] == [
        "process_started",
        "prompt_ready",
        "rollback",
        "failure",
    ]
    assert events[-1].retry_available is False
    assert registry.remove_calls == ["broken-add"]
    assert flow.commit_calls == []
    assert flow.rollback_calls == []
