use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::str::FromStr;
use std::sync::OnceLock;
use uuid::Uuid;

pub const PLUGIN_REGISTRY_SCHEMA: &str = "cephas.plugins.v1";
pub const PLUGIN_MODULE_SCHEMA: &str = "cephas.plugin-module.v1";
pub const PLUGIN_CHECK_SCHEMA: &str = "cephas.plugins.check.v1";
pub const PLUGIN_BACKEND_REGISTRY_SCHEMA: &str = "cephas.plugins-backend.registry.v1";
pub const PLUGIN_BACKEND_INGEST_REPORT_SCHEMA: &str = "cephas.plugins-backend.ingest-report.v1";
pub const DEFAULT_MAX_PLUGINS: usize = 128;

const MAX_PLUGIN_NAME_CHARS: usize = 80;
const MAX_PLUGIN_DESCRIPTION_CHARS: usize = 320;
const MAX_PLUGIN_VERSION_CHARS: usize = 32;
const MAX_PLUGIN_MODULE_ID_CHARS: usize = 64;
const MAX_PLUGIN_SOURCE_PATH_CHARS: usize = 160;

static BUILT_IN_PLUGINS: OnceLock<Vec<PluginManifest>> = OnceLock::new();
static BUNDLED_PLUGIN_MODULES: OnceLock<Vec<PluginModule>> = OnceLock::new();

