import os

os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]
# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
# @LAYER API
# @RELATION DEPENDS_ON -> AssistantApi
# @INVARIANT  Security-sensitive flows fail closed for unauthorized actors.
import asyncio
from datetime import UTC, datetime, timedelta
import os
import pytest
from types import SimpleNamespace

from fastapi import HTTPException

# Force isolated sqlite databases for test module before dependencies import.
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz.db")
os.environ.setdefault(
    "TASKS_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz_tasks.db"
)
os.environ.setdefault(
    "AUTH_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz_auth.db"
)
from src.api.routes import assistant as assistant_module
from src.models.assistant import (
    AssistantAuditRecord,
    AssistantConfirmationRecord,
    AssistantMessageRecord,
)


# #region _run_async [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Execute async endpoint handler in synchronous test context.
# @PRE  coroutine is awaitable endpoint invocation.
# @POST  Returns coroutine result or raises propagated exception.
def _run_async(coroutine):
    return asyncio.run(coroutine)
# #endregion _run_async
# #region _FakeTask [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Lightweight task model used for assistant authz tests.
# @PRE  task_id is non-empty string.
# @POST  Returns task with provided id, status, and user_id accessible as attributes.
class _FakeTask:
    def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
        self.id = task_id
        self.status = status
        self.user_id = user_id
# #endregion _FakeTask
# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().
# #region _FakeTaskManager [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF In-memory task manager double that records assistant-created tasks deterministically.
# @INVARIANT  Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.
class _FakeTaskManager:
    def __init__(self):
        self._created = []
    async def create_task(self, plugin_id, params, user_id=None):
        task_id = f"task-{len(self._created) + 1}"
        task = _FakeTask(task_id=task_id, status="RUNNING", user_id=user_id)
        self._created.append((plugin_id, params, user_id, task))
        return task
    def get_task(self, task_id):
        for _, _, _, task in self._created:
            if task.id == task_id:
                return task
        return None
    def get_tasks(self, limit=20, offset=0):
        return [x[3] for x in self._created][offset : offset + limit]
    def get_all_tasks(self):
        raise NotImplementedError(
            "get_all_tasks not implemented in authz FakeTaskManager"
        )
# #endregion _FakeTaskManager
# @CONTRACT: Partial ConfigManager stub for authz tests. Missing: get_config().
# #region _FakeConfigManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Provide deterministic environment aliases required by intent parsing.
# @PRE  No external config or DB state is required.
# @POST  get_environments() returns two deterministic SimpleNamespace stubs with id/name.
# @INVARIANT  get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.
class _FakeConfigManager:
    def get_environments(self):
        return [
            SimpleNamespace(id="dev", name="Development"),
            SimpleNamespace(id="prod", name="Production"),
        ]
    def get_config(self):
        raise NotImplementedError(
            "get_config not implemented in authz fake — add if route under test requires it"
        )
# #endregion _FakeConfigManager
# #region _admin_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build admin principal fixture.
# @PRE  Test requires privileged principal for risky operations.
# @POST  Returns admin-like user stub with Admin role.
def _admin_user():
    role = SimpleNamespace(name="Admin", permissions=[])
    return SimpleNamespace(id="u-admin", username="admin", roles=[role])
# #endregion _admin_user
# #region _other_admin_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build second admin principal fixture for ownership tests.
# @PRE  Ownership mismatch scenario needs distinct authenticated actor.
# @POST  Returns alternate admin-like user stub.
def _other_admin_user():
    role = SimpleNamespace(name="Admin", permissions=[])
    return SimpleNamespace(id="u-admin-2", username="admin2", roles=[role])
# #endregion _other_admin_user
# #region _limited_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build limited principal without required assistant execution privileges.
# @PRE  Permission denial scenario needs non-admin actor.
# @POST  Returns restricted user stub.
def _limited_user():
    role = SimpleNamespace(name="Operator", permissions=[])
    return SimpleNamespace(id="u-limited", username="limited", roles=[role])
