"""
API smoke integration tests.

Tests real HTTP API behavior using Quart test client.
Validates controller/service/routing wiring without real provider/platform.

Run: uv run pytest tests/integration/api/test_smoke.py -q
"""

from __future__ import annotations

from types import SimpleNamespace

import pytest
from unittest.mock import MagicMock, AsyncMock, Mock

from tests.factories import FakeApp


pytestmark = pytest.mark.integration


# ============== FIXTURE FOR SYS.MODULES ISOLATION ==============


@pytest.fixture(scope='module')
def mock_circular_import_chain():
    """
    Break circular import chain for API controller using isolated_sys_modules.

    Chain: http_controller → groups/plugins → core.app → pipeline entities

    We need to mock core.app to prevent the circular chain when importing HTTPController.
    But we must allow groups to be imported to populate preregistered_groups.
    """
    from tests.utils.import_isolation import isolated_sys_modules, MockLifecycleControlScope

    # Mock core.app with minimal Application that groups can reference
    class FakeMinimalApplication:
        pass

    mock_app = MagicMock()
    mock_app.Application = FakeMinimalApplication

    # Mock core.entities with proper Enum
    mock_entities = MagicMock()
    mock_entities.LifecycleControlScope = MockLifecycleControlScope

    # Modules to clear (force re-import after mocking)
    clear = [
        'langbot.pkg.api.http.controller.group',
        'langbot.pkg.api.http.controller.groups',
        'langbot.pkg.api.http.controller.groups.system',
        'langbot.pkg.api.http.controller.groups.user',
        'langbot.pkg.api.http.controller.main',
    ]

    with isolated_sys_modules(
        mocks={
            'langbot.pkg.core.app': mock_app,
            'langbot.pkg.core.entities': mock_entities,
        },
        clear=clear,
    ):
        # Import groups after mocking core.app/core.entities
        import langbot.pkg.api.http.controller.group as _group_module  # noqa: E402, F401
        import langbot.pkg.api.http.controller.groups.system as _system_group  # noqa: E402, F401
        import langbot.pkg.api.http.controller.groups.user as _user_group  # noqa: E402, F401

        yield


# ============== FAKE APPLICATION FOR API TESTS ==============


@pytest.fixture
def fake_api_app():
    """
    Create minimal FakeApp for API smoke tests with all required services.

    Uses tests.factories.FakeApp as base and adds API-specific services.
    """
    app = FakeApp()

    # API-specific config
    app.instance_config.data.update(
        {
            'api': {'port': 5300},
            'plugin': {'enable_marketplace': True},
            'space': {'url': 'https://space.langbot.app'},
            'system': {'allow_modify_login_info': True, 'recovery_key': 'recovery-secret', 'limitation': {}},
        }
    )

    # API-specific services
    app.user_service = Mock()
    app.user_service.is_initialized = AsyncMock(return_value=False)
    app.user_service.authenticate = AsyncMock(return_value='fake_token')
    app.user_service.create_user = AsyncMock()
    app.user_service.verify_jwt_token = AsyncMock(side_effect=ValueError('Invalid token'))
    app.user_service.get_user_by_email = AsyncMock(return_value=Mock())
    app.user_service.generate_jwt_token = AsyncMock(return_value='fake_token')

    app.apikey_service = Mock()
    app.apikey_service.verify_api_key = AsyncMock(return_value=True)

    app.maintenance_service = Mock()
    app.maintenance_service.get_storage_analysis = AsyncMock(return_value={})

    app.plugin_connector.is_enable_plugin = False
    app.plugin_connector.ping_plugin_runtime = AsyncMock()

    app.task_mgr.get_tasks_dict = Mock(return_value={'tasks': []})
    app.task_mgr.get_task_by_id = Mock(return_value=None)

    # Required by controller groups
    app.model_mgr = Mock()
    app.platform_mgr = Mock()
    app.pipeline_pool = Mock()
    app.pipeline_mgr = Mock()

    return app


# ============== QUART TEST CLIENT FIXTURE ==============


@pytest.fixture
async def quart_test_client(fake_api_app, http_controller_cls):
    """
    Create Quart test client with real HTTPController and route registration.

    Requires mock_circular_import_chain fixture to run first (usefixtures).
    """
    controller = http_controller_cls(fake_api_app)
    await controller.initialize()

    client = controller.quart_app.test_client()

    yield client