const BUNDLED_PLUGIN_MANIFESTS: &[&str] = &[
    include_str!("../plugins/reader-outline/plugin.json"),
    include_str!("../plugins/media-compat/plugin.json"),
    include_str!("../plugins/paper-theme-tools/plugin.json"),
    include_str!("../plugins/agent-toolkit/plugin.json"),
    include_str!("../plugins/privacy-rules/plugin.json"),
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PluginId(Uuid);

impl PluginId {
    pub fn new_v7() -> Self {
        Self(Uuid::now_v7())
    }

    pub fn as_uuid(&self) -> Uuid {
        self.0
    }

    pub fn is_v7(&self) -> bool {
        self.0.get_version_num() == 7
    }
}

impl fmt::Display for PluginId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

impl FromStr for PluginId {
    type Err = uuid::Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Uuid::parse_str(value).map(Self)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PluginKind {
    ContentBlocker,
    Theme,
    Tool,
    Privacy,
    Media,
    Agent,
    Reader,
}

impl PluginKind {
    pub fn label(self) -> &'static str {
        match self {
            Self::ContentBlocker => "Content blocker",
            Self::Theme => "Theme",
            Self::Tool => "Tool",
            Self::Privacy => "Privacy",
            Self::Media => "Media",
            Self::Agent => "Agent",
            Self::Reader => "Reader",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PluginCapability {
    ContentScripts,
    StyleSheets,
    ThemeAssets,
    NetworkFiltering,
    ReaderArtifacts,
    MediaHelpers,
    AgentHooks,
    Downloads,
    Tabs,
}

impl PluginCapability {
    pub fn label(self) -> &'static str {
        match self {
            Self::ContentScripts => "content scripts",
            Self::StyleSheets => "style sheets",
            Self::ThemeAssets => "theme assets",
            Self::NetworkFiltering => "network filtering",
            Self::ReaderArtifacts => "reader artifacts",
            Self::MediaHelpers => "media helpers",
            Self::AgentHooks => "agent hooks",
            Self::Downloads => "downloads",
            Self::Tabs => "tabs",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PluginPermission {
    ReadActiveTab,
    ModifyPages,
    NetworkRequests,
    StoreLocalData,
    CaptureMedia,
    UseAgentContext,
}

impl PluginPermission {
    pub fn label(self) -> &'static str {
        match self {
            Self::ReadActiveTab => "read active tab",
            Self::ModifyPages => "modify pages",
            Self::NetworkRequests => "network requests",
            Self::StoreLocalData => "store local data",
            Self::CaptureMedia => "capture media",
            Self::UseAgentContext => "use Agent context",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PluginStatus {
    Planned,
    Installed,
    Enabled,
    Disabled,
    Blocked,
}

impl PluginStatus {
    pub fn label(self) -> &'static str {
        match self {
            Self::Planned => "Planned",
            Self::Installed => "Installed",
            Self::Enabled => "Enabled",
            Self::Disabled => "Disabled",
            Self::Blocked => "Blocked",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
    pub id: PluginId,
    pub name: String,
    pub description: String,
    pub version: String,
    pub kind: PluginKind,
    pub status: PluginStatus,
    pub built_in: bool,
    #[serde(default)]
    pub capabilities: Vec<PluginCapability>,
    #[serde(default)]
    pub permissions: Vec<PluginPermission>,
    pub installed_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginModule {
    pub schema: String,
    pub module_id: String,
    pub source_path: String,
    pub manifest: PluginManifest,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PluginCheckReport {
    pub schema: &'static str,
    pub ok: bool,
    pub installed_count: usize,
    pub enabled_count: usize,
    pub planned_count: usize,
    pub bundled_count: usize,
    pub available_count: usize,
    pub max_plugins: usize,
    pub error_count: usize,
    pub warning_count: usize,
    pub issues: Vec<PluginCheckIssue>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PluginCheckSeverity {
    Error,
    Warning,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PluginCheckIssue {
    pub severity: PluginCheckSeverity,
    pub code: String,
    pub plugin_id: Option<String>,
    pub module_id: Option<String>,
    pub message: String,
}

impl PluginManifest {
    pub fn local(
        name: impl Into<String>,
        description: impl Into<String>,
        version: impl Into<String>,
        kind: PluginKind,
        capabilities: Vec<PluginCapability>,
        permissions: Vec<PluginPermission>,
    ) -> Self {
        let now = Utc::now();
        Self {
            id: PluginId::new_v7(),
            name: name.into(),
            description: description.into(),
            version: version.into(),
            kind,
            status: PluginStatus::Installed,
            built_in: false,
            capabilities,
            permissions,
            installed_at: now,
            updated_at: now,
        }
        .sanitized()
    }

    fn planned_builtin(
        id: &'static str,
        name: &'static str,
        description: &'static str,
        kind: PluginKind,
        capabilities: Vec<PluginCapability>,
        permissions: Vec<PluginPermission>,
    ) -> Self {
        let timestamp = DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
            .expect("valid built-in plugin timestamp")
            .with_timezone(&Utc);
        Self {
            id: PluginId(Uuid::parse_str(id).expect("built-in plugin UUID must be valid")),
            name: name.to_string(),
            description: description.to_string(),
            version: "0.1.0".to_string(),
            kind,
            status: PluginStatus::Planned,
            built_in: true,
            capabilities,
            permissions,
            installed_at: timestamp,
            updated_at: timestamp,
        }
        .sanitized()
    }

    fn sanitized(mut self) -> Self {
        self.name = clipped_non_empty(&self.name, MAX_PLUGIN_NAME_CHARS, "Untitled plugin");
        self.description = clipped_non_empty(
            &self.description,
            MAX_PLUGIN_DESCRIPTION_CHARS,
            "No description provided.",
        );
        self.version = clipped_non_empty(&self.version, MAX_PLUGIN_VERSION_CHARS, "0.0.0");
        dedupe_sorted(&mut self.capabilities);
        dedupe_sorted(&mut self.permissions);
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginRegistry {
    #[serde(default = "default_plugins_enabled")]
    pub enabled: bool,
    #[serde(default = "built_in_plugins")]
    pub installed: Vec<PluginManifest>,
    #[serde(default = "default_max_plugins")]
    pub max_plugins: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginBackendRegistry {
    pub schema: String,
    pub generated_at: DateTime<Utc>,
    pub registry_version: u64,
    pub plugins: Vec<PluginManifest>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PluginBackendIngestReport {
    pub schema: &'static str,
    pub backend_schema: String,
    pub registry_version: u64,
    pub received: usize,
    pub accepted: usize,
    pub updated: usize,
    pub rejected: Vec<PluginBackendRejection>,
    pub installed_count: usize,
    pub max_plugins: usize,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PluginBackendRejection {
    pub id: String,
    pub name: String,
    pub reason: String,
}

impl Default for PluginRegistry {
    fn default() -> Self {
        Self {
            enabled: true,
            installed: built_in_plugins(),
            max_plugins: DEFAULT_MAX_PLUGINS,
        }
    }
}

impl PluginRegistry {
    pub fn enforce_limits(&mut self) {
        let minimum = built_in_plugins().len();
        self.max_plugins = self.max_plugins.clamp(minimum, DEFAULT_MAX_PLUGINS);
        self.ensure_builtin_plugins();

        let mut seen = BTreeSet::new();
        self.installed
            .retain(|plugin| plugin.id.is_v7() && seen.insert(plugin.id));
        self.installed = self
            .installed
            .drain(..)
            .map(PluginManifest::sanitized)
            .collect();

        while self.installed.len() > self.max_plugins {
            let Some(index) = self
                .installed
                .iter()
                .enumerate()
                .filter(|(_, plugin)| !plugin.built_in)
                .min_by_key(|(_, plugin)| plugin.installed_at)
                .map(|(index, _)| index)
            else {
                break;
            };
            self.installed.remove(index);
        }
    }

    pub fn register(&mut self, manifest: PluginManifest) -> Result<PluginId, PluginRegistryError> {
        if self.installed.iter().any(|plugin| plugin.id == manifest.id) {
            return Err(PluginRegistryError::DuplicateId(manifest.id));
        }
        let id = manifest.id;
        self.installed.push(manifest.sanitized());
        self.enforce_limits();
        Ok(id)
    }

    pub fn install_bundled(
        &mut self,
        plugin_id: PluginId,
    ) -> Result<PluginManifest, PluginRegistryError> {
        if self.installed.iter().any(|plugin| plugin.id == plugin_id) {
            return Err(PluginRegistryError::DuplicateId(plugin_id));
        }

        let mut manifest = bundled_plugin_manifest(plugin_id)
            .ok_or(PluginRegistryError::UnknownBundledPlugin(plugin_id))?;
        manifest.built_in = false;
        manifest.status = PluginStatus::Installed;
        manifest.installed_at = Utc::now();
        manifest.updated_at = manifest.installed_at;
        self.register(manifest.clone())?;
        Ok(manifest)
    }

    pub fn install_all_available_bundled(
        &mut self,
    ) -> Result<Vec<PluginManifest>, PluginRegistryError> {
        let available = available_bundled_plugin_modules(self);
        let mut installed = Vec::with_capacity(available.len());

        for module in available {
            installed.push(self.install_bundled(module.manifest.id)?);
        }

        Ok(installed)
    }

    pub fn sync_bundled(
        &mut self,
        plugin_id: PluginId,
    ) -> Result<PluginManifest, PluginRegistryError> {
        let existing = self
            .installed
            .iter_mut()
            .find(|plugin| plugin.id == plugin_id)
            .ok_or(PluginRegistryError::NotInstalled(plugin_id))?;
        if existing.built_in {
            return Err(PluginRegistryError::BuiltInImmutable(plugin_id));
        }

        let mut manifest = bundled_plugin_manifest(plugin_id)
            .ok_or(PluginRegistryError::UnknownBundledPlugin(plugin_id))?;
        manifest.built_in = false;
        manifest.status = existing.status;
        manifest.installed_at = existing.installed_at;
        manifest.updated_at = Utc::now();
        *existing = manifest.sanitized();
        Ok(existing.clone())
    }

    pub fn sync_all_installed_bundled(
        &mut self,
    ) -> Result<Vec<PluginManifest>, PluginRegistryError> {
        let plugin_ids = self
            .installed
            .iter()
            .filter(|plugin| !plugin.built_in && bundled_plugin_manifest(plugin.id).is_some())
            .map(|plugin| plugin.id)
            .collect::<Vec<_>>();
        let mut synced = Vec::with_capacity(plugin_ids.len());

        for plugin_id in plugin_ids {
            synced.push(self.sync_bundled(plugin_id)?);
        }

        Ok(synced)
    }

    pub fn set_status(
        &mut self,
        plugin_id: PluginId,
        status: PluginStatus,
    ) -> Result<PluginManifest, PluginRegistryError> {
        let plugin = self
            .installed
            .iter_mut()
            .find(|plugin| plugin.id == plugin_id)
            .ok_or(PluginRegistryError::NotInstalled(plugin_id))?;
        if plugin.built_in {
            return Err(PluginRegistryError::BuiltInImmutable(plugin_id));
        }
        if plugin.status == PluginStatus::Blocked && status == PluginStatus::Enabled {
            return Err(PluginRegistryError::Blocked(plugin_id));
        }

        plugin.status = status;
        plugin.updated_at = Utc::now();
        Ok(plugin.clone())
    }

    pub fn remove(&mut self, plugin_id: PluginId) -> Result<PluginManifest, PluginRegistryError> {
        let index = self
            .installed
            .iter()
            .position(|plugin| plugin.id == plugin_id)
            .ok_or(PluginRegistryError::NotInstalled(plugin_id))?;
        if self.installed[index].built_in {
            return Err(PluginRegistryError::BuiltInImmutable(plugin_id));
        }

        Ok(self.installed.remove(index))
    }

    pub fn is_installed(&self, plugin_id: PluginId) -> bool {
        self.installed.iter().any(|plugin| plugin.id == plugin_id)
    }

    pub fn ingest_backend_registry(
        &mut self,
        backend: PluginBackendRegistry,
    ) -> Result<PluginBackendIngestReport, PluginBackendIngestError> {
        if backend.schema != PLUGIN_BACKEND_REGISTRY_SCHEMA {
            return Err(PluginBackendIngestError::UnsupportedSchema {
                expected: PLUGIN_BACKEND_REGISTRY_SCHEMA,
                actual: backend.schema,
            });
        }
        if backend.plugins.len() > DEFAULT_MAX_PLUGINS {
            return Err(PluginBackendIngestError::RegistryTooLarge {
                max: DEFAULT_MAX_PLUGINS,
                actual: backend.plugins.len(),
            });
        }

        let mut report = PluginBackendIngestReport {
            schema: PLUGIN_BACKEND_INGEST_REPORT_SCHEMA,
            backend_schema: backend.schema,
            registry_version: backend.registry_version,
            received: backend.plugins.len(),
            accepted: 0,
            updated: 0,
            rejected: Vec::new(),
            installed_count: 0,
            max_plugins: self.max_plugins,
        };
        let built_in_ids = built_in_plugin_ids();

        for mut plugin in backend.plugins {
            if !plugin.id.is_v7() {
                report
                    .rejected
                    .push(plugin_rejection(&plugin, "plugin id must be UUIDv7"));
                continue;
            }
            if plugin.built_in {
                report.rejected.push(plugin_rejection(
                    &plugin,
                    "backend plugins cannot set built_in=true",
                ));
                continue;
            }
            if built_in_ids.contains(&plugin.id) {
                report
                    .rejected
                    .push(plugin_rejection(&plugin, "plugin id is reserved by Cephas"));
                continue;
            }

            plugin.built_in = false;
            if plugin.status == PluginStatus::Enabled {
                plugin.status = PluginStatus::Installed;
            }

            if let Some(existing) = self
                .installed
                .iter_mut()
                .find(|installed| installed.id == plugin.id)
            {
                let local_status = existing.status;
                let installed_at = existing.installed_at;
                plugin.status = local_status;
                plugin.installed_at = installed_at;
                *existing = plugin.sanitized();
                report.updated += 1;
            } else {
                self.installed.push(plugin.sanitized());
                report.accepted += 1;
            }
        }

        self.enforce_limits();
        report.installed_count = self.installed_count();
        report.max_plugins = self.max_plugins;
        Ok(report)
    }

    pub fn installed_count(&self) -> usize {
        self.installed.len()
    }

    pub fn enabled_count(&self) -> usize {
        self.installed
            .iter()
            .filter(|plugin| plugin.status == PluginStatus::Enabled)
            .count()
    }

    pub fn planned_count(&self) -> usize {
        self.installed
            .iter()
            .filter(|plugin| plugin.status == PluginStatus::Planned)
            .count()
    }

    fn ensure_builtin_plugins(&mut self) {
        let builtins = built_in_plugins();
        let builtin_ids = builtins
            .iter()
            .map(|plugin| plugin.id)
            .collect::<BTreeSet<_>>();
        self.installed
            .retain(|plugin| !builtin_ids.contains(&plugin.id));
        self.installed.extend(builtins);
    }
}

fn plugin_rejection(plugin: &PluginManifest, reason: impl Into<String>) -> PluginBackendRejection {
    PluginBackendRejection {
        id: plugin.id.to_string(),
        name: clipped_non_empty(&plugin.name, MAX_PLUGIN_NAME_CHARS, "Untitled plugin"),
        reason: reason.into(),
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginRegistryError {
    DuplicateId(PluginId),
    UnknownBundledPlugin(PluginId),
    NotInstalled(PluginId),
    BuiltInImmutable(PluginId),
    Blocked(PluginId),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginBackendIngestError {
    UnsupportedSchema {
        expected: &'static str,
        actual: String,
    },
    RegistryTooLarge {
        max: usize,
        actual: usize,
    },
}

impl fmt::Display for PluginRegistryError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DuplicateId(id) => write!(formatter, "plugin {id} is already installed"),
            Self::UnknownBundledPlugin(id) => write!(formatter, "plugin {id} is not bundled"),
            Self::NotInstalled(id) => write!(formatter, "plugin {id} is not installed"),
            Self::BuiltInImmutable(id) => {
                write!(formatter, "built-in plugin {id} cannot be changed")
            }
            Self::Blocked(id) => write!(formatter, "plugin {id} is blocked"),
        }
    }
}

impl std::error::Error for PluginRegistryError {}

impl fmt::Display for PluginBackendIngestError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsupportedSchema { expected, actual } => {
                write!(
                    formatter,
                    "unsupported plugin backend schema {actual}; expected {expected}"
                )
            }
            Self::RegistryTooLarge { max, actual } => {
                write!(
                    formatter,
                    "plugin backend registry has {actual} plugins; maximum is {max}"
                )
            }
        }
    }
}

impl std::error::Error for PluginBackendIngestError {}

fn default_plugins_enabled() -> bool {
    true
}

fn default_max_plugins() -> usize {
    DEFAULT_MAX_PLUGINS
}

fn built_in_plugins() -> Vec<PluginManifest> {
    BUILT_IN_PLUGINS.get_or_init(build_built_in_plugins).clone()
}

pub fn bundled_plugin_modules() -> Vec<PluginModule> {
    BUNDLED_PLUGIN_MODULES
        .get_or_init(load_bundled_plugin_modules)
        .clone()
}

pub fn bundled_plugin_manifests() -> Vec<PluginManifest> {
    bundled_plugin_modules()
        .into_iter()
        .map(|module| module.manifest)
        .collect()
}

pub fn available_bundled_plugin_modules(registry: &PluginRegistry) -> Vec<PluginModule> {
    bundled_plugin_modules()
        .into_iter()
        .filter(|module| !registry.is_installed(module.manifest.id))
        .collect()
}

pub fn available_bundled_plugin_manifests(registry: &PluginRegistry) -> Vec<PluginManifest> {
    available_bundled_plugin_modules(registry)
        .into_iter()
        .map(|module| module.manifest)
        .collect()
}

pub fn bundled_plugin_manifest(plugin_id: PluginId) -> Option<PluginManifest> {
    bundled_plugin_manifests()
        .into_iter()
        .find(|manifest| manifest.id == plugin_id)
}

pub fn bundled_plugin_module_for_manifest(plugin_id: PluginId) -> Option<PluginModule> {
    bundled_plugin_modules()
        .into_iter()
        .find(|module| module.manifest.id == plugin_id)
}

pub fn bundled_plugin_id_for_module(module_id: &str) -> Option<PluginId> {
    let module_id = module_id.trim();
    bundled_plugin_modules()
        .into_iter()
        .find(|module| module.module_id == module_id)
        .map(|module| module.manifest.id)
}

pub fn resolve_plugin_selector(selector: &str) -> Option<PluginId> {
    let selector = selector.trim();
    selector
        .parse::<PluginId>()
        .ok()
        .or_else(|| bundled_plugin_id_for_module(selector))
}

pub fn check_plugin_registry(registry: &PluginRegistry) -> PluginCheckReport {
    let bundled = bundled_plugin_modules();
    let available = available_bundled_plugin_modules(registry);
    let mut issues = Vec::new();
    let builtins = built_in_plugins();
    let builtin_ids = builtins
        .iter()
        .map(|plugin| plugin.id)
        .collect::<BTreeSet<_>>();

    if registry.max_plugins < builtins.len() || registry.max_plugins > DEFAULT_MAX_PLUGINS {
        issues.push(plugin_check_issue(
            PluginCheckSeverity::Error,
            "invalid-max-plugins",
            None,
            None,
            format!(
                "max_plugins must be between {} and {}",
                builtins.len(),
                DEFAULT_MAX_PLUGINS
            ),
        ));
    }

    if registry.installed.len() > registry.max_plugins {
        issues.push(plugin_check_issue(
            PluginCheckSeverity::Error,
            "registry-over-capacity",
            None,
            None,
            format!(
                "registry has {} records but max_plugins is {}",
                registry.installed.len(),
                registry.max_plugins
            ),
        ));
    }

    let mut installed_ids = BTreeSet::new();
    for plugin in &registry.installed {
        if !installed_ids.insert(plugin.id) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "duplicate-plugin-id",
                Some(plugin.id),
                None,
                "plugin id appears more than once",
            ));
        }
        if !plugin.id.is_v7() {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "plugin-id-not-uuidv7",
                Some(plugin.id),
                None,
                "plugin id must be UUIDv7",
            ));
        }
        if plugin.built_in && !builtin_ids.contains(&plugin.id) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "unknown-built-in-plugin",
                Some(plugin.id),
                None,
                "built_in=true is reserved for Cephas-owned API records",
            ));
        }
        if plugin.built_in && plugin.status != PluginStatus::Planned {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Warning,
                "built-in-status-not-planned",
                Some(plugin.id),
                None,
                "built-in API records should remain planned",
            ));
        }
    }

    for builtin in &builtins {
        if !registry
            .installed
            .iter()
            .any(|plugin| plugin.id == builtin.id && plugin.built_in)
        {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "missing-built-in-plugin",
                Some(builtin.id),
                None,
                "required Cephas built-in plugin API record is missing",
            ));
        }
    }

    let mut module_ids = BTreeSet::new();
    let mut module_plugin_ids = BTreeSet::new();
    let mut bundled_by_plugin_id = BTreeMap::new();
    for module in &bundled {
        if module.schema != PLUGIN_MODULE_SCHEMA {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "invalid-module-schema",
                Some(module.manifest.id),
                Some(&module.module_id),
                format!("module schema must be {PLUGIN_MODULE_SCHEMA}"),
            ));
        }
        if !valid_bundled_module_id(&module.module_id) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "invalid-module-id",
                Some(module.manifest.id),
                Some(&module.module_id),
                "module_id must be a lowercase ASCII slug",
            ));
        }
        if !valid_bundled_source_path(&module.source_path, &module.module_id) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "invalid-module-source-path",
                Some(module.manifest.id),
                Some(&module.module_id),
                "source_path must be plugins/<module_id>/plugin.json",
            ));
        }
        if !module.manifest.id.is_v7() {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "module-plugin-id-not-uuidv7",
                Some(module.manifest.id),
                Some(&module.module_id),
                "bundled module plugin id must be UUIDv7",
            ));
        }
        if module.manifest.built_in {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "module-claims-built-in",
                Some(module.manifest.id),
                Some(&module.module_id),
                "bundled modules must not set built_in=true",
            ));
        }
        if !module_ids.insert(module.module_id.clone()) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "duplicate-module-id",
                Some(module.manifest.id),
                Some(&module.module_id),
                "module_id appears more than once",
            ));
        }
        if !module_plugin_ids.insert(module.manifest.id) {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Error,
                "duplicate-module-plugin-id",
                Some(module.manifest.id),
                Some(&module.module_id),
                "bundled module plugin id appears more than once",
            ));
        }
        bundled_by_plugin_id.insert(module.manifest.id, module);
    }

    for plugin in &registry.installed {
        if plugin.built_in {
            continue;
        }
        if let Some(module) = bundled_by_plugin_id.get(&plugin.id)
            && plugin_metadata_differs_from_module(plugin, &module.manifest)
        {
            issues.push(plugin_check_issue(
                PluginCheckSeverity::Warning,
                "bundled-metadata-drift",
                Some(plugin.id),
                Some(&module.module_id),
                "installed bundled plugin metadata differs from project module; run plugins sync",
            ));
        }
    }

    let error_count = issues
        .iter()
        .filter(|issue| issue.severity == PluginCheckSeverity::Error)
        .count();
    let warning_count = issues.len().saturating_sub(error_count);

    PluginCheckReport {
        schema: PLUGIN_CHECK_SCHEMA,
        ok: error_count == 0,
        installed_count: registry.installed_count(),
        enabled_count: registry.enabled_count(),
        planned_count: registry.planned_count(),
        bundled_count: bundled.len(),
        available_count: available.len(),
        max_plugins: registry.max_plugins,
        error_count,
        warning_count,
        issues,
    }
}

