"""Plugin contract compliance tests.

Asserts every bundled plugin satisfies the contract surface declared in
ADR-001 and the BasePlugin ABC. Run on every change to BasePlugin or any
plugin's plugin.py / manifest.json.
"""

import json
from pathlib import Path

import pytest
from flask import Blueprint

from shopdb import __contract_version__
from shopdb.plugins import plugin_manager
from shopdb.plugins.base import BasePlugin, PluginMeta


BUNDLED_PLUGINS = ('computers', 'equipment', 'network', 'notifications', 'printers', 'usb')


@pytest.fixture
def plugin_classes(app):
    """Load each bundled plugin's class without instantiating in app context."""
    classes = {}
    with app.app_context():
        loader = plugin_manager.loader
        for name in BUNDLED_PLUGINS:
            cls = loader.load_plugin_class(name)
            assert cls is not None, f'Plugin class for {name} could not be loaded'
            classes[name] = cls
    return classes


@pytest.fixture
def plugin_instances(app, plugin_classes):
    """Instantiate each bundled plugin in app context."""
    return {name: cls() for name, cls in plugin_classes.items()}


def test_contract_version_is_set():
    """Framework declares a contract version."""
    assert __contract_version__
    parts = __contract_version__.split('.')
    assert len(parts) == 3, f'Expected semver X.Y.Z, got {__contract_version__}'
    for part in parts:
        assert part.isdigit(), f'Non-numeric semver part in {__contract_version__}'


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_class_subclasses_baseplugin(plugin_classes, name):
    """Each plugin class extends BasePlugin."""
    cls = plugin_classes[name]
    assert issubclass(cls, BasePlugin), f'{name} does not subclass BasePlugin'


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_manifest_has_required_fields(name):
    """Each plugin's manifest.json declares the required fields."""
    manifestpath = Path('plugins') / name / 'manifest.json'
    assert manifestpath.exists(), f'{name} is missing manifest.json'

    manifest = json.loads(manifestpath.read_text())

    assert manifest.get('name') == name
    assert manifest.get('version'), f'{name} manifest missing version'
    assert manifest.get('description'), f'{name} manifest missing description'
    assert manifest.get('core_version'), f'{name} manifest missing core_version'


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_meta_returns_pluginmeta(plugin_instances, name):
    """plugin.meta returns a PluginMeta instance with sane fields."""
    plugin = plugin_instances[name]
    meta = plugin.meta
    assert isinstance(meta, PluginMeta)
    assert meta.name == name
    assert meta.version
    assert meta.api_prefix and meta.api_prefix.startswith('/api/')


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_get_blueprint_returns_blueprint_or_none(plugin_instances, name):
    """get_blueprint returns a Flask Blueprint or None."""
    plugin = plugin_instances[name]
    bp = plugin.get_blueprint()
    assert bp is None or isinstance(bp, Blueprint), (
        f'{name}.get_blueprint returned {type(bp).__name__}'
    )


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_get_models_returns_list(plugin_instances, name):
    """get_models returns a list of model classes."""
    plugin = plugin_instances[name]
    models = plugin.get_models()
    assert isinstance(models, list), f'{name}.get_models did not return a list'
    for model in models:
        assert hasattr(model, '__tablename__'), (
            f'{name}.get_models returned {model} without __tablename__'
        )


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_get_collector_schema_returns_dict_or_none(plugin_instances, name):
    """get_collector_schema returns None or a dict with identityfield + fields."""
    plugin = plugin_instances[name]
    schema = plugin.get_collector_schema()
    if schema is None:
        return
    assert isinstance(schema, dict)
    assert 'identityfield' in schema, (
        f'{name}.get_collector_schema must declare identityfield'
    )
    assert 'fields' in schema, (
        f'{name}.get_collector_schema must declare fields'
    )


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_get_navigation_items_is_iterable(plugin_instances, name):
    """get_navigation_items returns an iterable (default empty list)."""
    plugin = plugin_instances[name]
    items = plugin.get_navigation_items()
    assert isinstance(items, list)


@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
def test_plugin_get_searchable_fields_is_iterable(plugin_instances, name):
    """get_searchable_fields returns a list (default empty)."""
    plugin = plugin_instances[name]
    fields = plugin.get_searchable_fields()
    assert isinstance(fields, list)


def test_baseplugin_does_not_have_event_handlers_hook():
    """The removed get_event_handlers hook must not be on BasePlugin."""
    assert not hasattr(BasePlugin, 'get_event_handlers'), (
        'get_event_handlers was removed for v1. See ADR-001 contract surface.'
    )


def test_baseplugin_has_collector_schema_hook():
    """The collector schema hook is on the contract surface."""
    assert hasattr(BasePlugin, 'get_collector_schema')
