"""Tests for plugin system framework — all 14 acceptance criteria."""

from __future__ import annotations

import asyncio
from typing import Any

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import inspect, select, text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker

from app.core.db import close_engine, reset_engine_for_testing
from app.core.event_bus import get_event_bus
from app.core.service_container import get_container
from app.main import create_app
from app.models.plugin import Plugin as PluginModel
from app.models.plugin import PluginMigration
from app.plugins.base import BasePlugin
from app.plugins.builtins.test_sample import TestSamplePlugin
from app.plugins.manifest import PluginManifest
from app.plugins.migration_runner import MigrationRunner, MigrationValidationError
from app.plugins.registry import get_registry, reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users

# ─── Bad Plugin for AC11 (migration without tenant_id) ───


class BadMigrationPlugin(BasePlugin):
    """Plugin with a migration that creates a table WITHOUT tenant_id."""

    manifest = PluginManifest(
        name="bad_migration_plugin",
        version="1.0.0",
        display_name="Bad Migration Plugin",
        description="Plugin with invalid migration (no tenant_id) for validator testing.",
        dependencies=[],
        routes=[],
        events=[],
        migrations=["0001_bad_migration.sql"],
        permissions=[],
    )


# ─── Fixtures ───


@pytest_asyncio.fixture
async def plugin_app(engine: AsyncEngine, redis_client):
    """FastAPI app with plugin registry initialized for testing."""
    reset_engine_for_testing(engine)
    app = create_app()

    # Reset and initialize registry (lifespan doesn't run with ASGITransport)
    registry = reset_registry_for_testing()
    registry.initialize(engine, app)

    # Reset service container and event bus
    container = get_container()
    await container.initialize()

    # Register test plugins manually (don't auto-discover to avoid side effects)
    registry.register_plugin(TestSamplePlugin())
    registry.register_plugin(BadMigrationPlugin())

    # Reset plugin service to use the new registry
    reset_plugin_service_for_testing(registry)

    yield app
    await close_engine()