fn plugin_metadata_differs_from_module(
    installed: &PluginManifest,
    module: &PluginManifest,
) -> bool {
    installed.name != module.name
        || installed.description != module.description
        || installed.version != module.version
        || installed.kind != module.kind
        || installed.capabilities != module.capabilities
        || installed.permissions != module.permissions
}

fn plugin_check_issue(
    severity: PluginCheckSeverity,
    code: &str,
    plugin_id: Option<PluginId>,
    module_id: Option<&str>,
    message: impl Into<String>,
) -> PluginCheckIssue {
    PluginCheckIssue {
        severity,
        code: code.to_string(),
        plugin_id: plugin_id.map(|id| id.to_string()),
        module_id: module_id.map(str::to_string),
        message: message.into(),
    }
}

fn sanitize_plugin_module(mut module: PluginModule) -> PluginModule {
    module.module_id = clipped_non_empty(
        &module.module_id,
        MAX_PLUGIN_MODULE_ID_CHARS,
        "unnamed-module",
    );
    module.source_path = clipped_non_empty(
        &module.source_path,
        MAX_PLUGIN_SOURCE_PATH_CHARS,
        "plugins/unknown/plugin.json",
    );
    module.manifest = module.manifest.sanitized();
    module
}

fn validate_raw_plugin_module(module: &PluginModule) -> Result<(), String> {
    if module.schema != PLUGIN_MODULE_SCHEMA {
        return Err(format!("schema must be {PLUGIN_MODULE_SCHEMA}"));
    }
    if !valid_bundled_module_id(&module.module_id) {
        return Err(
            "module_id must be a lowercase ASCII slug within the module ID cap".to_string(),
        );
    }
    if !valid_bundled_source_path(&module.source_path, &module.module_id) {
        return Err("source_path must be plugins/<module_id>/plugin.json".to_string());
    }
    if module.source_path.chars().count() > MAX_PLUGIN_SOURCE_PATH_CHARS {
        return Err("source_path exceeds the source path cap".to_string());
    }

    let manifest = &module.manifest;
    if !manifest.id.is_v7() {
        return Err("manifest id must be UUIDv7".to_string());
    }
    if !raw_plugin_string_is_valid(&manifest.name, MAX_PLUGIN_NAME_CHARS) {
        return Err("manifest name must be non-empty and within the name cap".to_string());
    }
    if !raw_plugin_string_is_valid(&manifest.description, MAX_PLUGIN_DESCRIPTION_CHARS) {
        return Err(
            "manifest description must be non-empty and within the description cap".to_string(),
        );
    }
    if !raw_plugin_string_is_valid(&manifest.version, MAX_PLUGIN_VERSION_CHARS) {
        return Err("manifest version must be non-empty and within the version cap".to_string());
    }
    if manifest.built_in {
        return Err("bundled modules must not set built_in=true".to_string());
    }
    if manifest.status != PluginStatus::Installed {
        return Err("bundled modules must use installed status in raw metadata".to_string());
    }
    if has_duplicates(&manifest.capabilities) {
        return Err("manifest capabilities must be unique before sanitization".to_string());
    }
    if has_duplicates(&manifest.permissions) {
        return Err("manifest permissions must be unique before sanitization".to_string());
    }
    if manifest.updated_at < manifest.installed_at {
        return Err("manifest updated_at must not be earlier than installed_at".to_string());
    }

    Ok(())
}

