# [DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
# @TIER: STANDARD
# @SEMANTICS: tests, assistant, api, confirmation, status
# @PURPOSE: Validate assistant API endpoint logic via direct async handler invocation.
# @LAYER: UI (API Tests)
# @RELATION: DEPENDS_ON -> backend.src.api.routes.assistant
# @INVARIANT: Every test clears assistant in-memory state before execution.

import os
import asyncio
from types import SimpleNamespace
from datetime import datetime, timedelta
import pytest

# Force isolated sqlite databases for test module before dependencies import.
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_api.db")
os.environ.setdefault("TASKS_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_tasks.db")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_auth.db")

from src.api.routes import assistant as assistant_module
from src.models.assistant import (
    AssistantAuditRecord,
    AssistantConfirmationRecord,
    AssistantMessageRecord,
)


# [DEF:_run_async:Function]
# @TIER: TRIVIAL
# @PURPOSE: 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)


# [/DEF:_run_async:Function]
# [DEF:_FakeTask:Class]
# @TIER: TRIVIAL
# @PURPOSE: Lightweight task stub used by assistant API tests.
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


# [/DEF:_FakeTask:Class]
# [DEF:_FakeTaskManager:Class]
# @TIER: TRIVIAL
# @PURPOSE: Minimal async-compatible TaskManager fixture for deterministic test flows.
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:_FakeTaskManager:Class]
# [DEF:_FakeConfigManager:Class]
# @TIER: TRIVIAL
# @PURPOSE: Environment config fixture with dev/prod aliases for parser tests.
class _FakeConfigManager:
    def get_environments(self):
        return [
            SimpleNamespace(id="dev", name="Development", url="http://dev", credentials_id="dev", username="fakeuser", password="fakepassword"),
            SimpleNamespace(id="prod", name="Production", url="http://prod", credentials_id="prod", username="fakeuser", password="fakepassword"),
        ]

    def get_config(self):
        return SimpleNamespace(
            settings=SimpleNamespace(migration_sync_cron="0 0 * * *"),
            environments=self.get_environments()
        )
# [/DEF:_FakeConfigManager:Class]
# [DEF:_admin_user:Function]
# @TIER: TRIVIAL
# @PURPOSE: Build admin principal fixture.
# @PRE: Test harness requires authenticated admin-like principal object.
# @POST: Returns user stub with Admin role.
def _admin_user():
    role = SimpleNamespace(name="Admin", permissions=[])
    return SimpleNamespace(id="u-admin", username="admin", roles=[role])


# [/DEF:_admin_user:Function]
# [DEF:_limited_user:Function]
# @TIER: TRIVIAL
# @PURPOSE: Build non-admin principal fixture.
# @PRE: Test harness requires restricted principal for deny scenarios.
# @POST: Returns user stub without admin privileges.
def _limited_user():
    role = SimpleNamespace(name="Operator", permissions=[])
    return SimpleNamespace(id="u-limited", username="limited", roles=[role])


# [/DEF:_limited_user:Function]
# [DEF:_FakeQuery:Class]
# @TIER: TRIVIAL
# @PURPOSE: Minimal chainable query object for fake SQLAlchemy-like DB behavior in tests.
class _FakeQuery:
    def __init__(self, rows):
        self._rows = list(rows)

    def filter(self, *args, **kwargs):
        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 count(self):
        return len(self._rows)

    def offset(self, offset):
        self._rows = self._rows[offset:]
        return self

    def limit(self, limit):
        self._rows = self._rows[:limit]
        return self


# [/DEF:_FakeQuery:Class]
# [DEF:_FakeDb:Class]
# @TIER: TRIVIAL
# @PURPOSE: In-memory fake database implementing subset of Session interface used by assistant routes.
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)
            return
        if table == "assistant_confirmations":
            self._confirmations.append(row)
            return
        if table == "assistant_audit":
            self._audit.append(row)

    def merge(self, row):
        table = getattr(row, "__tablename__", "")
        if table != "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


# [/DEF:_FakeDb:Class]
# [DEF:_clear_assistant_state:Function]
# @TIER: TRIVIAL
# @PURPOSE: Reset in-memory assistant registries for isolation between tests.
# @PRE: Assistant module globals may contain residues from previous test runs.
# @POST: In-memory conversation/confirmation/audit dictionaries are empty.
def _clear_assistant_state():
    assistant_module.CONVERSATIONS.clear()
    assistant_module.USER_ACTIVE_CONVERSATION.clear()
    assistant_module.CONFIRMATIONS.clear()
    assistant_module.ASSISTANT_AUDIT.clear()


