from __future__ import annotations

import hashlib
import json
import uuid

import pytest
import sqlalchemy as sa
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import create_async_engine

from langbot.pkg.entity import persistence
from langbot.pkg.entity.persistence.base import Base
from langbot.pkg.persistence.alembic_runner import run_alembic_stamp, run_alembic_upgrade
from langbot.pkg.utils import importutil

from .resource_migration_support import TENANT_TABLES, create_legacy_resource_schema


pytestmark = [pytest.mark.integration, pytest.mark.asyncio]


async def _inspect(engine, callback):
    async with engine.connect() as conn:
        return await conn.run_sync(callback)


async def test_legacy_sqlite_resources_are_backfilled_and_contracted(tmp_path):
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "legacy-resources.db"}')
    try:
        await create_legacy_resource_schema(engine, instance_uuid='resource-migration-test')
        await run_alembic_stamp(engine, '0008_mcp_resource_prefs')
        await run_alembic_upgrade(engine, 'head')

        async with engine.connect() as conn:
            workspace_uuid = await conn.scalar(sa.text("SELECT uuid FROM workspaces WHERE source = 'local'"))
            assert workspace_uuid is not None
            for table_name in TENANT_TABLES:
                count, distinct_workspaces = (
                    await conn.execute(sa.text(f'SELECT COUNT(*), COUNT(DISTINCT workspace_uuid) FROM {table_name}'))
                ).one()
                assert count == 1, table_name
                assert distinct_workspaces == 1, table_name
                assert (
                    await conn.scalar(
                        sa.text(f'SELECT COUNT(*) FROM {table_name} WHERE workspace_uuid != :workspace_uuid'),
                        {'workspace_uuid': workspace_uuid},
                    )
                    == 0
                )

            api_key = (
                (await conn.execute(sa.text('SELECT key_hash, scopes, status, created_by_account_uuid FROM api_keys')))
                .mappings()
                .one()
            )
            assert api_key['key_hash'] == hashlib.sha256(b'lbk_legacy-secret').hexdigest()
            stored_scopes = api_key['scopes']
            if isinstance(stored_scopes, str):
                stored_scopes = json.loads(stored_scopes)
            assert stored_scopes == ['*']
            assert api_key['status'] == 'active'
            assert api_key['created_by_account_uuid'] is not None
            assert await conn.scalar(sa.text('SELECT normalized_email FROM users')) == 'owner@example.com'
            legacy_kb = (
                (
                    await conn.execute(
                        sa.text(
                            'SELECT collection_id, legacy_vector_collection FROM knowledge_bases WHERE uuid = :uuid'
                        ),
                        {'uuid': 'kb-1'},
                    )
                )
                .mappings()
                .one()
            )
            assert legacy_kb['collection_id'] == 'collection-1'
            assert legacy_kb['legacy_vector_collection'] == 1
            legacy_binary_storage = (
                (
                    await conn.execute(
                        sa.text(
                            'SELECT workspace_uuid, unique_key, key, owner_type, owner, value '
                            "FROM binary_storages WHERE owner_type = 'plugin' AND owner = 'demo'"
                        )
                    )
                )
                .mappings()
                .one()
            )
            assert legacy_binary_storage == {
                'workspace_uuid': workspace_uuid,
                'unique_key': 'plugin:demo:key',
                'key': 'key',
                'owner_type': 'plugin',
                'owner': 'demo',
                'value': b'legacy-plugin-value',
            }
            assert (
                await conn.scalar(
                    sa.text(
                        'SELECT COUNT(*) FROM metadata '
                        "WHERE key IN ('wizard_status', 'wizard_progress', 'rag_plugin_migration_needed')"
                    )
                )
                == 0
            )
            assert (
                await conn.scalar(
                    sa.text('SELECT COUNT(*) FROM workspace_metadata WHERE workspace_uuid = :workspace_uuid'),
                    {'workspace_uuid': workspace_uuid},
                )
                == 3
            )

        api_columns = await _inspect(
            engine,
            lambda conn: {column['name'] for column in sa.inspect(conn).get_columns('api_keys')},
        )
        assert 'key' not in api_columns
        assert {'uuid', 'key_hash', 'scopes', 'status', 'expires_at', 'last_used_at'} <= api_columns
        for table_name in TENANT_TABLES:
            columns = await _inspect(
                engine,
                lambda conn, name=table_name: {column['name']: column for column in sa.inspect(conn).get_columns(name)},
            )
            assert columns['workspace_uuid']['nullable'] is False, table_name
            if table_name == 'knowledge_bases':
                assert columns['legacy_vector_collection']['nullable'] is False

        pk_columns = {
            table_name: tuple(
                (
                    await _inspect(
                        engine,
                        lambda conn, name=table_name: sa.inspect(conn).get_pk_constraint(name),
                    )
                )['constrained_columns']
            )
            for table_name in ('binary_storages', 'plugin_settings', 'monitoring_sessions')
        }
        assert pk_columns == {
            'binary_storages': ('workspace_uuid', 'unique_key'),
            'plugin_settings': ('workspace_uuid', 'plugin_author', 'plugin_name'),
            'monitoring_sessions': ('workspace_uuid', 'session_id'),
        }

        pipeline_run_foreign_keys = await _inspect(
            engine,
            lambda conn: sa.inspect(conn).get_foreign_keys('pipeline_run_records'),
        )
        assert any(
            tuple(foreign_key['constrained_columns']) == ('workspace_uuid', 'pipeline_uuid')
            and foreign_key['referred_table'] == 'legacy_pipelines'
            and tuple(foreign_key['referred_columns']) == ('workspace_uuid', 'uuid')
            for foreign_key in pipeline_run_foreign_keys
        )
    finally:
        await engine.dispose()