# ============== API SMOKE TESTS ==============


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestHealthEndpoint:
    """Tests for /healthz endpoint - simplest smoke test."""

    @pytest.mark.asyncio
    async def test_healthz_returns_ok(self, quart_test_client):
        """
        /healthz endpoint returns {'code': 0, 'msg': 'ok'}.

        This tests:
        - HTTPController instantiation
        - Quart app creation
        - Route registration
        - Basic response handling
        """
        response = await quart_test_client.get('/healthz')

        assert response.status_code == 200
        data = await response.get_json()
        assert data == {'code': 0, 'msg': 'ok', 'resources': {}}

    @pytest.mark.asyncio
    async def test_healthz_no_auth_required(self, quart_test_client):
        """
        /healthz doesn't require authentication.

        Tests that AuthType.NONE endpoints work without headers.
        """
        response = await quart_test_client.get('/healthz')
        assert response.status_code == 200


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestSystemEndpoint:
    """Tests for /api/v1/system endpoints."""

    @pytest.mark.asyncio
    async def test_system_info_no_auth(self, quart_test_client):
        """
        /api/v1/system/info returns system information without auth.

        AuthType.NONE endpoint.
        """
        response = await quart_test_client.get('/api/v1/system/info')

        assert response.status_code == 200
        data = await response.get_json()

        # Verify response structure
        assert data['code'] == 0
        assert data['msg'] == 'ok'
        assert 'data' in data

        # Verify expected fields
        system_data = data['data']
        assert 'version' in system_data
        assert 'debug' in system_data
        assert 'edition' in system_data


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestProtectedEndpoints:
    """Tests for authentication/authorization behavior."""

    @pytest.mark.asyncio
    async def test_protected_endpoint_rejects_no_token(self, quart_test_client):
        """
        Protected endpoint (USER_TOKEN) returns 401 without auth.

        Tests that AuthType.USER_TOKEN properly rejects unauthorized requests.
        """
        # /api/v1/user/check-token requires USER_TOKEN
        response = await quart_test_client.get('/api/v1/user/check-token')

        assert response.status_code == 401
        data = await response.get_json()

        # Verify error response structure
        assert data['code'] == -1
        assert 'msg' in data

    @pytest.mark.asyncio
    async def test_protected_endpoint_with_invalid_token(self, quart_test_client):
        """
        Protected endpoint returns 401 with invalid token.
        """
        response = await quart_test_client.get(
            '/api/v1/user/check-token', headers={'Authorization': 'Bearer invalid_token'}
        )

        assert response.status_code == 401


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestInvalidPayload:
    """Tests for error handling with invalid payloads."""

    @pytest.mark.asyncio
    async def test_missing_json_body(self, quart_test_client):
        """
        POST endpoint without JSON body handles gracefully.
        """
        # /api/v1/user/auth expects JSON with 'user' and 'password'
        response = await quart_test_client.post('/api/v1/user/auth')

        # Should return error (500, 400, or 401) with stable JSON structure
        assert response.status_code in (400, 500, 401)
        data = await response.get_json()

        # Verify error response has expected structure
        assert 'code' in data
        assert 'msg' in data

    @pytest.mark.asyncio
    async def test_invalid_json_structure(self, quart_test_client):
        """
        POST with wrong JSON structure returns stable error.
        """
        response = await quart_test_client.post('/api/v1/user/auth', json={'wrong_field': 'value'})

        # Should return error with stable JSON structure
        assert response.status_code in (400, 500, 401)
        data = await response.get_json()
        assert 'code' in data
        assert 'msg' in data


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestUserInitEndpoint:
    """Tests for /api/v1/user/init endpoint."""

    @pytest.mark.asyncio
    async def test_user_init_get_returns_not_initialized(self, quart_test_client):
        """
        GET /api/v1/user/init returns initialized status.

        Uses fake user_service.is_initialized() = False.
        """
        response = await quart_test_client.get('/api/v1/user/init')

        assert response.status_code == 200
        data = await response.get_json()

        assert data['code'] == 0
        assert data['msg'] == 'ok'
        assert data['data']['initialized'] is False

    @pytest.mark.asyncio
    async def test_account_info_exposes_instance_capabilities_not_first_account(self, quart_test_client, fake_api_app):
        fake_api_app.user_service.is_initialized.return_value = True
        fake_api_app.user_service.get_login_capabilities = AsyncMock(
            return_value={'password_login_enabled': True, 'space_login_enabled': False}
        )
        fake_api_app.user_service.get_first_user = AsyncMock(
            side_effect=AssertionError('public login bootstrap must not inspect an account')
        )

        response = await quart_test_client.get('/api/v1/user/account-info')

        assert response.status_code == 200
        data = await response.get_json()
        assert data['data'] == {
            'initialized': True,
            'authenticated_invitation_acceptance_enabled': False,
            'invitation_registration_enabled': True,
            'password_login_enabled': True,
            'space_login_enabled': False,
        }
        fake_api_app.user_service.get_login_capabilities.assert_awaited_once_with()
        fake_api_app.user_service.get_first_user.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_account_info_enables_authenticated_invitation_acceptance_in_cloud(
        self, quart_test_client, fake_api_app
    ):
        fake_api_app.deployment = SimpleNamespace(mode='cloud')
        fake_api_app.user_service.is_initialized.return_value = True
        fake_api_app.user_service.get_login_capabilities = AsyncMock(
            return_value={'password_login_enabled': True, 'space_login_enabled': True}
        )

        response = await quart_test_client.get('/api/v1/user/account-info')

        assert response.status_code == 200
        data = await response.get_json()
        assert data['data'] == {
            'initialized': True,
            'authenticated_invitation_acceptance_enabled': True,
            'invitation_registration_enabled': False,
            'password_login_enabled': False,
            'space_login_enabled': True,
        }

    @pytest.mark.asyncio
    async def test_account_info_enables_local_invitation_registration_for_oauth_only_oss(
        self, quart_test_client, fake_api_app
    ):
        fake_api_app.user_service.is_initialized.return_value = True
        fake_api_app.user_service.get_login_capabilities = AsyncMock(
            return_value={'password_login_enabled': False, 'space_login_enabled': True}
        )

        response = await quart_test_client.get('/api/v1/user/account-info')

        assert response.status_code == 200
        data = await response.get_json()
        assert data['data'] == {
            'initialized': True,
            'authenticated_invitation_acceptance_enabled': False,
            'invitation_registration_enabled': True,
            'password_login_enabled': False,
            'space_login_enabled': True,
        }

    @pytest.mark.asyncio
    async def test_recovery_key_resets_any_existing_account(self, quart_test_client, fake_api_app, monkeypatch):
        fake_api_app.user_service.is_initialized.return_value = True
        fake_api_app.user_service.get_user_by_email.return_value = Mock(user='member@example.com')
        fake_api_app.user_service.reset_password = AsyncMock()
        monkeypatch.setattr('langbot.pkg.api.http.controller.groups.user.asyncio.sleep', AsyncMock())

        response = await quart_test_client.post(
            '/api/v1/user/reset-password',
            json={
                'user': 'member@example.com',
                'recovery_key': 'recovery-secret',
                'new_password': 'new-member-password',
            },
        )

        assert response.status_code == 200
        fake_api_app.user_service.reset_password.assert_awaited_once_with('member@example.com', 'new-member-password')