# [/DEF:_clear_assistant_state:Function]
# [DEF:test_unknown_command_returns_needs_clarification:Function]
# @PURPOSE: Unknown command should return clarification state and unknown intent.
# @PRE: Fake dependencies provide admin user and deterministic task/config/db services.
# @POST: Response state is needs_clarification and no execution side-effect occurs.
def test_unknown_command_returns_needs_clarification():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(message="сделай что-нибудь"),
            current_user=_admin_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )
    assert response.state == "needs_clarification"
    assert response.intent["domain"] == "unknown"


# [/DEF:test_unknown_command_returns_needs_clarification:Function]


# [DEF:test_capabilities_question_returns_successful_help:Function]
# @PURPOSE: Capability query should return deterministic help response, not clarification.
# @PRE: User sends natural-language "what can you do" style query.
# @POST: Response is successful and includes capabilities summary.
def test_capabilities_question_returns_successful_help():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(message="Что ты умеешь?"),
            current_user=_admin_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )
    assert response.state == "success"
    assert "Вот что я могу сделать" in response.text
    assert "Миграции" in response.text or "Git" in response.text


# [/DEF:test_capabilities_question_returns_successful_help:Function]
# [DEF:test_non_admin_command_returns_denied:Function]
# @PURPOSE: Non-admin user must receive denied state for privileged command.
# @PRE: Limited principal executes privileged git branch command.
# @POST: Response state is denied and operation is not executed.
def test_non_admin_command_returns_denied():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="создай ветку feature/test для дашборда 12"
            ),
            current_user=_limited_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )
    assert response.state == "denied"


