"""Tests for desktop auth allowlist, one-time flows, and local-claim safety."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from uuid import uuid4

import pytest

from src.core.exceptions import UnauthorizedError, ValidationError
from src.core.security.jwt import (
    create_desktop_session_token,
    decode_desktop_session_token,
    get_user_id_from_token,
)
from src.domain.desktop_auth.models import DesktopAuthStatus
from src.domain.desktop_auth.service import DesktopAuthService
from src.domain.desktop_auth.handoff_allowlist import normalize_handoff_next
from src.domain.devices.claim_service import DeviceClaimService
from src.domain.devices.models import DeviceStatus
from src.domain.devices.service import DeviceService


def test_handoff_allowlist_accepts_billing_and_settings():
    assert normalize_handoff_next("/billing") == "/billing"
    assert normalize_handoff_next("/billing/success") == "/billing/success"
    assert normalize_handoff_next("settings/security") == "/settings/security"
    assert normalize_handoff_next("/projects/abc/domains") == "/projects/abc/domains"
    assert normalize_handoff_next("/workspaces/ws1/projects") == "/workspaces/ws1/projects"


def test_handoff_allowlist_rejects_open_redirects():
    with pytest.raises(ValueError):
        normalize_handoff_next("https://evil.example/phish")
    with pytest.raises(ValueError):
        normalize_handoff_next("/login")
    with pytest.raises(ValueError):
        normalize_handoff_next("/../etc/passwd")
    with pytest.raises(ValueError):
        normalize_handoff_next("/admin")


def test_desktop_session_jwt_is_not_a_web_access_token():
    user_id = uuid4()
    token = create_desktop_session_token(user_id)
    assert decode_desktop_session_token(token) == user_id
    assert get_user_id_from_token(token) is None


@pytest.mark.asyncio
async def test_desktop_exchange_does_not_mint_second_session_after_race():
    row = SimpleNamespace(
        id=uuid4(),
        login_code="dl_test",
        user_id=uuid4(),
        status=DesktopAuthStatus.completed.value,
        expires_at=datetime.now(timezone.utc) + timedelta(minutes=5),
    )

    class RequestsRepo:
        async def get_by_login_code(self, login_code: str):
            return row

        async def mark_exchanged_if_completed(self, request_id):
            return False

    service = DesktopAuthService(
        requests=RequestsRepo(),
        handoffs=SimpleNamespace(),
        users=SimpleNamespace(),
        sessions=SimpleNamespace(),
        app_url="https://app.example.com",
    )

    row.status = DesktopAuthStatus.exchanged.value
    result = await service.exchange(login_code="dl_test")
    assert result == {"status": "exchanged"}


@pytest.mark.asyncio
async def test_browser_handoff_rejects_second_redemption():
    user_id = uuid4()
    row = SimpleNamespace(
        id=uuid4(),
        user_id=user_id,
        next_path="/billing",
        expires_at=datetime.now(timezone.utc) + timedelta(seconds=30),
        used_at=None,
    )
    user = SimpleNamespace(id=user_id)

    class HandoffsRepo:
        async def get_by_token_hash(self, token_hash: str):
            return row

        async def mark_used_if_unused(self, handoff_id, *, now):
            return False

    class UsersRepo:
        async def get_by_id(self, requested_user_id):
            return user if requested_user_id == user_id else None

    service = DesktopAuthService(
        requests=SimpleNamespace(),
        handoffs=HandoffsRepo(),
        users=UsersRepo(),
        sessions=SimpleNamespace(),
        app_url="https://app.example.com",
    )

    with pytest.raises(UnauthorizedError, match="already been used"):
        await service.consume_handoff(token="dho_test")


@pytest.mark.asyncio
async def test_pair_poll_does_not_return_stale_secret_when_other_poll_wins():
    device_id = uuid4()
    user_id = uuid4()
    active_device = SimpleNamespace(
        id=device_id,
        user_id=user_id,
        status=DeviceStatus.active.value,
        device_secret_hash=None,
        pairing_expires_at=None,
    )
    refreshed_device = SimpleNamespace(
        id=device_id,
        user_id=user_id,
        status=DeviceStatus.active.value,
        device_secret_hash="hashed",
        pairing_expires_at=None,
    )

    class DevicesRepo:
        async def get_by_device_code(self, device_code: str):
            return active_device

        async def deliver_secret_if_missing(self, device_id, device_secret_hash: str):
            return False

        async def get_by_id(self, device_id):
            return refreshed_device

    service = DeviceService(
        devices=DevicesRepo(),
        events=SimpleNamespace(),
        app_url="https://app.example.com",
    )

    result = await service.poll_pairing("dc_test")
    assert result == {
        "status": "already_paired",
        "device_id": device_id,
        "user_id": user_id,
    }


@pytest.mark.asyncio
async def test_local_claim_fails_closed_before_run_is_marked_running():
    run_id = uuid4()
    project_id = uuid4()
    device_id = uuid4()
    workspace_id = uuid4()

    run = SimpleNamespace(
        id=run_id,
        project_id=project_id,
        device_id=device_id,
        execution_target="local",
        plan={},
    )
    device = SimpleNamespace(id=device_id)
    project = SimpleNamespace(
        workspace_id=workspace_id,
        repo=SimpleNamespace(repo_full_name="owner/repo", preview_branch="preview"),
        type="react_app",
        project_ref="proj_ref",
    )

    class RunsRepo:
        def __init__(self):
            self.claim_calls = 0

        async def get_by_id(self, requested_run_id):
            return run if requested_run_id == run_id else None

        async def try_claim_for_device(self, requested_run_id, requested_device_id):
            self.claim_calls += 1
            return run

    runs_repo = RunsRepo()

    class ProjectsRepo:
        async def get_with_repo(self, requested_project_id):
            return project if requested_project_id == project_id else None

    class DevicesRepo:
        async def set_last_workspace(self, device_obj, workspace):
            return device_obj

    service = DeviceClaimService(
        runs=runs_repo,
        devices=DevicesRepo(),
        projects=ProjectsRepo(),
        db_factory=None,
    )

    with pytest.raises(ValidationError, match="blocked by policy"):
        await service.claim(device=device, run_id=run_id)

    assert runs_repo.claim_calls == 0