fn raw_plugin_string_is_valid(value: &str, max_chars: usize) -> bool {
    let value = value.trim();
    !value.is_empty() && value.chars().count() <= max_chars
}

fn has_duplicates<T: Ord + Copy>(values: &[T]) -> bool {
    let mut seen = BTreeSet::new();
    values.iter().any(|value| !seen.insert(*value))
}

fn valid_bundled_module_id(module_id: &str) -> bool {
    !module_id.is_empty()
        && module_id.len() <= MAX_PLUGIN_MODULE_ID_CHARS
        && module_id
            .bytes()
            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}

fn valid_bundled_source_path(source_path: &str, module_id: &str) -> bool {
    source_path == format!("plugins/{module_id}/plugin.json")
}

fn load_bundled_plugin_modules() -> Vec<PluginModule> {
    let mut seen = BTreeSet::new();
    let mut module_ids = BTreeSet::new();
    BUNDLED_PLUGIN_MANIFESTS
        .iter()
        .map(|json| {
            let mut module: PluginModule =
                serde_json::from_str(json).expect("bundled plugin module JSON must be valid");
            validate_raw_plugin_module(&module).unwrap_or_else(|message| {
                panic!(
                    "bundled plugin module {} has invalid raw metadata: {message}",
                    module.module_id
                )
            });
            module = sanitize_plugin_module(module);
            assert_eq!(
                module.schema, PLUGIN_MODULE_SCHEMA,
                "bundled plugin module schema must match"
            );
            assert!(
                valid_bundled_module_id(&module.module_id),
                "bundled plugin module IDs must be lowercase slug IDs"
            );
            assert!(
                valid_bundled_source_path(&module.source_path, &module.module_id),
                "bundled plugin source paths must point at plugins/<module>/plugin.json"
            );
            assert!(
                module_ids.insert(module.module_id.clone()),
                "bundled plugin module IDs must be unique"
            );
            assert!(
                module.manifest.id.is_v7(),
                "bundled plugin module IDs must be UUIDv7"
            );
            assert!(
                seen.insert(module.manifest.id),
                "bundled plugin module IDs must be unique"
            );
            module.manifest.built_in = false;
            if module.manifest.status == PluginStatus::Enabled {
                module.manifest.status = PluginStatus::Installed;
            }
            module
        })
        .collect()
}