# [/DEF:test_non_admin_command_returns_denied:Function]
# [DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
# @PURPOSE: Migration to prod must require confirmation and then start task after explicit confirm.
# @PRE: Admin principal submits dangerous migration command.
# @POST: Confirmation endpoint transitions flow to started state with task id.
def test_migration_to_prod_requires_confirmation_and_can_be_confirmed():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()

    first = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на prod для дашборда 12"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert first.state == "needs_confirmation"
    assert first.confirmation_id

    second = _run_async(
        assistant_module.confirm_operation(
            confirmation_id=first.confirmation_id,
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert second.state == "started"
    assert second.task_id.startswith("task-")


# [/DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
# [DEF:test_status_query_returns_task_status:Function]
# @PURPOSE: Task status command must surface current status text for existing task id.
# @PRE: At least one task exists after confirmed operation.
# @POST: Status query returns started/success and includes referenced task id.
def test_status_query_returns_task_status():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()

    start = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на prod для дашборда 10"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    confirm = _run_async(
        assistant_module.confirm_operation(
            confirmation_id=start.confirmation_id,
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    task_id = confirm.task_id

    status_resp = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message=f"проверь статус задачи {task_id}"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert status_resp.state in {"started", "success"}
    assert task_id in status_resp.text


# [/DEF:test_status_query_returns_task_status:Function]
# [DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
# @PURPOSE: Status command without explicit task_id should resolve to latest task for current user.
# @PRE: User has at least one created task in task manager history.
# @POST: Response references latest task status without explicit task id in command.
def test_status_query_without_task_id_returns_latest_user_task():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()

    start = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на prod для дашборда 33"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    _run_async(
        assistant_module.confirm_operation(
            confirmation_id=start.confirmation_id,
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )

    status_resp = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="покажи статус последней задачи"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert status_resp.state in {"started", "success"}
    assert "Последняя задача:" in status_resp.text


# [/DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
# [DEF:test_llm_validation_with_dashboard_ref_requires_confirmation:Function]
# @PURPOSE: LLM validation with dashboard_ref should now require confirmation before dispatch.
# @PRE: User sends natural-language validation request with dashboard name (not numeric id).
# @POST: Response state is needs_confirmation since all state-changing operations are now gated.
def test_llm_validation_with_dashboard_ref_requires_confirmation():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="Я хочу сделать валидацию дашборда test1"
            ),
            current_user=_admin_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )

    assert response.state == "needs_confirmation"
    assert response.confirmation_id is not None
    action_types = {a.type for a in response.actions}
    assert "confirm" in action_types
    assert "cancel" in action_types


# [/DEF:test_llm_validation_with_dashboard_ref_requires_confirmation:Function]


# [DEF:test_list_conversations_groups_by_conversation_and_marks_archived:Function]
# @PURPOSE: Conversations endpoint must group messages and compute archived marker by inactivity threshold.
# @PRE: Fake DB contains two conversations with different update timestamps.
# @POST: Response includes both conversations with archived flag set for stale one.
def test_list_conversations_groups_by_conversation_and_marks_archived():
    _clear_assistant_state()
    db = _FakeDb()
    now = datetime.utcnow()

    db.add(
        AssistantMessageRecord(
            id="m-1",
            user_id="u-admin",
            conversation_id="conv-active",
            role="user",
            text="active chat",
            created_at=now,
        )
    )
    db.add(
        AssistantMessageRecord(
            id="m-2",
            user_id="u-admin",
            conversation_id="conv-old",
            role="user",
            text="old chat",
            created_at=now - timedelta(days=32), # Hardcoded threshold+2
        )
    )

    result = _run_async(
        assistant_module.list_conversations(
            page=1,
            page_size=20,
            include_archived=True,
            search=None,
            current_user=_admin_user(),
            db=db,
        )
    )

    assert result["total"] == 2
    by_id = {item["conversation_id"]: item for item in result["items"]}
    assert by_id["conv-active"]["archived"] is False
    assert by_id["conv-old"]["archived"] is True


# [/DEF:test_list_conversations_groups_by_conversation_and_marks_archived:Function]


# [DEF:test_history_from_latest_returns_recent_page_first:Function]
# @PURPOSE: History endpoint from_latest mode must return newest page while preserving chronological order in chunk.
# @PRE: Conversation has more messages than single page size.
# @POST: First page returns latest messages and has_next indicates older pages exist.
def test_history_from_latest_returns_recent_page_first():
    _clear_assistant_state()
    db = _FakeDb()
    base_time = datetime.utcnow() - timedelta(minutes=10)
    conv_id = "conv-paginated"
    for i in range(4, -1, -1):
        db.add(
            AssistantMessageRecord(
                id=f"msg-{i}",
                user_id="u-admin",
                conversation_id=conv_id,
                role="user" if i % 2 == 0 else "assistant",
                text=f"message-{i}",
                created_at=base_time + timedelta(minutes=i),
            )
        )

    result = _run_async(
        assistant_module.get_history(
            page=1,
            page_size=2,
            conversation_id=conv_id,
            from_latest=True,
            current_user=_admin_user(),
            db=db,
        )
    )

    assert result["from_latest"] is True
    assert result["has_next"] is True
    # Chunk is chronological while representing latest page.
    assert [item["text"] for item in result["items"]] == ["message-3", "message-4"]


# [/DEF:test_history_from_latest_returns_recent_page_first:Function]


# [DEF:test_list_conversations_archived_only_filters_active:Function]
# @PURPOSE: archived_only mode must return only archived conversations.
# @PRE: Dataset includes one active and one archived conversation.
# @POST: Only archived conversation remains in response payload.
def test_list_conversations_archived_only_filters_active():
    _clear_assistant_state()
    db = _FakeDb()
    now = datetime.utcnow()
    db.add(
        AssistantMessageRecord(
            id="m-active",
            user_id="u-admin",
            conversation_id="conv-active-2",
            role="user",
            text="active",
            created_at=now,
        )
    )
    db.add(
        AssistantMessageRecord(
            id="m-archived",
            user_id="u-admin",
            conversation_id="conv-archived-2",
            role="user",
            text="archived",
            created_at=now - timedelta(days=33), # Hardcoded threshold+3
        )
    )

    result = _run_async(
        assistant_module.list_conversations(
            page=1,
            page_size=20,
            include_archived=True,
            archived_only=True,
            search=None,
            current_user=_admin_user(),
            db=db,
        )
    )

    assert result["total"] == 1
    assert result["items"][0]["conversation_id"] == "conv-archived-2"
    assert result["items"][0]["archived"] is True


# [/DEF:test_list_conversations_archived_only_filters_active:Function]


# [DEF:test_guarded_operation_always_requires_confirmation:Function]
# @PURPOSE: Non-dangerous (guarded) commands must still require confirmation before execution.
# @PRE: Admin user sends a backup command that was previously auto-executed.
# @POST: Response state is needs_confirmation with confirm and cancel actions.
def test_guarded_operation_always_requires_confirmation():
    _clear_assistant_state()
    response = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="сделай бэкап окружения dev"
            ),
            current_user=_admin_user(),
            task_manager=_FakeTaskManager(),
            config_manager=_FakeConfigManager(),
            db=_FakeDb(),
        )
    )
    assert response.state == "needs_confirmation"
    assert response.confirmation_id is not None
    action_types = {a.type for a in response.actions}
    assert "confirm" in action_types
    assert "cancel" in action_types
    assert "Выполнить" in response.text or "Подтвердите" in response.text


