from __future__ import annotations

import sys
from pathlib import Path
from types import SimpleNamespace

import pytest

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

from account_registry import (
    Account,
    AccountRef,
    AccountRegistryKind,
    AuthorityBinding,
    AuthorityMode,
)
from device_auth import DeviceAuthPrompt, DeviceAuthSession
from device_auth_operation import FlowEvent
from health_client import AccountSnapshot, HealthStatus
from provider_services import (
    AuthorityLifecycleAdapter,
    CodexHealthAdapter,
    CodexLifecycleAdapter,
    ProviderServiceMap,
    ProviderServices,
)


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


class _FakeLifecycleOperation:
    def __init__(self, add_events: list[FlowEvent], repair_events: list[FlowEvent]) -> None:
        self._add_events = list(add_events)
        self._repair_events = list(repair_events)
        self.add_calls: list[str] = []
        self.repair_calls: list[Account] = []

    def add(self, alias: str, *, slug: str | None = None):
        del slug
        self.add_calls.append(alias)
        yield from self._add_events

    def repair(self, account: Account):
        self.repair_calls.append(account)
        yield from self._repair_events


class _FakeHealthClient:
    def __init__(self, snapshot: AccountSnapshot) -> None:
        self.snapshot = snapshot
        self.fetch_calls: list[tuple[Path, float]] = []
        self.warmup_flags: list[bool] = []

    def fetch(
        self,
        codex_home: Path,
        timeout_secs: float = 10.0,
        *,
        allow_warmup: bool = False,
    ) -> AccountSnapshot:
        self.fetch_calls.append((codex_home, timeout_secs))
        self.warmup_flags.append(allow_warmup)
        return self.snapshot


def test_codex_lifecycle_adapter_translates_device_auth_events_and_ignores_login_hint() -> None:
    existing = _account("codex", "existing", "Existing")
    created = _account("codex", "new", "New Account")
    prompt = DeviceAuthPrompt(
        url="https://auth.openai.com/device",
        code="ABCD-EFGH",
        raw_text="Visit the URL and enter the code",
    )
    session = DeviceAuthSession(process=object(), codex_home=created.account_home, had_backup=False)
    failure = RuntimeError("boom")
    add_event = FlowEvent(
        kind="prompt_ready",
        operation="add",
        account=created,
        alias="New Account",
        session=session,
        prompt=prompt,
        existing_accounts=[("Existing", "existing@example.com")],
        collision=existing,
        retry_available=False,
        message="show prompt",
        error=failure,
    )
    success_event = FlowEvent(
        kind="success",
        operation="repair",
        account=existing,
        message="done",
    )
    operation = _FakeLifecycleOperation([add_event], [success_event])
    adapter = CodexLifecycleAdapter(operation)

    translated_add = list(adapter.add("New Account", login_hint="ignored@example.com"))
    translated_reauth = list(adapter.reauthenticate(existing))

    assert operation.add_calls == ["New Account"]
    assert operation.repair_calls == [existing]
    assert len(translated_add) == 1
    assert translated_add[0].kind == add_event.kind
    assert translated_add[0].account == add_event.account
    assert translated_add[0].message == add_event.message
    assert translated_add[0].error == add_event.error
    assert translated_add[0].prompt == add_event.prompt
    assert translated_add[0].session == add_event.session
    assert translated_add[0].collision == add_event.collision
    assert translated_add[0].existing_accounts == add_event.existing_accounts
    assert translated_reauth[0].kind == "success"
    assert translated_reauth[0].account == existing


def test_codex_health_adapter_fetches_using_account_home_and_timeout() -> None:
    account = _account("codex", "rafa", "Rafa")
    snapshot = AccountSnapshot(HealthStatus.OK, 10, 20)
    client = _FakeHealthClient(snapshot)
    adapter = CodexHealthAdapter(client)

    returned = adapter.fetch(account, timeout_secs=2.5)

    assert returned == snapshot
    assert client.fetch_calls == [(account.account_home, 2.5)]
    assert client.warmup_flags == [True]


def test_provider_service_map_dispatches_same_slug_accounts_by_tool() -> None:
    codex_account = _account("codex", "shared", "Codex Shared")
    claude_account = _account("claude", "shared", "Claude Shared")
    codex_services = ProviderServices(
        tool=AccountRegistryKind.CODEX,
        registry=object(),
        lifecycle=object(),
        health=object(),
    )
    claude_services = ProviderServices(
        tool=AccountRegistryKind.CLAUDE,
        registry=object(),
        lifecycle=object(),
        health=object(),
    )
    service_map = ProviderServiceMap([codex_services, claude_services])

    assert service_map.for_tool(AccountRegistryKind.CODEX) is codex_services
    assert service_map.for_tool("CODEX") is codex_services
    assert service_map.for_tool("claude") is claude_services
    assert service_map.for_account(codex_account) is codex_services
    assert service_map.for_account(claude_account) is claude_services