fn build_built_in_plugins() -> Vec<PluginManifest> {
    vec![
        PluginManifest::planned_builtin(
            "0194c6b5-6f20-7a31-9f0b-111111111111",
            "Content Blocker Plugin API",
            "UUIDv7 extension point for installable content blockers and declarative network filters.",
            PluginKind::ContentBlocker,
            vec![PluginCapability::NetworkFiltering],
            vec![PluginPermission::NetworkRequests],
        ),
        PluginManifest::planned_builtin(
            "0194c6b5-6f20-7a32-9f0b-222222222222",
            "Theme Pack Plugin API",
            "UUIDv7 extension point for installable theme packs and local visual assets.",
            PluginKind::Theme,
            vec![PluginCapability::ThemeAssets, PluginCapability::StyleSheets],
            vec![PluginPermission::StoreLocalData],
        ),
        PluginManifest::planned_builtin(
            "0194c6b5-6f20-7a33-9f0b-333333333333",
            "Media Helper Plugin API",
            "UUIDv7 extension point for media helpers, site compatibility shims, and playback diagnostics.",
            PluginKind::Media,
            vec![PluginCapability::MediaHelpers],
            vec![
                PluginPermission::ReadActiveTab,
                PluginPermission::CaptureMedia,
            ],
        ),
        PluginManifest::planned_builtin(
            "0194c6b5-6f20-7a34-9f0b-444444444444",
            "Agent Tool Plugin API",
            "UUIDv7 extension point for audited Agent commands, local tools, and browser automation surfaces.",
            PluginKind::Agent,
            vec![PluginCapability::AgentHooks, PluginCapability::Tabs],
            vec![
                PluginPermission::UseAgentContext,
                PluginPermission::ReadActiveTab,
            ],
        ),
        PluginManifest::planned_builtin(
            "0194c6b5-6f20-7a35-9f0b-555555555555",
            "Reader Plugin API",
            "UUIDv7 extension point for article extraction, summaries, outlines, and structured data handoff.",
            PluginKind::Reader,
            vec![PluginCapability::ReaderArtifacts],
            vec![PluginPermission::ReadActiveTab],
        ),
    ]
}