# [/DEF:test_guarded_operation_always_requires_confirmation:Function]


# [DEF:test_guarded_operation_confirm_roundtrip:Function]
# @PURPOSE: Guarded operation must execute successfully after explicit confirmation.
# @PRE: Admin user sends a non-dangerous migration command (dev → dev).
# @POST: After confirmation, response transitions to started/success with task_id.
def test_guarded_operation_confirm_roundtrip():
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()

    first = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="запусти миграцию с dev на dev для дашборда 5"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert first.state == "needs_confirmation"
    assert first.confirmation_id

    second = _run_async(
        assistant_module.confirm_operation(
            confirmation_id=first.confirmation_id,
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )
    assert second.state == "started"
    assert second.task_id is not None


# [/DEF:test_guarded_operation_confirm_roundtrip:Function]
# [DEF:test_confirm_nonexistent_id_returns_404:Function]
# @PURPOSE: Confirming a non-existent ID should raise 404.
# @PRE: user tries to confirm a random/fake UUID.
# @POST: FastAPI HTTPException with status 404.
def test_confirm_nonexistent_id_returns_404():
    from fastapi import HTTPException
    _clear_assistant_state()
    with pytest.raises(HTTPException) as exc:
        _run_async(
            assistant_module.confirm_operation(
                confirmation_id="non-existent-id",
                current_user=_admin_user(),
                task_manager=_FakeTaskManager(),
                config_manager=_FakeConfigManager(),
                db=_FakeDb(),
            )
        )
    assert exc.value.status_code == 404


# [/DEF:test_confirm_nonexistent_id_returns_404:Function]
# [DEF:test_migration_with_dry_run_includes_summary:Function]
# @PURPOSE: Migration command with dry run flag must return the dry run summary in confirmation text.
# @PRE: user specifies a migration with --dry-run flag.
# @POST: Response state is needs_confirmation and text contains dry-run summary counts.
def test_migration_with_dry_run_includes_summary(monkeypatch):
    import src.core.migration.dry_run_orchestrator as dry_run_module
    from unittest.mock import MagicMock
    _clear_assistant_state()
    task_manager = _FakeTaskManager()
    db = _FakeDb()

    class _FakeDryRunService:
        def run(self, selection, source_client, target_client, db_session):
            return {
                "summary": {
                    "dashboards": {"create": 1, "update": 0, "delete": 0},
                    "charts": {"create": 3, "update": 2, "delete": 1},
                    "datasets": {"create": 0, "update": 1, "delete": 0}
                }
            }
            
    monkeypatch.setattr(dry_run_module, "MigrationDryRunService", _FakeDryRunService)
    
    import src.core.superset_client as superset_client_module
    monkeypatch.setattr(superset_client_module, "SupersetClient", lambda env: MagicMock())

    start = _run_async(
        assistant_module.send_message(
            request=assistant_module.AssistantMessageRequest(
                message="миграция с dev на prod для дашборда 10 --dry-run"
            ),
            current_user=_admin_user(),
            task_manager=task_manager,
            config_manager=_FakeConfigManager(),
            db=db,
        )
    )

    assert start.state == "needs_confirmation"
    assert "отчет dry-run: ВКЛ" in start.text
    assert "Отчет dry-run:" in start.text
    assert "создано новых объектов: 4" in start.text
    assert "обновлено: 3" in start.text
    assert "удалено: 1" in start.text
# [/DEF:test_migration_with_dry_run_includes_summary:Function]
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