@pytest_asyncio.fixture
async def plugin_client(plugin_app) -> AsyncClient:
    """HTTP test client with plugin registry active."""
    transport = ASGITransport(app=plugin_app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


@pytest_asyncio.fixture
async def authed_plugin_client(plugin_client: AsyncClient, db_session: AsyncSession) -> AsyncClient:
    """Authenticated admin client for plugin tests."""
    await seed_tenant_and_users(db_session)
    await login_client(plugin_client, "admin@tenanta.com")
    return plugin_client


@pytest_asyncio.fixture
async def db_session_for_plugins(engine: AsyncEngine) -> AsyncSession:
    """DB session for plugin test verification."""
    factory = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
    async with factory() as session:
        yield session
        await session.rollback()


# ─── AC1: GET /api/v1/plugins → 200 + list of plugins with status ───


@pytest.mark.asyncio
async def test_ac01_list_plugins(authed_plugin_client: AsyncClient):
    """AC1: GET /api/v1/plugins returns 200 with plugin list and status."""
    resp = await authed_plugin_client.get("/api/v1/plugins", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert "plugins" in data
    assert "total" in data
    assert data["total"] >= 2
    plugin_names = [p["name"] for p in data["plugins"]]
    assert "test_sample" in plugin_names
    assert "bad_migration_plugin" in plugin_names
    # Check status field exists
    for p in data["plugins"]:
        assert "status" in p
        assert "installed" in p
        assert "active" in p


# ─── AC2: POST /api/v1/plugins/{name}/install → 200, status=installed, migrations run ───


@pytest.mark.asyncio
async def test_ac02_install_plugin(
    authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
    """AC2: Install plugin runs migrations and sets status=installed."""
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "test_sample"
    assert data["status"] == "installed"
    assert data["installed"] is True
    assert data["active"] is False

    # Verify migration table was created (tenant_id column exists)
    result = await db_session_for_plugins.execute(
        text(
            "SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_test_data' AND column_name = 'tenant_id'"
        )
    )
    assert result.fetchone() is not None, "plugin_test_data table should have tenant_id column"


# ─── AC3: POST /api/v1/plugins/{name}/activate → 200, status=active, routes registered ───


@pytest.mark.asyncio
async def test_ac03_activate_plugin(authed_plugin_client: AsyncClient):
    """AC3: Activate plugin sets status=active and registers event listeners."""
    # Install first
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200

    # Activate
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "test_sample"
    assert data["status"] == "active"
    assert data["active"] is True


# ─── AC4: POST /api/v1/plugins/{name}/deactivate → 200, status=inactive, routes unregistered ───


@pytest.mark.asyncio
async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient):
    """AC4: Deactivate plugin sets status=inactive and unregisters event listeners."""
    # Install and activate first
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)

    # Deactivate
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "test_sample"
    assert data["status"] == "inactive"
    assert data["active"] is False


# ─── AC5: DELETE /api/v1/plugins/{name} → 200, plugin removed ───


@pytest.mark.asyncio
async def test_ac05_uninstall_plugin(
    authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
    """AC5: Uninstall plugin removes DB record."""
    # Install first
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)

    # Uninstall
    resp = await authed_plugin_client.delete("/api/v1/plugins/test_sample", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "uninstalled"
    assert data["installed"] is False

    # Verify DB record is gone
    result = await db_session_for_plugins.execute(
        select(PluginModel).where(PluginModel.name == "test_sample")
    )
    assert result.scalar_one_or_none() is None


# ─── AC6: DELETE /api/v1/plugins/{name}?remove_data=true → 200, plugin tables dropped ───


@pytest.mark.asyncio
async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, engine: AsyncEngine):
    """AC6: Uninstall with remove_data=true drops plugin tables."""
    # Install first (creates plugin_test_data table)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)

    # Verify table exists
    def _check_table(sync_conn):
        insp = inspect(sync_conn)
        return "plugin_test_data" in insp.get_table_names()

    async with engine.connect() as conn:
        table_exists_before = await conn.run_sync(_check_table)
    assert table_exists_before, "plugin_test_data table should exist after install"

    # Uninstall with remove_data=true
    resp = await authed_plugin_client.delete(
        "/api/v1/plugins/test_sample?remove_data=true", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "uninstalled"
    assert "plugin_test_data" in data.get("dropped_tables", [])

    # Verify table is gone
    async with engine.connect() as conn:
        table_exists_after = await conn.run_sync(_check_table)
    assert (
        not table_exists_after
    ), "plugin_test_data table should be dropped after uninstall with remove_data=true"


# ─── AC7: GET /api/v1/plugins/manifest → 200 + manifest schema documentation ───


@pytest.mark.asyncio
async def test_ac07_manifest_schema(authed_plugin_client: AsyncClient):
    """AC7: GET /api/v1/plugins/manifest returns schema documentation."""
    resp = await authed_plugin_client.get("/api/v1/plugins/manifest", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert "fields" in data
    assert "example" in data
    # Check required fields are documented
    assert "name" in data["fields"]
    assert "version" in data["fields"]
    assert "display_name" in data["fields"]
    assert "dependencies" in data["fields"]
    assert "routes" in data["fields"]
    assert "events" in data["fields"]
    assert "migrations" in data["fields"]
    # Check example has expected values
    assert data["example"]["name"] == "example_plugin"


# ─── AC8: Plugin activation registers event listeners on event bus ───


@pytest.mark.asyncio
async def test_ac08_activation_registers_event_listeners(
    authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
    """AC8: Activating a plugin registers its event listeners on the event bus."""
    registry = get_registry()
    plugin = registry.get_plugin("test_sample")
    assert plugin is not None

    # Install and activate
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)

    event_bus = get_event_bus()

    # Check that event bus has handlers for the plugin's events
    assert len(event_bus._handlers.get("company.created", [])) > 0
    assert len(event_bus._handlers.get("contact.created", [])) > 0

    # Publish an event and verify the handler is called
    await event_bus.publish("company.created", {"company_id": "test-123", "name": "Test Corp"})
    # Give async tasks a moment to complete
    await asyncio.sleep(0.1)

    # Check that the plugin's event handler was called
    assert len(plugin.event_log) > 0
    assert plugin.event_log[0]["event"] == "company.created"


# ─── AC9: Plugin deactivation unregisters event listeners ───


@pytest.mark.asyncio
async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_client: AsyncClient):
    """AC9: Deactivating a plugin unregisters its event listeners from the event bus."""
    registry = get_registry()
    plugin = registry.get_plugin("test_sample")
    assert plugin is not None

    # Install, activate, then deactivate
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER)

    event_bus = get_event_bus()

    # Publish an event — handler should NOT be called after deactivation
    initial_log_count = len(plugin.event_log)
    await event_bus.publish(
        "company.created", {"company_id": "test-456", "name": "After Deactivate"}
    )
    await asyncio.sleep(0.1)

    # Event log should not have grown
    assert (
        len(plugin.event_log) == initial_log_count
    ), "Event handler should not be called after deactivation"

    # Check event bus no longer has the plugin's handlers
    handlers = event_bus._handlers.get("company.created", [])
    # The specific handler reference should be removed
    for handler in handlers:
        assert handler not in plugin._event_handlers.values()


# ─── AC10: Plugin migration creates tables with tenant_id column ───


@pytest.mark.asyncio
async def test_ac10_migration_creates_tenant_id(
    authed_plugin_client: AsyncClient, engine: AsyncEngine
):
    """AC10: Plugin migration creates tables that have tenant_id column."""
    # Install the test_sample plugin which has a migration creating plugin_test_data
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200

    # Verify the table has tenant_id column
    def _get_columns(sync_conn):
        insp = inspect(sync_conn)
        if "plugin_test_data" not in insp.get_table_names():
            return None
        return [col["name"] for col in insp.get_columns("plugin_test_data")]

    async with engine.connect() as conn:
        columns = await conn.run_sync(_get_columns)

    assert columns is not None, "plugin_test_data table should exist"
    assert "tenant_id" in columns, "plugin_test_data table must have tenant_id column"


# ─── AC11: Plugin migration validator rejects tables without tenant_id ───


@pytest.mark.asyncio
async def test_ac11_validator_rejects_no_tenant_id(authed_plugin_client: AsyncClient):
    """AC11: Migration validator rejects tables created without tenant_id column."""
    # Try to install the bad_migration_plugin — should fail with 422
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/bad_migration_plugin/install", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 422
    data = resp.json()
    # detail is a dict with {detail: str, code: str}
    detail = data.get("detail", {})
    if isinstance(detail, dict):
        detail_text = detail.get("detail", "").lower()
        detail_code = detail.get("code", "")
    else:
        detail_text = str(detail).lower()
        detail_code = data.get("code", "")
    assert "tenant_id" in detail_text or "migration_validation" in detail_code


# ─── AC12: Plugin DB migrations tracked in plugin_migrations table ───


@pytest.mark.asyncio
async def test_ac12_migrations_tracked(
    authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
    """AC12: Plugin migrations are tracked in plugin_migrations table."""
    # Install the test_sample plugin
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200

    # Check plugin_migrations table has a record
    result = await db_session_for_plugins.execute(
        select(PluginMigration).where(PluginMigration.plugin_name == "test_sample")
    )
    migrations = result.scalars().all()
    assert len(migrations) > 0, "plugin_migrations table should have a record for test_sample"
    assert migrations[0].migration_file == "0001_test_plugin.sql"
    assert migrations[0].status == "applied"


# ─── AC13: Activating already-active plugin → idempotent (200, no error) ───


@pytest.mark.asyncio
async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient):
    """AC13: Activating an already-active plugin is idempotent (returns 200, no error)."""
    # Install and activate
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == "active"

    # Activate again — should be idempotent
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "active"
    assert data["active"] is True
    assert "already active" in data["message"].lower()


# ─── AC14: Deactivating inactive plugin → idempotent (200) ───


@pytest.mark.asyncio
async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClient):
    """AC14: Deactivating an already-inactive plugin is idempotent (returns 200, no error)."""
    # Install and activate
    await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
    await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)

    # Deactivate
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == "inactive"

    # Deactivate again — should be idempotent
    resp = await authed_plugin_client.post(
        "/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "inactive"
    assert data["active"] is False
    assert "already inactive" in data["message"].lower()


# ─── Additional Tests: Direct MigrationRunner Unit Tests ───


@pytest.mark.asyncio
async def test_registry_discover_builtins(engine: AsyncEngine):
    """Test that registry can discover built-in plugins."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    discovered = registry.discover_builtins()
    # test_sample should be discovered from builtins package
    assert "test_sample" in discovered
    assert registry.get_plugin("test_sample") is not None


@pytest.mark.asyncio
async def test_registry_list_plugins_mixed_states(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test listing plugins in various states (discovered, installed, active, inactive)."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    # List before install — all discovered
    plugins = await registry.list_plugins(db_session_for_plugins)
    assert len(plugins) == 1
    assert plugins[0]["status"] == "discovered"
    assert plugins[0]["installed"] is False

    # Install
    await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    # List after install — installed
    plugins = await registry.list_plugins(db_session_for_plugins)
    assert len(plugins) == 1
    assert plugins[0]["status"] == "installed"
    assert plugins[0]["installed"] is True

    # Activate
    await registry.activate(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    # List after activate — active
    plugins = await registry.list_plugins(db_session_for_plugins)
    assert plugins[0]["status"] == "active"
    assert plugins[0]["active"] is True


@pytest.mark.asyncio
async def test_registry_install_idempotent(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test that installing an already-installed plugin is idempotent."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    record1 = await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    record2 = await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    # Same record returned
    assert record1.id == record2.id


@pytest.mark.asyncio
async def test_registry_not_found_errors(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
    """Test registry raises ValueError for unknown plugins."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)

    with pytest.raises(ValueError, match="not found"):
        await registry.install(db_session_for_plugins, "nonexistent")

    with pytest.raises(ValueError, match="not found"):
        await registry.activate(db_session_for_plugins, "nonexistent")

    with pytest.raises(ValueError, match="not found"):
        await registry.deactivate(db_session_for_plugins, "nonexistent")

    with pytest.raises(ValueError, match="not found"):
        await registry.uninstall(db_session_for_plugins, "nonexistent")


@pytest.mark.asyncio
async def test_registry_activate_without_install(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test that activating without install raises error."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    with pytest.raises(ValueError, match="not installed"):
        await registry.activate(db_session_for_plugins, "test_sample")


@pytest.mark.asyncio
async def test_migration_runner_drop_tables(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test MigrationRunner.drop_plugin_tables removes tables and migration records."""
    runner = MigrationRunner(engine)

    # Run migration first
    await runner.run_migration(db_session_for_plugins, "drop_test", "0001_test_plugin.sql")
    await db_session_for_plugins.commit()

    # Verify table exists
    result = await db_session_for_plugins.execute(
        text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_test_data'")
    )
    assert result.fetchone() is not None

    # Drop tables
    dropped = await runner.drop_plugin_tables(db_session_for_plugins, "drop_test")
    await db_session_for_plugins.commit()

    assert "plugin_test_data" in dropped

    # Verify table is gone
    result = await db_session_for_plugins.execute(
        text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_test_data'")
    )
    assert result.fetchone() is None

    # Verify migration records are removed
    result = await db_session_for_plugins.execute(
        select(PluginMigration).where(PluginMigration.plugin_name == "drop_test")
    )
    assert result.scalars().all() == []


@pytest.mark.asyncio
async def test_migration_runner_file_not_found(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test MigrationRunner raises FileNotFoundError for missing migration file."""
    runner = MigrationRunner(engine)
    with pytest.raises(FileNotFoundError):
        await runner.run_migration(db_session_for_plugins, "test", "nonexistent.sql")


def test_migration_runner_split_sql():
    """Test SQL splitting logic."""
    sql = "CREATE TABLE a();\nCREATE INDEX idx ON a();\n"
    statements = MigrationRunner._split_sql(sql)
    assert len(statements) == 2

    # Test with comments
    sql2 = "-- comment\nCREATE TABLE b();\n"
    statements2 = MigrationRunner._split_sql(sql2)
    assert len(statements2) == 1

    # Test empty SQL
    assert MigrationRunner._split_sql("") == []


def test_migration_runner_extract_table_names():
    """Test table name extraction from SQL."""
    sql = "CREATE TABLE foo (); CREATE TABLE IF NOT EXISTS bar ();"
    names = MigrationRunner._extract_table_names(sql)
    assert "foo" in names
    assert "bar" in names


def test_plugin_manifest_validation():
    """Test PluginManifest field validation."""
    # Valid manifest
    m = PluginManifest(
        name="my_plugin",
        version="1.0.0",
        display_name="My Plugin",
    )
    assert m.name == "my_plugin"
    assert m.dependencies == []
    assert m.routes == []

    # Name is lowercased
    m2 = PluginManifest(name="MyPlugin", version="1.0.0", display_name="Test")
    assert m2.name == "myplugin"

    # Invalid name with special chars
    with pytest.raises(Exception):  # noqa: B017
        PluginManifest(name="my-plugin!", version="1.0.0", display_name="Test")


def test_base_plugin_repr():
    """Test BasePlugin __repr__ method."""
    plugin = TestSamplePlugin()
    repr_str = repr(plugin)
    assert "test_sample" in repr_str
    assert "1.0.0" in repr_str


def test_base_plugin_no_manifest_error():
    """Test that BasePlugin without manifest raises ValueError."""

    class NoManifestPlugin(BasePlugin):
        pass

    with pytest.raises(ValueError, match="manifest"):
        NoManifestPlugin()


def test_base_plugin_name_version_properties():
    """Test BasePlugin name and version properties."""
    plugin = TestSamplePlugin()
    assert plugin.name == "test_sample"
    assert plugin.version == "1.0.0"


@pytest.mark.asyncio
async def test_base_plugin_on_install_default():
    """Test default on_install returns None (no-op)."""

    class MinimalPlugin(BasePlugin):
        manifest = PluginManifest(
            name="minimal",
            version="1.0.0",
            display_name="Minimal",
        )

    plugin = MinimalPlugin()
    result = await plugin.on_install(None, None)
    assert result is None


@pytest.mark.asyncio
async def test_base_plugin_on_uninstall_default():
    """Test default on_uninstall returns None (no-op)."""

    class MinimalPlugin(BasePlugin):
        manifest = PluginManifest(
            name="minimal2",
            version="1.0.0",
            display_name="Minimal",
        )

    plugin = MinimalPlugin()
    result = await plugin.on_uninstall(None, None)
    assert result is None


def test_base_plugin_get_routes_empty():
    """Test get_routes returns empty list when manifest has no routes."""
    plugin = TestSamplePlugin()
    routes = plugin.get_routes()
    assert routes == []


def test_base_plugin_make_event_handler_noop():
    """Test that _make_event_handler creates noop for unknown events."""

    class MinimalPlugin(BasePlugin):
        manifest = PluginManifest(
            name="minimal3",
            version="1.0.0",
            display_name="Minimal",
            events=["unknown.event"],
        )

    plugin = MinimalPlugin()
    handler = plugin._make_event_handler("unknown.event")
    assert handler is not None
    # It should be callable
    assert callable(handler)


@pytest.mark.asyncio
async def test_service_manifest_schema():
    """Test plugin service get_manifest_schema returns dict."""
    from app.services.plugin_service import PluginService

    service = PluginService()
    schema = service.get_manifest_schema()
    assert isinstance(schema, dict)
    assert "fields" in schema
    assert "example" in schema


@pytest.mark.asyncio
async def test_uninstall_inactive_plugin(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
    """Test uninstalling a plugin that is installed but not active."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    # Install only (not activated)
    await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    # Uninstall should work without deactivation
    record = await registry.uninstall(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()
    assert record.status == "uninstalled"


@pytest.mark.asyncio
async def test_registry_activate_with_app_routes(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test that activating a plugin with a FastAPI app registers routes."""
    from fastapi import APIRouter, FastAPI

    from app.plugins.manifest import PluginManifest

    # Create a plugin with a route
    class RoutePlugin(BasePlugin):
        manifest = PluginManifest(
            name="route_plugin",
            version="1.0.0",
            display_name="Route Plugin",
            events=["test.event"],
        )

        def get_routes(self) -> list:
            test_router = APIRouter()

            @test_router.get("/api/v1/route-plugin/test")
            async def test_endpoint():
                return {"status": "ok"}

            return [test_router]

    app = FastAPI()
    registry = reset_registry_for_testing()
    registry.initialize(engine, app)
    registry.register_plugin(RoutePlugin())

    # Install and activate
    await registry.install(db_session_for_plugins, "route_plugin")
    await db_session_for_plugins.commit()
    await registry.activate(db_session_for_plugins, "route_plugin")
    await db_session_for_plugins.commit()

    # Verify route was mounted
    route_paths = [r.path for r in app.router.routes]
    assert "/api/v1/route-plugin/test" in route_paths

    # Deactivate — route should be unmounted
    await registry.deactivate(db_session_for_plugins, "route_plugin")
    await db_session_for_plugins.commit()
    route_paths_after = [r.path for r in app.router.routes]
    assert "/api/v1/route-plugin/test" not in route_paths_after


@pytest.mark.asyncio
async def test_registry_uninstall_with_remove_data(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test registry uninstall with remove_data drops tables."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    record = await registry.uninstall(db_session_for_plugins, "test_sample", remove_data=True)
    await db_session_for_plugins.commit()
    assert record.status == "uninstalled"
    assert "plugin_test_data" in getattr(record, "dropped_tables", [])


@pytest.mark.asyncio
async def test_registry_not_initialized_errors():
    """Test that uninitialized registry raises RuntimeError."""
    registry = reset_registry_for_testing()
    with pytest.raises(RuntimeError, match="not initialized"):
        _ = registry.migration_runner
    with pytest.raises(RuntimeError, match="not initialized"):
        _ = registry.engine


@pytest.mark.asyncio
async def test_registry_deactivate_already_inactive_direct(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test registry.deactivate is idempotent when already inactive."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())

    await registry.install(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()

    # Deactivate without activating first — status is 'installed', not 'inactive'
    # But calling deactivate should still work (sets to inactive)
    record = await registry.deactivate(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()
    assert record.status == "inactive"

    # Deactivate again — idempotent
    record2 = await registry.deactivate(db_session_for_plugins, "test_sample")
    await db_session_for_plugins.commit()
    assert record2.status == "inactive"


@pytest.mark.asyncio
async def test_plugin_service_install_error_handling(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test plugin service raises ValueError for unknown plugin."""
    from app.services.plugin_service import PluginService

    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    service = PluginService(registry=registry)

    with pytest.raises(ValueError, match="not found"):
        await service.install_plugin(db_session_for_plugins, "nonexistent")


@pytest.mark.asyncio
async def test_plugin_service_activate_error_handling(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test plugin service raises ValueError when activating uninstalled plugin."""
    from app.services.plugin_service import PluginService

    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    registry.register_plugin(TestSamplePlugin())
    service = PluginService(registry=registry)

    with pytest.raises(ValueError, match="not installed"):
        await service.activate_plugin(db_session_for_plugins, "test_sample")


@pytest.mark.asyncio
async def test_plugin_service_deactivate_error_handling(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test plugin service raises ValueError for deactivating unknown plugin."""
    from app.services.plugin_service import PluginService

    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    service = PluginService(registry=registry)

    with pytest.raises(ValueError, match="not found"):
        await service.deactivate_plugin(db_session_for_plugins, "nonexistent")


@pytest.mark.asyncio
async def test_plugin_service_uninstall_error_handling(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test plugin service raises ValueError for uninstalling unknown plugin."""
    from app.services.plugin_service import PluginService

    registry = reset_registry_for_testing()
    registry.initialize(engine, None)
    service = PluginService(registry=registry)

    with pytest.raises(ValueError, match="not found"):
        await service.uninstall_plugin(db_session_for_plugins, "nonexistent")


@pytest.mark.asyncio
async def test_migration_runner_split_sql_dollar_quotes():
    """Test SQL splitting with dollar-quoted blocks."""
    sql = "CREATE FUNCTION foo() RETURNS void AS $$\nBEGIN\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TABLE bar();\n"
    statements = MigrationRunner._split_sql(sql)
    assert len(statements) >= 2


@pytest.mark.asyncio
async def test_registry_list_plugins_db_only_record(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test listing plugins includes DB-only records (installed but not discovered)."""
    registry = reset_registry_for_testing()
    registry.initialize(engine, None)

    # Manually create a DB record without registering the plugin
    record = PluginModel(
        name="orphan_plugin",
        display_name="Orphan Plugin",
        version="0.1.0",
        status="installed",
        installed=True,
        active=False,
    )
    db_session_for_plugins.add(record)
    await db_session_for_plugins.commit()

    plugins = await registry.list_plugins(db_session_for_plugins)
    names = [p["name"] for p in plugins]
    assert "orphan_plugin" in names
    orphan = [p for p in plugins if p["name"] == "orphan_plugin"][0]
    assert orphan["installed"] is True


@pytest.mark.asyncio
async def test_base_plugin_on_event_fallback():
    """Test that on_event fallback handler works."""

    class FallbackPlugin(BasePlugin):
        manifest = PluginManifest(
            name="fallback",
            version="1.0.0",
            display_name="Fallback",
            events=["custom.event"],
        )

        async def on_event(self, payload: dict[str, Any]) -> None:
            self.event_received = payload

    plugin = FallbackPlugin()
    handler = plugin._make_event_handler("custom.event")
    await handler({"test": True})
    assert plugin.event_received == {"test": True}


@pytest.mark.asyncio
async def test_migration_runner_valid_migration(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test MigrationRunner directly with a valid migration (has tenant_id)."""
    runner = MigrationRunner(engine)
    record = await runner.run_migration(
        db_session_for_plugins,
        "test_plugin",
        "0001_test_plugin.sql",
    )
    await db_session_for_plugins.commit()
    assert record.plugin_name == "test_plugin"
    assert record.migration_file == "0001_test_plugin.sql"
    assert record.status == "applied"


@pytest.mark.asyncio
async def test_migration_runner_invalid_migration(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test MigrationRunner directly rejects migration without tenant_id."""
    runner = MigrationRunner(engine)
    with pytest.raises(MigrationValidationError) as exc_info:
        await runner.run_migration(
            db_session_for_plugins,
            "bad_plugin",
            "0001_bad_migration.sql",
        )
    assert "tenant_id" in str(exc_info.value).lower()


@pytest.mark.asyncio
async def test_migration_runner_idempotent(
    engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
    """Test MigrationRunner skips already-applied migrations."""
    runner = MigrationRunner(engine)
    # Run migration
    await runner.run_migration(db_session_for_plugins, "test_idempotent", "0001_test_plugin.sql")
    await db_session_for_plugins.commit()

    # Run all migrations again — should skip
    records = await runner.run_all_migrations(
        db_session_for_plugins, "test_idempotent", ["0001_test_plugin.sql"]
    )
    assert len(records) == 0, "Already-applied migration should be skipped"
