from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import threading
import time

import api.oauth as oauth


def setup_function():
    with oauth._OAUTH_FLOWS_LOCK:
        oauth._OAUTH_FLOWS.clear()
    with oauth._OAUTH_START_LOCKS_LOCK:
        oauth._OAUTH_START_LOCKS.clear()


def teardown_function():
    with oauth._OAUTH_FLOWS_LOCK:
        oauth._OAUTH_FLOWS.clear()
    with oauth._OAUTH_START_LOCKS_LOCK:
        oauth._OAUTH_START_LOCKS.clear()


def _run_two_concurrent(fn):
    ready = threading.Barrier(2)

    def wrapped():
        ready.wait(timeout=5)
        return fn()

    with ThreadPoolExecutor(max_workers=2) as executor:
        futures = [executor.submit(wrapped), executor.submit(wrapped)]
        return [future.result(timeout=5) for future in futures]


def test_anthropic_onboarding_oauth_start_reuses_pending_flow(monkeypatch, tmp_path):
    """Repeated unauthenticated starts must not spawn unbounded pending workers."""
    spawned = []
    hermes_home = tmp_path / "hermes-home"

    monkeypatch.setattr(oauth, "_read_claude_code_credentials", lambda: None)
    monkeypatch.setattr(oauth, "_spawn_anthropic_credential_worker", lambda flow_id: spawned.append(flow_id))

    first = oauth._start_anthropic_flow(hermes_home)
    second = oauth._start_anthropic_flow(hermes_home)

    assert first["status"] == "pending"
    assert second["status"] == "pending"
    assert second["flow_id"] == first["flow_id"]
    assert spawned == [first["flow_id"]]
    with oauth._OAUTH_FLOWS_LOCK:
        pending = [flow for flow in oauth._OAUTH_FLOWS.values() if flow.get("status") == "pending"]
    assert len(pending) == 1


def test_anthropic_onboarding_oauth_start_single_flight_is_thread_safe(monkeypatch, tmp_path):
    """Concurrent unauthenticated Anthropic starts must reserve one pending worker atomically."""
    spawned = []
    hermes_home = tmp_path / "hermes-home"
    credential_reads = threading.Barrier(2)

    def no_credentials():
        credential_reads.wait(timeout=5)
        return None

    monkeypatch.setattr(oauth, "_read_claude_code_credentials", no_credentials)
    monkeypatch.setattr(oauth, "_spawn_anthropic_credential_worker", lambda flow_id: spawned.append(flow_id))

    first, second = _run_two_concurrent(lambda: oauth._start_anthropic_flow(hermes_home))

    assert first["status"] == "pending"
    assert second["status"] == "pending"
    assert second["flow_id"] == first["flow_id"]
    assert spawned == [first["flow_id"]]
    with oauth._OAUTH_FLOWS_LOCK:
        pending = [flow for flow in oauth._OAUTH_FLOWS.values() if flow.get("status") == "pending"]
    assert len(pending) == 1


def test_codex_onboarding_oauth_start_reuses_pending_flow_before_requesting_code(monkeypatch, tmp_path):
    """A second Codex start should reuse the live flow before creating a new device code or worker."""
    spawned = []
    requested = []
    hermes_home = tmp_path / "hermes-home"

    monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: Path(hermes_home))

    def fake_request_code():
        requested.append(True)
        return {"user_code": "ABCD-EFGH", "device_auth_id": "device-1", "interval": 5, "expires_in": 900}

    monkeypatch.setattr(oauth, "_request_codex_user_code", fake_request_code)
    monkeypatch.setattr(oauth, "_spawn_codex_oauth_worker", lambda flow_id: spawned.append(flow_id))

    first = oauth.start_onboarding_oauth_flow({"provider": "openai-codex"})
    second = oauth.start_onboarding_oauth_flow({"provider": "openai-codex"})

    assert first["status"] == "pending"
    assert second["flow_id"] == first["flow_id"]
    assert second["user_code"] == "ABCD-EFGH"
    assert requested == [True]
    assert spawned == [first["flow_id"]]
    with oauth._OAUTH_FLOWS_LOCK:
        pending = [flow for flow in oauth._OAUTH_FLOWS.values() if flow.get("status") == "pending"]
    assert len(pending) == 1


def test_codex_onboarding_oauth_start_does_not_hold_lock_while_requesting_code(monkeypatch, tmp_path):
    """The slow Codex device-code request must not block unrelated flow operations."""
    lock_available_during_request = []
    hermes_home = tmp_path / "hermes-home"

    monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: Path(hermes_home))
    monkeypatch.setattr(oauth, "_spawn_codex_oauth_worker", lambda flow_id: None)

    def fake_request_code():
        acquired = oauth._OAUTH_FLOWS_LOCK.acquire(blocking=False)
        lock_available_during_request.append(acquired)
        if acquired:
            oauth._OAUTH_FLOWS_LOCK.release()
        return {"user_code": "ABCD-EFGH", "device_auth_id": "device-1", "interval": 5, "expires_in": 900}

    monkeypatch.setattr(oauth, "_request_codex_user_code", fake_request_code)

    result = oauth.start_onboarding_oauth_flow({"provider": "openai-codex"})

    assert result["status"] == "pending"
    assert lock_available_during_request == [True]


def test_codex_onboarding_oauth_start_single_flight_is_thread_safe(monkeypatch, tmp_path):
    """Concurrent Codex starts must request one device code and spawn one worker."""
    spawned = []
    requested = []
    hermes_home = tmp_path / "hermes-home"

    monkeypatch.setattr(oauth, "_get_active_hermes_home", lambda: Path(hermes_home))

    def fake_request_code():
        requested.append(True)
        time.sleep(0.05)
        return {"user_code": "ABCD-EFGH", "device_auth_id": "device-1", "interval": 5, "expires_in": 900}

    monkeypatch.setattr(oauth, "_request_codex_user_code", fake_request_code)
    monkeypatch.setattr(oauth, "_spawn_codex_oauth_worker", lambda flow_id: spawned.append(flow_id))

    first, second = _run_two_concurrent(lambda: oauth.start_onboarding_oauth_flow({"provider": "openai-codex"}))

    assert first["status"] == "pending"
    assert second["status"] == "pending"
    assert second["flow_id"] == first["flow_id"]
    assert second["user_code"] == "ABCD-EFGH"
    assert requested == [True]
    assert spawned == [first["flow_id"]]
    with oauth._OAUTH_FLOWS_LOCK:
        pending = [flow for flow in oauth._OAUTH_FLOWS.values() if flow.get("status") == "pending"]
    assert len(pending) == 1