fn built_in_plugin_ids() -> BTreeSet<PluginId> {
    built_in_plugins()
        .into_iter()
        .map(|plugin| plugin.id)
        .collect()
}

fn clipped_non_empty(value: &str, max_chars: usize, fallback: &str) -> String {
    let value = value.trim();
    if value.is_empty() {
        return fallback.to_string();
    }
    value.chars().take(max_chars).collect()
}

fn dedupe_sorted<T>(values: &mut Vec<T>)
where
    T: Ord + Copy,
{
    values.sort();
    values.dedup();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn built_in_plugin_ids_are_uuidv7_and_unique() {
        let registry = PluginRegistry::default();
        let mut ids = BTreeSet::new();
        assert_eq!(registry.max_plugins, DEFAULT_MAX_PLUGINS);
        assert!(!registry.installed.is_empty());
        for plugin in registry.installed {
            assert!(ids.insert(plugin.id));
            assert_eq!(plugin.id.as_uuid().get_version_num(), 7);
            assert!(plugin.built_in);
            assert_eq!(plugin.status, PluginStatus::Planned);
            assert!(!plugin.capabilities.is_empty());
        }
    }

    #[test]
    fn local_plugins_use_uuidv7_ids() {
        let plugin = PluginManifest::local(
            "Local Tool",
            "Does useful local work",
            "1.0.0",
            PluginKind::Tool,
            vec![PluginCapability::Downloads],
            vec![PluginPermission::StoreLocalData],
        );

        assert_eq!(plugin.id.as_uuid().get_version_num(), 7);
        assert!(!plugin.built_in);
        assert_eq!(plugin.status, PluginStatus::Installed);
    }

    #[test]
    fn bundled_plugin_modules_are_uuidv7_unique_and_installable() {
        let modules = bundled_plugin_modules();
        let mut ids = BTreeSet::new();

        assert!(!modules.is_empty());
        for module in modules {
            assert_eq!(module.schema, PLUGIN_MODULE_SCHEMA);
            assert!(valid_bundled_module_id(&module.module_id));
            assert!(valid_bundled_source_path(
                &module.source_path,
                &module.module_id
            ));
            assert!(module.manifest.id.is_v7());
            assert!(ids.insert(module.manifest.id));
            assert!(!module.manifest.built_in);
            assert_eq!(module.manifest.status, PluginStatus::Installed);
            assert!(!module.manifest.capabilities.is_empty());
        }
    }

    #[test]
    fn bundled_plugin_raw_metadata_is_valid_before_sanitization() {
        for module_json in BUNDLED_PLUGIN_MANIFESTS {
            let module: PluginModule = serde_json::from_str(module_json).unwrap();
            validate_raw_plugin_module(&module).unwrap();
            assert_eq!(
                sanitize_plugin_module(module.clone()).module_id,
                module.module_id
            );
        }
    }

    #[test]
    fn raw_plugin_module_validation_rejects_sanitizable_mistakes() {
        let mut module: PluginModule = serde_json::from_str(BUNDLED_PLUGIN_MANIFESTS[0]).unwrap();
        module.module_id = "reader-outline".repeat(10);
        module.source_path = format!("plugins/{}/plugin.json", module.module_id);

        assert!(
            validate_raw_plugin_module(&module)
                .unwrap_err()
                .contains("module_id")
        );

        let mut module: PluginModule = serde_json::from_str(BUNDLED_PLUGIN_MANIFESTS[0]).unwrap();
        module.manifest.name = " ".to_string();
        assert!(
            validate_raw_plugin_module(&module)
                .unwrap_err()
                .contains("name")
        );

        let mut module: PluginModule = serde_json::from_str(BUNDLED_PLUGIN_MANIFESTS[0]).unwrap();
        module
            .manifest
            .capabilities
            .push(module.manifest.capabilities[0]);
        assert!(
            validate_raw_plugin_module(&module)
                .unwrap_err()
                .contains("capabilities")
        );
    }

    #[test]
    fn registry_installs_enables_disables_and_removes_bundled_plugins() {
        let plugin_id = bundled_plugin_manifests().first().unwrap().id;
        let mut registry = PluginRegistry::default();

        let installed = registry.install_bundled(plugin_id).unwrap();
        assert_eq!(installed.id, plugin_id);
        assert!(registry.is_installed(plugin_id));

        let enabled = registry
            .set_status(plugin_id, PluginStatus::Enabled)
            .unwrap();
        assert_eq!(enabled.status, PluginStatus::Enabled);
        assert_eq!(registry.enabled_count(), 1);

        let disabled = registry
            .set_status(plugin_id, PluginStatus::Disabled)
            .unwrap();
        assert_eq!(disabled.status, PluginStatus::Disabled);

        let removed = registry.remove(plugin_id).unwrap();
        assert_eq!(removed.id, plugin_id);
        assert!(!registry.is_installed(plugin_id));
    }

    #[test]
    fn bundled_availability_excludes_installed_plugins() {
        let plugin_id = bundled_plugin_manifests().first().unwrap().id;
        let mut registry = PluginRegistry::default();
        let before = available_bundled_plugin_modules(&registry).len();

        registry.install_bundled(plugin_id).unwrap();
        let available = available_bundled_plugin_modules(&registry);

        assert_eq!(available.len(), before - 1);
        assert!(
            !available
                .iter()
                .any(|module| module.manifest.id == plugin_id)
        );
    }

    #[test]
    fn registry_installs_all_available_bundled_plugins() {
        let mut registry = PluginRegistry::default();
        let available = available_bundled_plugin_modules(&registry).len();

        let installed = registry.install_all_available_bundled().unwrap();

        assert_eq!(installed.len(), available);
        assert_eq!(available_bundled_plugin_modules(&registry).len(), 0);
        assert_eq!(
            registry
                .installed
                .iter()
                .filter(|plugin| !plugin.built_in)
                .count(),
            available
        );
    }

    #[test]
    fn registry_syncs_bundled_metadata_without_changing_status() {
        let module = bundled_plugin_modules().first().unwrap().clone();
        let mut registry = PluginRegistry::default();
        registry.install_bundled(module.manifest.id).unwrap();
        registry
            .set_status(module.manifest.id, PluginStatus::Enabled)
            .unwrap();

        let synced = registry.sync_bundled(module.manifest.id).unwrap();

        assert_eq!(synced.id, module.manifest.id);
        assert_eq!(synced.name, module.manifest.name);
        assert_eq!(synced.status, PluginStatus::Enabled);
        assert!(!synced.built_in);
    }

    #[test]
    fn registry_syncs_all_installed_bundled_plugins() {
        let mut registry = PluginRegistry::default();
        let installed = registry.install_all_available_bundled().unwrap();

        let synced = registry.sync_all_installed_bundled().unwrap();

        assert_eq!(synced.len(), installed.len());
        assert!(synced.iter().all(|plugin| !plugin.built_in));
    }

    #[test]
    fn plugin_check_report_accepts_default_registry() {
        let registry = PluginRegistry::default();

        let report = check_plugin_registry(&registry);

        assert!(report.ok);
        assert_eq!(report.schema, PLUGIN_CHECK_SCHEMA);
        assert_eq!(report.error_count, 0);
        assert_eq!(report.bundled_count, bundled_plugin_modules().len());
        assert_eq!(report.available_count, bundled_plugin_modules().len());
    }

    #[test]
    fn plugin_check_report_detects_registry_errors_and_metadata_drift() {
        let module = bundled_plugin_modules().first().unwrap().clone();
        let mut registry = PluginRegistry::default();
        registry.install_bundled(module.manifest.id).unwrap();
        let drifted = registry
            .installed
            .iter_mut()
            .find(|plugin| plugin.id == module.manifest.id)
            .unwrap();
        drifted.name = "Drifted module name".to_string();
        let mut legacy = PluginManifest::local(
            "Legacy",
            "Legacy id",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        legacy.id = PluginId(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
        registry.installed.push(legacy);

        let report = check_plugin_registry(&registry);

        assert!(!report.ok);
        assert!(report.error_count > 0);
        assert!(
            report
                .issues
                .iter()
                .any(|issue| issue.code == "plugin-id-not-uuidv7")
        );
        assert!(
            report
                .issues
                .iter()
                .any(|issue| issue.code == "bundled-metadata-drift")
        );
    }

    #[test]
    fn plugin_selectors_resolve_uuid_or_bundled_module_id() {
        let module = bundled_plugin_modules().first().unwrap().clone();

        assert_eq!(
            resolve_plugin_selector(&module.manifest.id.to_string()),
            Some(module.manifest.id)
        );
        assert_eq!(
            resolve_plugin_selector(&module.module_id),
            Some(module.manifest.id)
        );
        assert_eq!(resolve_plugin_selector("missing-module"), None);
    }

    #[test]
    fn registry_caps_plugins_and_preserves_builtins() {
        let built_in_count = built_in_plugins().len();
        let mut registry = PluginRegistry {
            max_plugins: built_in_count + 1,
            ..PluginRegistry::default()
        };
        let first_plugin = PluginManifest::local(
            "First",
            "Oldest local plugin",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        let first = first_plugin.id;

        registry.register(first_plugin).unwrap();
        registry
            .register(PluginManifest::local(
                "Second",
                "Newest local plugin",
                "1.0.0",
                PluginKind::Tool,
                Vec::new(),
                Vec::new(),
            ))
            .unwrap();

        assert_eq!(registry.installed_count(), built_in_count + 1);
        assert_eq!(
            registry
                .installed
                .iter()
                .filter(|plugin| plugin.built_in)
                .count(),
            built_in_count
        );
        assert!(!registry.installed.iter().any(|plugin| plugin.id == first));
    }

    #[test]
    fn duplicate_plugin_ids_are_rejected() {
        let mut registry = PluginRegistry::default();
        let plugin = PluginManifest::local(
            "Duplicate",
            "Duplicate plugin",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        let duplicate = plugin.clone();

        registry.register(plugin).unwrap();
        let err = registry.register(duplicate).unwrap_err();

        assert!(matches!(err, PluginRegistryError::DuplicateId(_)));
    }

    #[test]
    fn registry_drops_non_uuidv7_plugin_ids() {
        let mut plugin = PluginManifest::local(
            "Legacy",
            "Legacy non-v7 plugin",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        plugin.id = PluginId(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
        let legacy_id = plugin.id;
        let mut registry = PluginRegistry {
            installed: vec![plugin],
            ..PluginRegistry::default()
        };

        registry.enforce_limits();

        assert!(
            !registry
                .installed
                .iter()
                .any(|plugin| plugin.id == legacy_id)
        );
        assert!(registry.installed.iter().all(|plugin| plugin.id.is_v7()));
    }

    #[test]
    fn backend_ingest_rejects_unsafe_plugin_records() {
        let mut legacy = PluginManifest::local(
            "Legacy",
            "Legacy non-v7 plugin",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        legacy.name = "Legacy".repeat(40);
        legacy.id = PluginId(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
        let mut spoofed_builtin = PluginManifest::local(
            "Spoof",
            "Tries to look built in",
            "1.0.0",
            PluginKind::Theme,
            Vec::new(),
            Vec::new(),
        );
        spoofed_builtin.built_in = true;

        let mut registry = PluginRegistry::default();
        let report = registry
            .ingest_backend_registry(PluginBackendRegistry {
                schema: PLUGIN_BACKEND_REGISTRY_SCHEMA.to_string(),
                generated_at: Utc::now(),
                registry_version: 1,
                plugins: vec![legacy, spoofed_builtin],
            })
            .unwrap();

        assert_eq!(report.received, 2);
        assert_eq!(report.accepted, 0);
        assert_eq!(report.rejected.len(), 2);
        assert!(
            report
                .rejected
                .iter()
                .any(|rejection| rejection.reason.contains("UUIDv7"))
        );
        assert!(
            report
                .rejected
                .iter()
                .any(|rejection| rejection.reason.contains("built_in"))
        );
        assert!(
            report
                .rejected
                .iter()
                .all(|rejection| rejection.name.chars().count() <= MAX_PLUGIN_NAME_CHARS)
        );
    }

    #[test]
    fn backend_ingest_does_not_silently_enable_plugins() {
        let mut plugin = PluginManifest::local(
            "Backend Theme",
            "Remote theme metadata",
            "1.0.0",
            PluginKind::Theme,
            vec![PluginCapability::ThemeAssets],
            vec![PluginPermission::StoreLocalData],
        );
        plugin.status = PluginStatus::Enabled;
        let plugin_id = plugin.id;

        let mut registry = PluginRegistry::default();
        let report = registry
            .ingest_backend_registry(PluginBackendRegistry {
                schema: PLUGIN_BACKEND_REGISTRY_SCHEMA.to_string(),
                generated_at: Utc::now(),
                registry_version: 2,
                plugins: vec![plugin],
            })
            .unwrap();

        let installed = registry
            .installed
            .iter()
            .find(|installed| installed.id == plugin_id)
            .unwrap();
        assert_eq!(report.accepted, 1);
        assert_eq!(installed.status, PluginStatus::Installed);
    }

    #[test]
    fn backend_ingest_preserves_existing_local_status() {
        let mut local = PluginManifest::local(
            "Backend Tool",
            "Initial metadata",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        local.status = PluginStatus::Disabled;
        let plugin_id = local.id;
        let installed_at = local.installed_at;

        let mut registry = PluginRegistry::default();
        registry.register(local).unwrap();

        let mut update = PluginManifest::local(
            "Backend Tool Updated",
            "Updated metadata",
            "2.0.0",
            PluginKind::Tool,
            vec![PluginCapability::Downloads],
            Vec::new(),
        );
        update.id = plugin_id;
        update.status = PluginStatus::Enabled;

        let report = registry
            .ingest_backend_registry(PluginBackendRegistry {
                schema: PLUGIN_BACKEND_REGISTRY_SCHEMA.to_string(),
                generated_at: Utc::now(),
                registry_version: 3,
                plugins: vec![update],
            })
            .unwrap();

        let installed = registry
            .installed
            .iter()
            .find(|installed| installed.id == plugin_id)
            .unwrap();
        assert_eq!(report.updated, 1);
        assert_eq!(installed.name, "Backend Tool Updated");
        assert_eq!(installed.status, PluginStatus::Disabled);
        assert_eq!(installed.installed_at, installed_at);
    }

    #[test]
    fn backend_ingest_rejects_unknown_schema() {
        let mut registry = PluginRegistry::default();
        let err = registry
            .ingest_backend_registry(PluginBackendRegistry {
                schema: "wrong.schema".to_string(),
                generated_at: Utc::now(),
                registry_version: 1,
                plugins: Vec::new(),
            })
            .unwrap_err();

        assert!(matches!(
            err,
            PluginBackendIngestError::UnsupportedSchema { .. }
        ));
    }

    #[test]
    fn backend_ingest_rejects_oversized_registries_before_iteration() {
        let mut registry = PluginRegistry::default();
        let plugin = PluginManifest::local(
            "Backend Tool",
            "Remote metadata",
            "1.0.0",
            PluginKind::Tool,
            Vec::new(),
            Vec::new(),
        );
        let err = registry
            .ingest_backend_registry(PluginBackendRegistry {
                schema: PLUGIN_BACKEND_REGISTRY_SCHEMA.to_string(),
                generated_at: Utc::now(),
                registry_version: 1,
                plugins: vec![plugin; DEFAULT_MAX_PLUGINS + 1],
            })
            .unwrap_err();

        assert!(matches!(
            err,
            PluginBackendIngestError::RegistryTooLarge { .. }
        ));
    }
}
