import importlib
from pathlib import Path
from unittest.mock import AsyncMock, Mock

import pytest
from langchain_core.messages import AIMessage, HumanMessage
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine

import app.models  # noqa: F401
from app.database import Base, ensure_document_columns, ensure_message_columns
from app.agents.graph import _ainvoke, _compile_graph


@pytest.mark.anyio
async def test_ensure_message_columns_adds_attachments_for_existing_messages_table(tmp_path):
    db_path = tmp_path / 'test_messages.db'
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)

    async with engine.begin() as conn:
        await conn.execute(text(
            '''
            CREATE TABLE messages (
                id VARCHAR(36) PRIMARY KEY,
                conversation_id VARCHAR(36) NOT NULL,
                role VARCHAR(20) NOT NULL,
                content TEXT NOT NULL,
                model VARCHAR(100),
                tokens_used INTEGER,
                created_at DATETIME,
                updated_at DATETIME
            )
            '''
        ))
        result = await conn.execute(text("PRAGMA table_info(messages)"))
        columns_before = {row[1] for row in result.fetchall()}
        assert 'attachments' not in columns_before

        await ensure_message_columns(conn)

        result = await conn.execute(text("PRAGMA table_info(messages)"))
        columns_after = {row[1] for row in result.fetchall()}
        assert 'attachments' in columns_after

    await engine.dispose()


@pytest.mark.anyio
async def test_ainvoke_falls_back_to_invoke_for_wrapped_llm_services():
    llm = Mock()
    llm.ainvoke = None
    llm.invoke = AsyncMock(return_value=AIMessage(content='ok'))

    response = await _ainvoke(llm, [HumanMessage(content='ping')])

    assert response.content == 'ok'
    llm.invoke.assert_awaited_once()


def test_compile_graph_falls_back_when_callbacks_are_unsupported():
    compiled_without_callbacks = object()
    graph = Mock()
    graph.compile.side_effect = [TypeError("unexpected keyword argument 'callbacks'"), compiled_without_callbacks]

    compiled = _compile_graph(graph, callbacks=['cb'])

    assert compiled is compiled_without_callbacks
    assert graph.compile.call_count == 2
    assert graph.compile.call_args_list[0].kwargs == {'callbacks': ['cb']}
    assert graph.compile.call_args_list[1].kwargs == {}


def test_settings_resolve_data_paths_from_backend_directory():
    config_module = importlib.import_module('app.config')
    expected_data_dir = (Path(config_module.__file__).resolve().parent.parent / 'data').resolve()

    assert Path(config_module.settings.DATA_DIR) == expected_data_dir
    assert config_module.settings.DATABASE_URL.replace('\\', '/').endswith('/backend/data/jarvis.db')
    assert Path(config_module.settings.CHROMA_PERSIST_DIR) == expected_data_dir / 'chroma'
    assert Path(config_module.settings.UPLOAD_DIR) == expected_data_dir / 'uploads'


@pytest.mark.anyio
async def test_ensure_document_columns_adds_ingestion_fields_for_existing_documents_table(tmp_path):
    db_path = tmp_path / 'test_documents.db'
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)

    async with engine.begin() as conn:
        await conn.execute(text(
            '''
            CREATE TABLE documents (
                id VARCHAR(36) PRIMARY KEY,
                user_id VARCHAR(36) NOT NULL,
                title VARCHAR(500) NOT NULL,
                filename VARCHAR(500) NOT NULL,
                file_type VARCHAR(50) NOT NULL,
                file_size INTEGER NOT NULL,
                file_path VARCHAR(1000) NOT NULL,
                folder_id VARCHAR(36),
                summary TEXT,
                chunk_count INTEGER,
                is_indexed BOOLEAN,
                created_at DATETIME,
                updated_at DATETIME
            )
            '''
        ))
        result = await conn.execute(text("PRAGMA table_info(documents)"))
        columns_before = {row[1] for row in result.fetchall()}
        assert 'ingestion_status' not in columns_before
        assert 'ingestion_error' not in columns_before
        assert 'indexed_at' not in columns_before
        assert 'parser_version' not in columns_before
        assert 'index_version' not in columns_before
        assert 'normalized_content' not in columns_before
        assert 'normalized_format' not in columns_before

        await ensure_document_columns(conn)

        result = await conn.execute(text("PRAGMA table_info(documents)"))
        columns_after = {row[1] for row in result.fetchall()}
        assert 'ingestion_status' in columns_after
        assert 'ingestion_error' in columns_after
        assert 'indexed_at' in columns_after
        assert 'parser_version' in columns_after
        assert 'index_version' in columns_after
        assert 'normalized_content' in columns_after
        assert 'normalized_format' in columns_after

    await engine.dispose()