# #endregion _limited_user
# #region _FakeQuery [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Minimal chainable query object for fake DB interactions.
# @INVARIANT  filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.
class _FakeQuery:
    def __init__(self, rows):
        self._rows = list(rows)
    def filter(self, *args, **kwargs):
        # @INVARIANT  filter() is predicate-blind; returns all records regardless of user_id scope
        return self
    def order_by(self, *args, **kwargs):
        return self
    def first(self):
        return self._rows[0] if self._rows else None
    def all(self):
        return list(self._rows)
    def limit(self, limit):
        self._rows = self._rows[:limit]
        return self
    def offset(self, offset):
        self._rows = self._rows[offset:]
        return self
    def count(self):
        return len(self._rows)
# #endregion _FakeQuery
# #region _FakeDb [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.
# @INVARIANT  query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.
class _FakeDb:
    def __init__(self):
        self._messages = []
        self._confirmations = []
        self._audit = []
    def add(self, row):
        table = getattr(row, "__tablename__", "")
        if table == "assistant_messages":
            self._messages.append(row)
        elif table == "assistant_confirmations":
            self._confirmations.append(row)
        elif table == "assistant_audit":
            self._audit.append(row)
    def merge(self, row):
        if getattr(row, "__tablename__", "") != "assistant_confirmations":
            self.add(row)
            return row
        for i, existing in enumerate(self._confirmations):
            if getattr(existing, "id", None) == getattr(row, "id", None):
                self._confirmations[i] = row
                return row
        self._confirmations.append(row)
        return row
    def query(self, model):
        if model is AssistantMessageRecord:
            return _FakeQuery(self._messages)
        if model is AssistantConfirmationRecord:
            return _FakeQuery(self._confirmations)
        if model is AssistantAuditRecord:
            return _FakeQuery(self._audit)
        return _FakeQuery([])
    def commit(self):
        return None
    def rollback(self):
        return None
# #endregion _FakeDb
# #region _clear_assistant_state [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Reset assistant process-local state between test cases.
# @PRE  Assistant globals may contain state from prior tests.
# @POST  Assistant in-memory state dictionaries are cleared.
def _clear_assistant_state():
    assistant_module.CONVERSATIONS.clear()
    assistant_module.USER_ACTIVE_CONVERSATION.clear()
    assistant_module.CONFIRMATIONS.clear()
    assistant_module.ASSISTANT_AUDIT.clear()
# #endregion _clear_assistant_state
# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.
# @PRE  Confirmation token is created by first admin actor.
# @POST  Second actor receives 403 on confirm operation.
def test_confirmation_owner_mismatch_returns_403():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()
    create = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на prod для дашборда 18"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert create.state == "needs_confirmation"
    with pytest.raises(HTTPException) as exc:
        _run_async(
            assistant_module.confirm_operation(
                confirmation_id=create.confirmation_id,
                current_user=_other_admin_user(),
                task_manager=task_manager,
                config_manager=_FakeConfigManager(),
                db=db,
            )
        )
    assert exc.value.status_code == 403
# #endregion test_confirmation_owner_mismatch_returns_403
# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Expired confirmation token should be rejected and not create task.
# @PRE  Confirmation token exists and is manually expired before confirm request.
# @POST  Confirm endpoint raises 400 and no task is created.
def test_expired_confirmation_cannot_be_confirmed():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()
    create = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на prod для дашборда 19"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (
        datetime.now(UTC) - timedelta(minutes=1)
    )
    with pytest.raises(HTTPException) as exc:
        _run_async(
            assistant_module.confirm_operation(
                confirmation_id=create.confirmation_id,
                current_user=_admin_user(),
                task_manager=task_manager,
                config_manager=_FakeConfigManager(),
                db=db,
            )
        )
    assert exc.value.status_code == 400
    assert task_manager.get_tasks(limit=10, offset=0) == []
# #endregion test_expired_confirmation_cannot_be_confirmed
# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Limited user should receive denied state for privileged operation.
# @PRE  Restricted user attempts dangerous deploy command.
# @POST  Assistant returns denied state and does not execute operation.
def test_limited_user_cannot_launch_restricted_operation():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="задеплой дашборд 88 в production"
            ),
            current_user=_limited_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )
    assert response.state == "denied"
# #endregion test_limited_user_cannot_launch_restricted_operation
# #endregion TestAssistantAuthz
