from __future__ import annotations

import ast
from types import SimpleNamespace
from unittest.mock import AsyncMock
from unittest.mock import patch

import pytest
from amsdal.models.core.file import File
from amsdal_utils.models.enums import ModuleType
from amsdal_utils.models.enums import TransactionType

from amsdal_ml.mcp_server.server_model_explorer.services.models import TransactionSpec
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import _annotation_node_to_text
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import _classify_annotation
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import _extract_optional_inner
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import collect_transaction_catalog
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import is_system_file_model
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import resolve_transaction_name
from amsdal_ml.mcp_server.server_model_explorer.services.types import ModelInfo
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionParameterKind
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionScope
from tests.fixtures.models.book import Book


def _parse_annotation(src: str) -> ast.expr | None:
    stmt = ast.parse(src).body[0]
    assert isinstance(stmt, ast.AnnAssign)
    return stmt.annotation


def test_classify_annotation_pep604_optional_model_nullable():
    node = _parse_annotation('value: MockAuthor | None')
    meta = _classify_annotation(node, model_names={'MockAuthor'})

    assert meta.kind == TransactionParameterKind.MODEL
    assert meta.model_name == 'MockAuthor'
    assert meta.nullable is True


def test_classify_annotation_supports_list_primitive_kind():
    node = _parse_annotation('value: list[str]')
    meta = _classify_annotation(node, model_names={'MockAuthor'})

    assert meta.kind == TransactionParameterKind.PRIMITIVE
    assert meta.model_name is None
    assert meta.nullable is False


def test_classify_annotation_supports_optional_primitive_typing_optional():
    node = _parse_annotation('value: Optional[str]')
    meta = _classify_annotation(node, model_names={'MockAuthor'})

    assert meta.kind == TransactionParameterKind.PRIMITIVE
    assert meta.model_name is None
    assert meta.nullable is True


def test_annotation_node_to_text_missing_annotation():
    assert _annotation_node_to_text(None) == 'Any'


def test_extract_optional_inner_handles_pep604_both_sides():
    node_left = _parse_annotation('value: None | str')
    node_right = _parse_annotation('value: str | None')

    assert _extract_optional_inner(node_left) is not None
    assert _extract_optional_inner(node_right) is not None


def test_resolve_transaction_name_exact_and_case_insensitive():
    tx = TransactionSpec(name='AddToCart', description='', scope=TransactionType.USER, parameters=[])
    catalog = {'AddToCart': tx, 'DeleteItem': tx}

    assert resolve_transaction_name(catalog, 'AddToCart') == 'AddToCart'
    assert resolve_transaction_name(catalog, 'addtocart') == 'AddToCart'


def test_resolve_transaction_name_not_found():
    with pytest.raises(ValueError):
        resolve_transaction_name({}, 'Missing')


def test_is_system_file_model_true_false():
    assert is_system_file_model(File) is True
    assert is_system_file_model(Book) is False


@pytest.mark.asyncio
async def test_collect_transaction_catalog_respects_scope_and_annotation_meta():
    fake_def = SimpleNamespace(
        name='TxA',
        args=SimpleNamespace(
            args=[
                SimpleNamespace(arg='book', annotation=_parse_annotation('value: Book')),
                SimpleNamespace(arg='amount', annotation=_parse_annotation('value: int')),
            ]
        ),
    )
    fake_row = SimpleNamespace(
        title='TxA',
        description='create cart item',
        properties={
            'book': SimpleNamespace(type='Book'),
            'amount': SimpleNamespace(type='int'),
        },
    )
    fake_response = SimpleNamespace(rows=[fake_row])

    with (
        patch(
            'amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils._get_model_registry',
            return_value={'Book': ModelInfo(model=Book, scope=ModuleType.USER)},
        ),
        patch(
            'amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils.TransactionApi.get_transaction_definitions_with_type',
            return_value=[(fake_def, None, TransactionType.USER)],
        ),
        patch(
            'amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils.TransactionApi.get_transactions',
            new_callable=AsyncMock,
            return_value=fake_response,
        ) as mock_get_transactions,
    ):
        catalog = await collect_transaction_catalog(scope=TransactionScope.USER)

    mock_get_transactions.assert_awaited_once_with(scope=TransactionType.USER)
    assert 'TxA' in catalog
    assert catalog['TxA'].scope == TransactionType.USER
    assert catalog['TxA'].parameters[0].kind == TransactionParameterKind.MODEL
    assert catalog['TxA'].parameters[0].model_name == 'Book'
    assert catalog['TxA'].parameters[1].kind == TransactionParameterKind.PRIMITIVE