def test_provider_service_map_rejects_duplicate_and_missing_providers() -> None:
    codex_services = ProviderServices(
        tool=AccountRegistryKind.CODEX,
        registry=object(),
        lifecycle=object(),
        health=object(),
    )

    with pytest.raises(ValueError, match="duplicate"):
        ProviderServiceMap([codex_services, codex_services])

    service_map = ProviderServiceMap([codex_services])

    with pytest.raises(LookupError, match="claude"):
        service_map.for_tool("claude")

    with pytest.raises(ValueError, match="unknown"):
        service_map.for_tool("unknown")


class _NativeLifecycle:
    def __init__(self) -> None:
        self.add_calls: list[tuple[str, str | None, str | None]] = []
        self.reauth_calls: list[Account] = []
        self.code_calls: list[tuple[object, str]] = []

    def add(self, alias: str, *, login_hint: str | None = None, slug: str | None = None):
        self.add_calls.append((alias, login_hint, slug))
        yield type("Event", (), {"kind": "success"})()

    def reauthenticate(self, account: Account):
        self.reauth_calls.append(account)
        yield type("Event", (), {"kind": "success"})()

    def submit_code(self, session, code: str) -> None:
        self.code_calls.append((session, code))


@pytest.mark.parametrize("provider,device_auth", [("codex", True), ("claude", False)])
def test_authority_lifecycle_repair_uses_local_service_identity_not_native_credentials(
    tmp_path: Path, provider: str, device_auth: bool
) -> None:
    grant = (tmp_path / "grant.key").resolve()
    grant.write_text("synthetic", encoding="utf-8")
    grant.chmod(0o600)
    binding = AuthorityBinding(
        mode=AuthorityMode.SUBROUTER,
        authority_name="fixture-authority",
        route_id=f"route-{provider}",
        provider=provider,
        proxy_grant_ref=grant,
    )
    account = Account(
        ref=AccountRef(provider, f"fixture-{provider}"),
        alias="Fixture",
        account_home=tmp_path / provider,
        email=None,
        plan=None,
        account_id=None,
        authority_binding=binding,
    )
    native = _NativeLifecycle()
    calls: list[list[str]] = []

    def runner(argv, *, check=False):
        assert check is False
        calls.append(list(argv))
        return SimpleNamespace(returncode=0)

    adapter = AuthorityLifecycleAdapter(
        native,
        binary=Path("/fixture/subrouter"),
        state_dir=Path("/fixture/state"),
        command_runner=runner,
    )
    events = list(adapter.reauthenticate(account))

    assert [event.kind for event in events] == ["started", "success"]
    assert native.reauth_calls == []
    assert len(calls) == 1
    command = calls[0]
    assert command[:5] == [
        "deck-sudo",
        "-u",
        "overdeck-subrouter",
        "--",
        "/fixture/subrouter",
    ]
    assert command[5:10] == [
        "authority-account",
        "repair",
        "--state-dir",
        "/fixture/state",
        "--provider",
    ]
    assert provider in command
    assert account.slug in command
    assert ("--device-auth" in command) is device_auth
    assert not any("token" in item.lower() or "credential" in item.lower() for item in command)


def test_authority_lifecycle_add_stays_native_and_failed_repair_is_bounded(tmp_path: Path) -> None:
    native = _NativeLifecycle()
    adapter = AuthorityLifecycleAdapter(
        native,
        binary=Path("/fixture/subrouter"),
        state_dir=Path("/fixture/state"),
        command_runner=lambda *_args, **_kwargs: SimpleNamespace(returncode=23),
    )
    assert [event.kind for event in adapter.add("Fresh", login_hint="fresh@example.invalid", slug="fresh")] == ["success"]
    assert native.add_calls == [("Fresh", "fresh@example.invalid", "fresh")]

    grant = (tmp_path / "grant.key").resolve()
    grant.write_text("synthetic", encoding="utf-8")
    grant.chmod(0o600)
    account = Account(
        ref=AccountRef("codex", "fixture"),
        alias="Fixture",
        account_home=tmp_path / "home",
        email=None,
        plan=None,
        account_id=None,
        authority_binding=AuthorityBinding(
            mode=AuthorityMode.SUBROUTER_DARK,
            authority_name="fixture-authority",
            route_id="route-fixture",
            provider="codex",
            proxy_grant_ref=grant,
        ),
    )
    events = list(adapter.reauthenticate(account))
    assert [event.kind for event in events] == ["started", "failure"]
    assert events[-1].message == "Gateway repair failed"
    assert native.reauth_calls == []