async def test_legacy_vector_marker_backfill_resumes_from_nullable_expand_step(tmp_path):
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "legacy-vector-retry.db"}')
    try:
        await create_legacy_resource_schema(engine, instance_uuid='legacy-vector-retry')
        async with engine.begin() as conn:
            await conn.execute(sa.text('ALTER TABLE knowledge_bases ADD COLUMN legacy_vector_collection BOOLEAN NULL'))
        await run_alembic_stamp(engine, '0008_mcp_resource_prefs')
        await run_alembic_upgrade(engine, 'head')

        async with engine.connect() as conn:
            assert (
                await conn.scalar(
                    sa.text('SELECT legacy_vector_collection FROM knowledge_bases WHERE uuid = :uuid'),
                    {'uuid': 'kb-1'},
                )
                == 1
            )
        columns = await _inspect(
            engine,
            lambda conn: {column['name']: column for column in sa.inspect(conn).get_columns('knowledge_bases')},
        )
        assert columns['legacy_vector_collection']['nullable'] is False
    finally:
        await engine.dispose()


async def test_sqlite_scoped_keys_allow_cross_workspace_but_reject_same_workspace(tmp_path):
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "scoped-keys.db"}')
    try:
        await create_legacy_resource_schema(engine, instance_uuid='scoped-key-test')
        await run_alembic_stamp(engine, '0008_mcp_resource_prefs')
        await run_alembic_upgrade(engine, 'head')

        second_workspace_uuid = str(uuid.uuid4())
        async with engine.begin() as conn:
            await conn.execute(sa.text('PRAGMA foreign_keys=ON'))
            first_workspace_uuid = await conn.scalar(sa.text("SELECT uuid FROM workspaces WHERE source = 'local'"))
            await conn.execute(
                sa.text(
                    'INSERT INTO workspaces '
                    '(uuid, instance_uuid, name, slug, type, status, source, projection_revision) '
                    "VALUES (:uuid, 'scoped-key-test', 'Second', 'second', 'team', 'active', "
                    "'cloud_projection', 0)"
                ),
                {'uuid': second_workspace_uuid},
            )
            await conn.execute(
                sa.text(
                    'INSERT INTO mcp_servers (uuid, workspace_uuid, name, enable, updated_at) '
                    "VALUES ('mcp-2', :workspace_uuid, 'shared-name', 1, CURRENT_TIMESTAMP)"
                ),
                {'workspace_uuid': second_workspace_uuid},
            )
            await conn.execute(
                sa.text(
                    'INSERT INTO plugin_settings '
                    '(workspace_uuid, plugin_author, plugin_name, enabled, '
                    'installation_uuid, artifact_digest, runtime_revision) '
                    "VALUES (:workspace_uuid, 'author', 'plugin', 1, "
                    ':installation_uuid, :artifact_digest, 1)'
                ),
                {
                    'workspace_uuid': second_workspace_uuid,
                    'installation_uuid': str(uuid.uuid4()),
                    'artifact_digest': hashlib.sha256(b'test-plugin-artifact').hexdigest(),
                },
            )
            await conn.execute(
                sa.text(
                    'INSERT INTO binary_storages '
                    '(workspace_uuid, unique_key, key, owner_type, owner, value) '
                    "VALUES (:workspace_uuid, 'plugin:demo:key', 'key', 'plugin', 'demo', X'')"
                ),
                {'workspace_uuid': second_workspace_uuid},
            )
            await conn.execute(
                sa.text(
                    'INSERT INTO monitoring_sessions '
                    '(workspace_uuid, session_id, bot_id, last_activity, is_active) '
                    "VALUES (:workspace_uuid, 'session-1', 'bot-2', CURRENT_TIMESTAMP, 1)"
                ),
                {'workspace_uuid': second_workspace_uuid},
            )

        with pytest.raises(IntegrityError):
            async with engine.begin() as conn:
                await conn.execute(sa.text('PRAGMA foreign_keys=ON'))
                await conn.execute(
                    sa.text(
                        'INSERT INTO mcp_servers (uuid, workspace_uuid, name, enable, updated_at) '
                        "VALUES ('mcp-duplicate', :workspace_uuid, 'shared-name', 1, CURRENT_TIMESTAMP)"
                    ),
                    {'workspace_uuid': first_workspace_uuid},
                )

        with pytest.raises(IntegrityError):
            async with engine.begin() as conn:
                await conn.execute(sa.text('PRAGMA foreign_keys=ON'))
                await conn.execute(
                    sa.text(
                        'INSERT INTO llm_models (uuid, workspace_uuid, name, provider_uuid) '
                        "VALUES ('cross-workspace-model', :workspace_uuid, 'model', 'provider-1')"
                    ),
                    {'workspace_uuid': second_workspace_uuid},
                )

        with pytest.raises(IntegrityError):
            async with engine.begin() as conn:
                await conn.execute(sa.text('PRAGMA foreign_keys=ON'))
                await conn.execute(
                    sa.text(
                        'INSERT INTO pipeline_run_records '
                        '(uuid, workspace_uuid, pipeline_uuid, created_at) '
                        "VALUES ('cross-workspace-run', :workspace_uuid, 'pipeline-1', CURRENT_TIMESTAMP)"
                    ),
                    {'workspace_uuid': second_workspace_uuid},
                )

        with pytest.raises(IntegrityError):
            async with engine.begin() as conn:
                await conn.execute(
                    sa.text(
                        'INSERT INTO mcp_servers (uuid, name, enable, updated_at) '
                        "VALUES ('unscoped-mcp', 'unscoped', 1, CURRENT_TIMESTAMP)"
                    )
                )
    finally:
        await engine.dispose()


async def test_fresh_sqlite_schema_matches_resource_tenancy_contract(tmp_path):
    importutil.import_modules_in_pkg(persistence)
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "fresh-resources.db"}')
    try:
        async with engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)
        await run_alembic_stamp(engine, '0001_baseline')
        await run_alembic_upgrade(engine, 'head')

        tables = await _inspect(engine, lambda conn: set(sa.inspect(conn).get_table_names()))
        assert set(TENANT_TABLES) | {'workspace_metadata'} <= tables
        for table_name in TENANT_TABLES:
            columns = await _inspect(
                engine,
                lambda conn, name=table_name: {column['name']: column for column in sa.inspect(conn).get_columns(name)},
            )
            assert columns['workspace_uuid']['nullable'] is False, table_name
            if table_name == 'knowledge_bases':
                assert columns['legacy_vector_collection']['nullable'] is False
    finally:
        await engine.dispose()
