from __future__ import annotations

import uuid
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from typing import Literal
from unittest.mock import patch

import pytest
from faker import Faker
from flask import Flask
from sqlalchemy.orm import Session

from controllers.openapi.auth.data import AuthData
from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, reset_auth_ctx, set_auth_ctx
from models import Account, Tenant
from services.account_service import AccountService, TenantService
from tests.test_containers_integration_tests.helpers import generate_valid_password


@pytest.fixture
def app(flask_app_with_containers: Flask) -> Flask:
    return flask_app_with_containers


@pytest.fixture
def make_account(db_session_with_containers: Session) -> Callable[..., Account]:
    """Factory that registers a real Account and gives it an owner workspace.

    System feature gates are stubbed (registration / workspace creation
    allowed) exactly like the AppDslService integration tests, so this stays a
    pure account+tenant setup helper.
    """

    # Depend on db_session_with_containers so the app context / DB session is
    # active for the real AccountService/TenantService calls below.
    assert db_session_with_containers is not None

    def _make(*, with_owner_tenant: bool = True) -> Account:
        fake = Faker()
        with patch("services.account_service.FeatureService") as mock_feature_service:
            mock_feature_service.get_system_features.return_value.is_allow_register = True
            account = AccountService.create_account(
                email=fake.email(),
                name=fake.name(),
                interface_language="en-US",
                password=generate_valid_password(fake),
            )
            if with_owner_tenant:
                TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
        return account

    return _make


def add_tenant_for_account(account: Account, *, role: str = "normal", name: str = "Second WS") -> Tenant:
    """Create an additional tenant and join ``account`` to it (real service calls)."""
    with patch("services.account_service.FeatureService") as mock_feature_service:
        mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True
        tenant = TenantService.create_tenant(name=name)
    TenantService.create_tenant_member(tenant, account, role=role)
    return tenant


def auth_for(
    account: Account,
    *,
    app_model: object | None = None,
    token_id: uuid.UUID | None = None,
    caller_kind: Literal["account", "end_user"] | None = None,
) -> AuthData:
    """Build an AuthData for ``account`` (and optionally an app context).

    ``token_id`` is needed by the self-revoke endpoint, and ``caller_kind`` by
    any handler calling ``require_app_context`` (e.g. file upload / task stop).
    """
    return AuthData(
        token_type=TokenType.OAUTH_ACCOUNT,
        account_id=uuid.UUID(str(account.id)),
        token_hash="integration-test",
        token_id=token_id,
        scopes=frozenset({Scope.FULL}),
        caller=account,
        caller_kind=caller_kind,
        app=app_model,  # type: ignore[arg-type]
    )


@contextmanager
def account_auth_context(
    account: Account,
    *,
    token_id: uuid.UUID,
    client_id: str = "integration-cli",
) -> Iterator[AuthContext]:
    """Publish an account ``AuthContext`` for handlers that read ``get_auth_ctx()``.

    The auth pipeline normally sets this ContextVar; the integration suite
    bypasses the pipeline via ``inspect.unwrap``, so endpoints that resolve the
    caller through ``get_auth_ctx()`` (the ``/account/sessions*`` family) need it
    set explicitly. Resets on exit so the worker thread can't leak identity.
    """
    ctx = AuthContext(
        subject_type=SubjectType.ACCOUNT,
        subject_email=account.email,
        subject_issuer=None,
        account_id=uuid.UUID(str(account.id)),
        client_id=client_id,
        scopes=frozenset({Scope.FULL}),
        token_id=token_id,
        token_type=TokenType.OAUTH_ACCOUNT,
        expires_at=None,
        token_hash="integration-test",
    )
    reset_token = set_auth_ctx(ctx)
    try:
        yield ctx
    finally:
        reset_auth_ctx(reset_token)
