"""GE-Enforce manifestblobs model-vs-migration parity gate.

manifestblobs is created by the SECOND geenforce migration
(0002_manifest_blobs.py), and the existing DDL-parity test only diffs the
baseline (0001) tables - it does not even list ManifestBlob. So the http-blob
store table has had NO parity coverage: a column added to the ManifestBlob model
but not the migration (or vice versa) would ship a broken schema silently.

This gate builds manifestblobs two ways and diffs it column-by-column:
  1. the ManifestBlob model via metadata.create_all (desired shape), and
  2. the 0002 migration's upgrade() run against a fresh engine (shipped shape).
Both go through the same SQLite dialect so reflected types render identically.
"""

import importlib.util
from pathlib import Path

from sqlalchemy import create_engine, inspect
from alembic.migration import MigrationContext
from alembic.operations import Operations


_BLOBS_MIGRATION = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
                    'geenforce' / 'migrations' / 'versions' /
                    '0002_manifest_blobs.py')

_EXPECTED_COLUMNS = ('sha256', 'filename', 'contenttype', 'sizebytes',
                     'createdat')


def _migration_columns():
    """Run the 0002 migration upgrade() on a fresh engine; return the reflected
    manifestblobs columns as {name: column-dict}."""
    spec = importlib.util.spec_from_file_location('geenforce_blobs',
                                                  str(_BLOBS_MIGRATION))
    migration = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(migration)

    engine = create_engine('sqlite://')
    with engine.connect() as connection:
        operations = Operations(MigrationContext.configure(connection))
        operations._install_proxy()
        try:
            migration.upgrade()
        finally:
            operations._remove_proxy()
        connection.commit()
        inspector = inspect(connection)
        assert 'manifestblobs' in inspector.get_table_names(), \
            'migration did not create manifestblobs'
        return {column['name']: column
                for column in inspector.get_columns('manifestblobs')}


def _model_columns(db):
    """Build manifestblobs from the ManifestBlob model; return {name: column}."""
    from plugins.geenforce.models.manifest import ManifestBlob

    engine = create_engine('sqlite://')
    db.metadata.create_all(engine, tables=[ManifestBlob.__table__])
    return {column['name']: column
            for column in inspect(engine).get_columns('manifestblobs')}


def _colkey(column):
    """Normalize a reflected column to the facets we gate on (type, nullable)."""
    return (str(column['type']), bool(column['nullable']))


def test_manifestblob_model_matches_migration(app, db):
    """Every ManifestBlob column matches what the 0002 migration builds."""
    with app.app_context():
        modelcols = _model_columns(db)
        migrationcols = _migration_columns()

        drift = []
        for name in sorted(set(modelcols) - set(migrationcols)):
            drift.append(f'{name}: in model, not in migration')
        for name in sorted(set(migrationcols) - set(modelcols)):
            drift.append(f'{name}: in migration, not in model')
        for name in sorted(set(modelcols) & set(migrationcols)):
            if _colkey(modelcols[name]) != _colkey(migrationcols[name]):
                drift.append(
                    f'{name}: model {_colkey(modelcols[name])} != '
                    f'migration {_colkey(migrationcols[name])}')

        assert not drift, ('manifestblobs model/migration DDL drift:\n'
                           + '\n'.join(drift))


def test_manifestblob_expected_columns_present_both_sides(app, db):
    """Lock the load-bearing columns (sha256 primary key + registry fields)
    exist in BOTH the model and the migration."""
    with app.app_context():
        modelcols = _model_columns(db)
        migrationcols = _migration_columns()
        for name in _EXPECTED_COLUMNS:
            assert name in modelcols, f'{name} missing from ManifestBlob model'
            assert name in migrationcols, f'{name} missing from migration'


def test_manifestblob_sha256_is_not_nullable(app, db):
    """The content hash is the primary key: it must be NOT NULL on both sides."""
    with app.app_context():
        modelcols = _model_columns(db)
        migrationcols = _migration_columns()
        assert modelcols['sha256']['nullable'] is False
        assert migrationcols['sha256']['nullable'] is False