@pytest.mark.usefixtures('mock_circular_import_chain')
class TestRealImports:
    """Tests that verify real production code is imported."""

    def test_http_controller_real_import(self):
        """
        Verify HTTPController is real production class, not mock.
        """
        from langbot.pkg.api.http.controller.main import HTTPController

        assert HTTPController.__name__ == 'HTTPController'
        assert hasattr(HTTPController, 'initialize')
        assert hasattr(HTTPController, 'register_routes')

    def test_group_real_import(self):
        """
        Verify RouterGroup and AuthType are real production classes.
        """
        from langbot.pkg.api.http.controller.group import RouterGroup, AuthType, preregistered_groups

        assert RouterGroup.__name__ == 'RouterGroup'
        assert hasattr(AuthType, 'NONE')
        assert hasattr(AuthType, 'USER_TOKEN')
        assert isinstance(preregistered_groups, list)

    def test_system_group_registered(self):
        """
        Verify SystemRouterGroup is registered in preregistered_groups.
        """
        from langbot.pkg.api.http.controller.group import preregistered_groups

        # Find system group
        system_group = None
        for g in preregistered_groups:
            if g.name == 'system':
                system_group = g
                break

        assert system_group is not None
        assert system_group.path == '/api/v1/system'

    def test_user_group_registered(self):
        """
        Verify UserRouterGroup is registered in preregistered_groups.
        """
        from langbot.pkg.api.http.controller.group import preregistered_groups

        # Find user group
        user_group = None
        for g in preregistered_groups:
            if g.name == 'user':
                user_group = g
                break

        assert user_group is not None
        assert user_group.path == '/api/v1/user'
