//! Exact authored-file guards plus observational source-control and ownership status.

use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use bevy::prelude::*;
use bevy_egui::egui;
use egui_phosphor_icons::icons;
use shared::PrefabInstance;

use crate::assets::EditorAssets;
use crate::project_io::ProjectWorkspace;
use crate::scene::recovery::atomic_write_with_pre_rename;
use crate::scene_io::SceneIo;

const STATUS_REFRESH_INTERVAL: Duration = Duration::from_secs(2);

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileRevision {
    Missing,
    Present([u8; 32]),
}

impl FileRevision {
    pub fn short_label(&self) -> String {
        match self {
            Self::Missing => "missing".into(),
            Self::Present(hash) => hash[..6].iter().map(|byte| format!("{byte:02x}")).collect(),
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileComparisonMetadata {
    pub byte_len: Option<u64>,
    pub modified_unix_millis: Option<u128>,
    pub readonly: bool,
}

impl FileComparisonMetadata {
    fn from_metadata(metadata: &fs::Metadata) -> Self {
        Self {
            byte_len: Some(metadata.len()),
            modified_unix_millis: metadata.modified().ok().and_then(system_time_millis),
            readonly: metadata.permissions().readonly(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileSnapshot {
    pub revision: FileRevision,
    pub metadata: FileComparisonMetadata,
}

impl FileSnapshot {
    pub fn missing() -> Self {
        Self {
            revision: FileRevision::Missing,
            metadata: FileComparisonMetadata::default(),
        }
    }

    pub fn capture(path: &Path) -> Result<Self, String> {
        match fs::read(path) {
            Ok(bytes) => Ok(Self::from_loaded_bytes(path, &bytes)),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Self::missing()),
            Err(error) => Err(format!("could not read {}: {error}", path.display())),
        }
    }

    pub fn from_loaded_bytes(path: &Path, bytes: &[u8]) -> Self {
        let metadata = fs::metadata(path)
            .ok()
            .map(|metadata| FileComparisonMetadata::from_metadata(&metadata))
            .unwrap_or_else(|| FileComparisonMetadata {
                byte_len: Some(bytes.len() as u64),
                ..Default::default()
            });
        Self {
            revision: FileRevision::Present(*blake3::hash(bytes).as_bytes()),
            metadata,
        }
    }
}

fn system_time_millis(time: SystemTime) -> Option<u128> {
    time.duration_since(UNIX_EPOCH)
        .ok()
        .map(|duration| duration.as_millis())
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GuardedWriteError {
    Changed {
        expected: Box<FileSnapshot>,
        current: Box<FileSnapshot>,
    },
    ReadOnly {
        expected: Box<FileSnapshot>,
        current: Box<FileSnapshot>,
    },
    Io(String),
}

impl fmt::Display for GuardedWriteError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Changed { .. } => write!(formatter, "the file changed outside Blacksite"),
            Self::ReadOnly { .. } => write!(formatter, "the file is read-only"),
            Self::Io(error) => formatter.write_str(error),
        }
    }
}

impl From<String> for GuardedWriteError {
    fn from(error: String) -> Self {
        Self::Io(error)
    }
}

pub fn guarded_atomic_write(
    path: &Path,
    bytes: &[u8],
    expected: &FileSnapshot,
) -> Result<FileSnapshot, GuardedWriteError> {
    guarded_atomic_write_inner(path, bytes, expected, || {})
}

fn guarded_atomic_write_inner(
    path: &Path,
    bytes: &[u8],
    expected: &FileSnapshot,
    before_final_check: impl FnOnce(),
) -> Result<FileSnapshot, GuardedWriteError> {
    verify_expected_revision(path, expected)?;
    atomic_write_with_pre_rename(path, bytes, || {
        before_final_check();
        verify_expected_revision(path, expected).map(|_| ())
    })?;
    Ok(FileSnapshot::from_loaded_bytes(path, bytes))
}

fn verify_expected_revision(
    path: &Path,
    expected: &FileSnapshot,
) -> Result<FileSnapshot, GuardedWriteError> {
    let current = FileSnapshot::capture(path).map_err(GuardedWriteError::Io)?;
    if current.revision != expected.revision {
        return Err(GuardedWriteError::Changed {
            expected: Box::new(expected.clone()),
            current: Box::new(current),
        });
    }
    if current.metadata.readonly {
        return Err(GuardedWriteError::ReadOnly {
            expected: Box::new(expected.clone()),
            current: Box::new(current),
        });
    }
    Ok(current)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitFileState {
    Clean,
    Modified,
    Untracked,
    Conflicted,
}

impl GitFileState {
    fn priority(self) -> u8 {
        match self {
            Self::Clean => 0,
            Self::Modified => 1,
            Self::Untracked => 2,
            Self::Conflicted => 3,
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
enum GitAvailability {
    Available,
    #[default]
    Unavailable,
    NotRepository,
    Error(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileOwnership {
    pub path: PathBuf,
    pub owner: Option<String>,
    pub locked_by_other: bool,
    pub detail: Option<String>,
}

/// Optional vendor/team integration. Calls run on the collaboration worker thread.
pub trait FileOwnershipProvider: Send + Sync + 'static {
    fn name(&self) -> &str;

    fn query(
        &self,
        project_root: &Path,
        project_relative_paths: &[PathBuf],
    ) -> Result<Vec<FileOwnership>, String>;
}

#[derive(Resource, Default)]
pub struct FileOwnershipProviders {
    providers: Vec<Arc<dyn FileOwnershipProvider>>,
}

impl FileOwnershipProviders {
    pub fn register(&mut self, provider: Arc<dyn FileOwnershipProvider>) {
        self.providers.push(provider);
    }

    pub fn register_typed(&mut self, provider: impl FileOwnershipProvider) {
        self.register(Arc::new(provider));
    }

    pub fn is_empty(&self) -> bool {
        self.providers.is_empty()
    }
}

pub fn register_file_ownership_provider(app: &mut App, provider: impl FileOwnershipProvider) {
    app.init_resource::<FileOwnershipProviders>();
    app.world_mut()
        .resource_mut::<FileOwnershipProviders>()
        .register_typed(provider);
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ProviderFileState {
    provider: String,
    owner: Option<String>,
    locked_by_other: bool,
    detail: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ObservedFileMetadata {
    readonly: bool,
}

#[derive(Debug, Clone, Default)]
struct CollaborationScan {
    git_availability: GitAvailability,
    git_files: HashMap<PathBuf, GitFileState>,
    ownership: HashMap<PathBuf, Vec<ProviderFileState>>,
    provider_errors: Vec<String>,
    observed_files: HashMap<PathBuf, ObservedFileMetadata>,
}

struct CollaborationScanJob {
    result: Arc<Mutex<Option<CollaborationScan>>>,
    worker: Option<JoinHandle<()>>,
}

impl Drop for CollaborationScanJob {
    fn drop(&mut self) {
        if let Some(worker) = self.worker.take() {
            // Provider integrations are expected to be bounded, but a faulty provider must not
            // hold editor shutdown hostage. Dropping a live handle detaches the worker safely.
            if worker.is_finished() {
                let _ = worker.join();
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FileWriteIntent {
    Scene { tab_id: u64, entity_count: usize },
    Material,
    MaterialInstance,
    ProjectSettings,
    PrefabSource { instance_root: Entity },
    PrefabSourceHistory { instance_root: Entity },
    Standalone { description: String },
}

impl FileWriteIntent {
    fn display_name(&self) -> &str {
        match self {
            Self::Scene { .. } => "scene",
            Self::Material => "material",
            Self::MaterialInstance => "material instance",
            Self::ProjectSettings => "project settings",
            Self::PrefabSource { .. } => "prefab source",
            Self::PrefabSourceHistory { .. } => "prefab source history",
            Self::Standalone { description } => description,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum FileConflictReason {
    ExternalChange,
    ReadOnly,
    ProviderLocked {
        provider: String,
        owner: Option<String>,
        detail: Option<String>,
    },
}

#[derive(Debug, Clone)]
struct PendingFileConflict {
    path: PathBuf,
    expected: FileSnapshot,
    current: FileSnapshot,
    reason: FileConflictReason,
    intent: FileWriteIntent,
    proposed_bytes: Vec<u8>,
    show_metadata: bool,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileCollaborationStatus {
    pub git: Option<GitFileState>,
    pub readonly: bool,
    pub owner: Option<String>,
    pub locked_by_other: bool,
    pub provider: Option<String>,
    pub ownership_detail: Option<String>,
    pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileStatusTone {
    Neutral,
    Success,
    Warning,
    Error,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileStatusIndicator {
    pub label: &'static str,
    pub tone: FileStatusTone,
}

impl FileCollaborationStatus {
    pub fn indicator(&self) -> Option<FileStatusIndicator> {
        if self.locked_by_other {
            return Some(FileStatusIndicator {
                label: "LOCKED",
                tone: FileStatusTone::Error,
            });
        }
        if self.readonly {
            return Some(FileStatusIndicator {
                label: "READ ONLY",
                tone: FileStatusTone::Error,
            });
        }
        match self.git {
            Some(GitFileState::Conflicted) => Some(FileStatusIndicator {
                label: "CONFLICT",
                tone: FileStatusTone::Error,
            }),
            Some(GitFileState::Modified) => Some(FileStatusIndicator {
                label: "MODIFIED",
                tone: FileStatusTone::Warning,
            }),
            Some(GitFileState::Untracked) => Some(FileStatusIndicator {
                label: "UNTRACKED",
                tone: FileStatusTone::Neutral,
            }),
            Some(GitFileState::Clean) => Some(FileStatusIndicator {
                label: "CLEAN",
                tone: FileStatusTone::Success,
            }),
            None if self.owner.is_some() => Some(FileStatusIndicator {
                label: "OWNED",
                tone: FileStatusTone::Neutral,
            }),
            None if !self.diagnostics.is_empty() => Some(FileStatusIndicator {
                label: "STATUS",
                tone: FileStatusTone::Warning,
            }),
            None => None,
        }
    }

    pub fn tooltip(&self, path: &Path) -> String {
        let mut lines = vec![path.display().to_string()];
        if let Some(git) = self.git {
            lines.push(format!(
                "Git: {}",
                match git {
                    GitFileState::Clean => "clean",
                    GitFileState::Modified => "modified",
                    GitFileState::Untracked => "untracked",
                    GitFileState::Conflicted => "conflicted",
                }
            ));
        }
        if self.readonly {
            lines.push("Filesystem: read-only".into());
        }
        if let Some(provider) = &self.provider {
            let owner = self.owner.as_deref().unwrap_or("unassigned");
            let lock = if self.locked_by_other {
                "locked by another owner"
            } else {
                "available"
            };
            lines.push(format!("{provider}: {lock} ({owner})"));
        }
        if let Some(detail) = &self.ownership_detail {
            lines.push(detail.clone());
        }
        lines.extend(self.diagnostics.iter().cloned());
        lines.join("\n")
    }
}

pub(crate) fn file_status_indicator_ui(
    ui: &mut egui::Ui,
    status: &FileCollaborationStatus,
    path: &Path,
) -> Option<egui::Response> {
    let indicator = status.indicator()?;
    let color = match indicator.tone {
        FileStatusTone::Neutral => crate::ui::theme::TEXT_DIM,
        FileStatusTone::Success => crate::ui::theme::SUCCESS,
        FileStatusTone::Warning => crate::ui::theme::WARNING,
        FileStatusTone::Error => crate::ui::theme::ERROR,
    };
    let icon = if status.locked_by_other || status.readonly {
        icons::LOCK_SIMPLE
    } else {
        match status.git {
            Some(GitFileState::Clean) => icons::CHECK_CIRCLE,
            Some(GitFileState::Conflicted) => icons::WARNING,
            _ => icons::GIT_BRANCH,
        }
    };
    let fill = egui::Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 18);
    let response = egui::Frame::new()
        .fill(fill)
        .stroke(egui::Stroke::new(1.0, color.linear_multiply(0.55)))
        .corner_radius(2.0)
        .inner_margin(egui::Margin::symmetric(5, 1))
        .show(ui, |ui| {
            ui.spacing_mut().item_spacing.x = 4.0;
            ui.horizontal(|ui| {
                ui.label(
                    egui::RichText::new(icon.as_str())
                        .font(egui::FontId::new(
                            12.0,
                            egui::FontFamily::Name("phosphor-regular".into()),
                        ))
                        .color(color),
                );
                ui.label(
                    egui::RichText::new(indicator.label)
                        .small()
                        .strong()
                        .color(color),
                );
            });
        })
        .response;
    Some(response.on_hover_text(status.tooltip(path)))
}

#[derive(Resource, Default)]
pub struct CollaborationState {
    project_root: PathBuf,
    tracked_paths: Vec<PathBuf>,
    git_availability: GitAvailability,
    git_files: HashMap<PathBuf, GitFileState>,
    ownership: HashMap<PathBuf, Vec<ProviderFileState>>,
    provider_errors: Vec<String>,
    observed_files: HashMap<PathBuf, ObservedFileMetadata>,
    scan_job: Option<CollaborationScanJob>,
    last_scan_started: Option<Instant>,
    pending_conflict: Option<PendingFileConflict>,
}

impl CollaborationState {
    pub fn has_pending_conflict(&self) -> bool {
        self.pending_conflict.is_some()
    }

    pub fn request_refresh(&mut self) {
        self.last_scan_started = None;
    }

    pub fn file_status(&self, path: &Path) -> FileCollaborationStatus {
        let Some(relative) = project_relative_path(&self.project_root, path) else {
            return FileCollaborationStatus::default();
        };
        let git = matches!(self.git_availability, GitAvailability::Available).then(|| {
            self.git_files
                .get(&relative)
                .copied()
                .unwrap_or(GitFileState::Clean)
        });
        let ownership = self
            .ownership
            .get(&relative)
            .and_then(|states| {
                states
                    .iter()
                    .find(|state| state.locked_by_other)
                    .or_else(|| states.first())
            })
            .cloned();
        let mut diagnostics = self.provider_errors.clone();
        if let GitAvailability::Error(error) = &self.git_availability {
            diagnostics.push(error.clone());
        }
        FileCollaborationStatus {
            git,
            readonly: self
                .observed_files
                .get(&relative)
                .is_some_and(|metadata| metadata.readonly),
            owner: ownership.as_ref().and_then(|state| state.owner.clone()),
            locked_by_other: ownership
                .as_ref()
                .is_some_and(|state| state.locked_by_other),
            provider: ownership.as_ref().map(|state| state.provider.clone()),
            ownership_detail: ownership.and_then(|state| state.detail),
            diagnostics,
        }
    }
}

pub struct CollaborationPlugin;

impl Plugin for CollaborationPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<FileOwnershipProviders>()
            .init_resource::<CollaborationState>()
            .add_systems(Update, tick_collaboration_status);
    }
}

pub(crate) fn publish_authored_file(
    world: &mut World,
    path: &Path,
    bytes: &[u8],
    expected: &FileSnapshot,
    intent: FileWriteIntent,
) -> Result<FileSnapshot, String> {
    if let Some((provider, owner, detail)) = world
        .get_resource::<CollaborationState>()
        .map(|state| state.file_status(path))
        .filter(|status| status.locked_by_other)
        .map(|status| (status.provider, status.owner, status.ownership_detail))
    {
        let current = FileSnapshot::capture(path)?;
        let provider = provider.unwrap_or_else(|| "ownership provider".into());
        queue_conflict(
            world,
            PendingFileConflict {
                path: path.to_path_buf(),
                expected: expected.clone(),
                current,
                reason: FileConflictReason::ProviderLocked {
                    provider: provider.clone(),
                    owner,
                    detail,
                },
                intent,
                proposed_bytes: bytes.to_vec(),
                show_metadata: false,
            },
        );
        return Err(format!(
            "{} is locked by another owner via {provider}",
            path.display()
        ));
    }

    match guarded_atomic_write(path, bytes, expected) {
        Ok(snapshot) => {
            if let Some(mut state) = world.get_resource_mut::<CollaborationState>() {
                state.request_refresh();
            }
            Ok(snapshot)
        }
        Err(GuardedWriteError::Changed { expected, current }) => {
            queue_conflict(
                world,
                PendingFileConflict {
                    path: path.to_path_buf(),
                    expected: *expected,
                    current: *current,
                    reason: FileConflictReason::ExternalChange,
                    intent,
                    proposed_bytes: bytes.to_vec(),
                    show_metadata: false,
                },
            );
            Err(format!("{} changed outside Blacksite", path.display()))
        }
        Err(GuardedWriteError::ReadOnly { expected, current }) => {
            queue_conflict(
                world,
                PendingFileConflict {
                    path: path.to_path_buf(),
                    expected: *expected,
                    current: *current,
                    reason: FileConflictReason::ReadOnly,
                    intent,
                    proposed_bytes: bytes.to_vec(),
                    show_metadata: false,
                },
            );
            Err(format!("{} is read-only", path.display()))
        }
        Err(GuardedWriteError::Io(error)) => Err(error),
    }
}

enum ConflictUiAction {
    Cancel,
    ToggleMetadata,
    Reload,
    SaveAs,
}

pub(crate) fn file_conflict_modal(world: &mut World, ctx: &egui::Context) {
    let conflict = world
        .get_resource::<CollaborationState>()
        .and_then(|state| state.pending_conflict.clone());
    let Some(conflict) = conflict else {
        return;
    };

    let mut action = None;
    let response = egui::Modal::new(egui::Id::new("collaborative_file_conflict")).show(ctx, |ui| {
        ui.set_width(480.0);
        ui.horizontal(|ui| {
            ui.label(
                egui::RichText::new(icons::WARNING.as_str())
                    .font(egui::FontId::new(
                        22.0,
                        egui::FontFamily::Name("phosphor-regular".into()),
                    ))
                    .color(crate::ui::theme::WARNING),
            );
            ui.heading("Authored File Not Saved");
        });
        ui.add_space(6.0);
        ui.add(
            egui::Label::new(
                egui::RichText::new(conflict.path.display().to_string())
                    .monospace()
                    .color(crate::ui::theme::TEXT),
            )
            .wrap(),
        );
        ui.add_space(8.0);
        ui.label(conflict_reason_text(&conflict));
        ui.small(
            egui::RichText::new("The existing file is unchanged.")
                .color(crate::ui::theme::TEXT_DIM),
        );

        if conflict.show_metadata {
            ui.add_space(10.0);
            egui::Frame::new()
                .fill(crate::ui::theme::PANEL_BG_DARK)
                .stroke(egui::Stroke::new(1.0, crate::ui::theme::BORDER))
                .corner_radius(3.0)
                .inner_margin(egui::Margin::same(9))
                .show(ui, |ui| {
                    metadata_comparison_ui(ui, &conflict);
                });
        }

        ui.add_space(12.0);
        ui.separator();
        ui.add_space(8.0);
        ui.horizontal_wrapped(|ui| {
            if ui.button(format!("{} Cancel", icons::X.as_str())).clicked() {
                action = Some(ConflictUiAction::Cancel);
            }
            if ui
                .button(format!(
                    "{} {}",
                    icons::ARROWS_LEFT_RIGHT.as_str(),
                    if conflict.show_metadata {
                        "Hide Metadata"
                    } else {
                        "Compare Metadata"
                    }
                ))
                .clicked()
            {
                action = Some(ConflictUiAction::ToggleMetadata);
            }
            if ui
                .button(format!("{} Save As", icons::COPY.as_str()))
                .clicked()
            {
                action = Some(ConflictUiAction::SaveAs);
            }
            let reload_supported = !matches!(conflict.intent, FileWriteIntent::Standalone { .. });
            if ui
                .add_enabled(
                    reload_supported,
                    egui::Button::new(format!("{} Reload", icons::ARROW_CLOCKWISE.as_str())),
                )
                .on_disabled_hover_text("This output is not an open editor document")
                .clicked()
            {
                action = Some(ConflictUiAction::Reload);
            }
        });
    });
    if action.is_none() && response.should_close() {
        action = Some(ConflictUiAction::Cancel);
    }

    match action {
        Some(ConflictUiAction::Cancel) => {
            clear_file_conflict(world);
            world.resource_mut::<SceneIo>().set_status(format!(
                "Save conflict cancelled; the local {} remains unsaved",
                conflict.intent.display_name()
            ));
        }
        Some(ConflictUiAction::ToggleMetadata) => {
            if let Some(pending) = world
                .resource_mut::<CollaborationState>()
                .pending_conflict
                .as_mut()
            {
                pending.show_metadata = !pending.show_metadata;
            }
        }
        Some(ConflictUiAction::Reload) => resolve_conflict_reload(world, &conflict),
        Some(ConflictUiAction::SaveAs) => resolve_conflict_save_as(world, &conflict),
        None => {}
    }
}

fn conflict_reason_text(conflict: &PendingFileConflict) -> String {
    match &conflict.reason {
        FileConflictReason::ExternalChange => format!(
            "The {} changed on disk after Blacksite loaded it.",
            conflict.intent.display_name()
        ),
        FileConflictReason::ReadOnly => format!(
            "The {} is read-only and cannot be replaced.",
            conflict.intent.display_name()
        ),
        FileConflictReason::ProviderLocked {
            provider,
            owner,
            detail,
        } => {
            let owner = owner.as_deref().unwrap_or("another owner");
            let detail = detail
                .as_deref()
                .map(|detail| format!(" {detail}"))
                .unwrap_or_default();
            format!("{provider} reports this file is locked by {owner}.{detail}")
        }
    }
}

fn metadata_comparison_ui(ui: &mut egui::Ui, conflict: &PendingFileConflict) {
    egui::Grid::new("file_conflict_metadata")
        .num_columns(3)
        .striped(true)
        .show(ui, |ui| {
            ui.strong("Field");
            ui.strong("Loaded");
            ui.strong("Current");
            ui.end_row();
            metadata_row(
                ui,
                "Revision",
                conflict.expected.revision.short_label(),
                conflict.current.revision.short_label(),
            );
            metadata_row(
                ui,
                "Bytes",
                metadata_size(&conflict.expected.metadata),
                metadata_size(&conflict.current.metadata),
            );
            metadata_row(
                ui,
                "Modified",
                metadata_modified(&conflict.expected.metadata),
                metadata_modified(&conflict.current.metadata),
            );
            metadata_row(
                ui,
                "Permissions",
                metadata_permissions(&conflict.expected.metadata),
                metadata_permissions(&conflict.current.metadata),
            );
        });
}

fn metadata_row(ui: &mut egui::Ui, label: &str, loaded: String, current: String) {
    ui.label(label);
    ui.monospace(loaded);
    ui.monospace(current);
    ui.end_row();
}

fn metadata_size(metadata: &FileComparisonMetadata) -> String {
    metadata
        .byte_len
        .map(|bytes| bytes.to_string())
        .unwrap_or_else(|| "-".into())
}

fn metadata_modified(metadata: &FileComparisonMetadata) -> String {
    metadata
        .modified_unix_millis
        .map(|millis| format!("{}.{:03}", millis / 1_000, millis % 1_000))
        .unwrap_or_else(|| "-".into())
}

fn metadata_permissions(metadata: &FileComparisonMetadata) -> String {
    if metadata.readonly {
        "read-only".into()
    } else {
        "writable".into()
    }
}

fn resolve_conflict_reload(world: &mut World, conflict: &PendingFileConflict) {
    let result = match &conflict.intent {
        FileWriteIntent::Scene { tab_id, .. } => {
            crate::scene_io::reload_scene_after_file_conflict(world, *tab_id, &conflict.path)
        }
        FileWriteIntent::Material | FileWriteIntent::MaterialInstance => {
            crate::ui::reload_material_after_file_conflict(world, &conflict.path, &conflict.intent)
        }
        FileWriteIntent::ProjectSettings => {
            crate::settings_ui::reload_project_settings_after_file_conflict(world, &conflict.path)
        }
        FileWriteIntent::PrefabSource { instance_root }
        | FileWriteIntent::PrefabSourceHistory { instance_root } => {
            crate::assets::prefab_overrides::reload_prefab_source_after_file_conflict(
                world,
                *instance_root,
            )
        }
        FileWriteIntent::Standalone { .. } => {
            Err("This output is not an open editor document".into())
        }
    };
    match result {
        Ok(status) => {
            clear_file_conflict(world);
            if let Some(mut state) = world.get_resource_mut::<CollaborationState>() {
                state.request_refresh();
            }
            world.resource_mut::<SceneIo>().set_status(status);
        }
        Err(error) => world
            .resource_mut::<SceneIo>()
            .set_status(format!("Reload failed: {error}")),
    }
}

fn resolve_conflict_save_as(world: &mut World, conflict: &PendingFileConflict) {
    let directory = conflict.path.parent().unwrap_or_else(|| Path::new("."));
    let file_name = conflict_copy_file_name(&conflict.path);
    let Some(destination) = rfd::FileDialog::new()
        .set_directory(directory)
        .set_file_name(file_name)
        .save_file()
    else {
        world
            .resource_mut::<SceneIo>()
            .set_status("Save conflict copy cancelled");
        return;
    };
    let project_root = world
        .get_resource::<CollaborationState>()
        .map(|state| state.project_root.clone())
        .unwrap_or_else(|| PathBuf::from("."));
    if paths_equivalent(&project_root, &conflict.path, &destination) {
        world.resource_mut::<SceneIo>().set_status(
            "Save As must use a different path; the conflicted source was not overwritten",
        );
        return;
    }
    let material_catalog_path = if matches!(
        conflict.intent,
        FileWriteIntent::Material | FileWriteIntent::MaterialInstance
    ) {
        match crate::ui::validate_material_conflict_destination(world, &destination) {
            Ok(path) => Some(path),
            Err(error) => {
                world.resource_mut::<SceneIo>().set_status(error);
                return;
            }
        }
    } else {
        None
    };
    let expected = match FileSnapshot::capture(&destination) {
        Ok(expected) => expected,
        Err(error) => {
            world.resource_mut::<SceneIo>().set_status(error);
            return;
        }
    };
    let disk_snapshot = match publish_authored_file(
        world,
        &destination,
        &conflict.proposed_bytes,
        &expected,
        conflict.intent.clone(),
    ) {
        Ok(snapshot) => snapshot,
        Err(error) => {
            world
                .resource_mut::<SceneIo>()
                .set_status(format!("Save As failed: {error}"));
            return;
        }
    };

    let result = match &conflict.intent {
        FileWriteIntent::Scene {
            tab_id,
            entity_count,
        } => crate::scene_io::adopt_scene_conflict_save_as(
            world,
            *tab_id,
            destination,
            disk_snapshot,
            *entity_count,
        ),
        FileWriteIntent::Material | FileWriteIntent::MaterialInstance => {
            crate::ui::adopt_material_conflict_save_as(
                world,
                &conflict.path,
                material_catalog_path.expect("validated material path"),
                disk_snapshot,
                &conflict.intent,
            )
        }
        FileWriteIntent::ProjectSettings => {
            crate::settings_ui::project_settings_conflict_copy_saved(world);
            Ok(format!(
                "Saved project settings copy to {}; active project manifest unchanged",
                destination.display()
            ))
        }
        FileWriteIntent::PrefabSource { .. } | FileWriteIntent::PrefabSourceHistory { .. } => {
            Ok(format!(
                "Saved proposed prefab source copy to {}; original link unchanged",
                destination.display()
            ))
        }
        FileWriteIntent::Standalone { description } => Ok(format!(
            "Saved {description} copy to {}",
            destination.display()
        )),
    };
    clear_file_conflict(world);
    match result {
        Ok(status) => world.resource_mut::<SceneIo>().set_status(status),
        Err(error) => world.resource_mut::<SceneIo>().set_status(format!(
            "Saved the copy, but editor state could not adopt it: {error}"
        )),
    }
}

fn conflict_copy_file_name(path: &Path) -> String {
    let stem = path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or("authored-file");
    path.extension()
        .and_then(|extension| extension.to_str())
        .map(|extension| format!("{stem}-copy.{extension}"))
        .unwrap_or_else(|| format!("{stem}-copy"))
}

fn paths_equivalent(project_root: &Path, first: &Path, second: &Path) -> bool {
    let absolute = |path: &Path| {
        let path = if path.is_absolute() {
            path.to_path_buf()
        } else {
            project_root.join(path)
        };
        path.canonicalize()
            .unwrap_or_else(|_| lexical_normalize(&path))
    };
    absolute(first) == absolute(second)
}

fn clear_file_conflict(world: &mut World) {
    if let Some(mut state) = world.get_resource_mut::<CollaborationState>() {
        state.pending_conflict = None;
    }
}

fn queue_conflict(world: &mut World, conflict: PendingFileConflict) {
    if matches!(conflict.intent, FileWriteIntent::Scene { .. }) {
        let active_path = world.resource::<SceneIo>().active_path.clone();
        if active_path.is_none() || active_path.as_deref() == Some(&conflict.path) {
            world.resource_mut::<SceneIo>().mark_dirty();
        }
    }
    if let Some(mut state) = world.get_resource_mut::<CollaborationState>() {
        state.pending_conflict = Some(conflict);
    }
}

fn tick_collaboration_status(world: &mut World) {
    finish_scan_if_ready(world);

    let project_root = PathBuf::from(&world.resource::<ProjectWorkspace>().root);
    let project_root = project_root.canonicalize().unwrap_or(project_root);
    let tracked_paths = collect_tracked_paths(world, &project_root);
    let should_start = {
        let mut state = world.resource_mut::<CollaborationState>();
        if state.project_root != project_root {
            state.project_root.clone_from(&project_root);
            state.git_availability = GitAvailability::Unavailable;
            state.git_files.clear();
            state.ownership.clear();
            state.observed_files.clear();
            state.request_refresh();
        }
        if state.tracked_paths != tracked_paths {
            state.tracked_paths.clone_from(&tracked_paths);
            state.request_refresh();
        }
        state.scan_job.is_none()
            && state
                .last_scan_started
                .is_none_or(|started| started.elapsed() >= STATUS_REFRESH_INTERVAL)
    };
    if !should_start {
        return;
    }

    let providers = world.resource::<FileOwnershipProviders>().providers.clone();
    let job = spawn_collaboration_scan(project_root, tracked_paths, providers);
    let mut state = world.resource_mut::<CollaborationState>();
    state.scan_job = Some(job);
    state.last_scan_started = Some(Instant::now());
}

fn finish_scan_if_ready(world: &mut World) {
    let completed = world
        .get_resource::<CollaborationState>()
        .and_then(|state| state.scan_job.as_ref())
        .and_then(|job| job.result.lock().ok()?.take());
    if let Some(scan) = completed {
        let job = world.resource_mut::<CollaborationState>().scan_job.take();
        drop(job);
        let mut state = world.resource_mut::<CollaborationState>();
        state.git_availability = scan.git_availability;
        state.git_files = scan.git_files;
        state.ownership = scan.ownership;
        state.provider_errors = scan.provider_errors;
        state.observed_files = scan.observed_files;
        return;
    }

    let worker_finished = world
        .get_resource::<CollaborationState>()
        .and_then(|state| state.scan_job.as_ref())
        .and_then(|job| job.worker.as_ref())
        .is_some_and(JoinHandle::is_finished);
    if worker_finished {
        let job = world.resource_mut::<CollaborationState>().scan_job.take();
        drop(job);
        let mut state = world.resource_mut::<CollaborationState>();
        state.git_availability = GitAvailability::Error(
            "Collaboration status worker stopped before returning a result".into(),
        );
        state.last_scan_started = Some(Instant::now());
    }
}

fn collect_tracked_paths(world: &mut World, project_root: &Path) -> Vec<PathBuf> {
    let mut paths = HashSet::new();
    for tab in &world.resource::<SceneIo>().tabs {
        if let Some(path) = tab
            .path
            .as_deref()
            .and_then(|path| project_relative_path(project_root, path))
        {
            paths.insert(path);
        }
    }
    if let Some(path) = world
        .resource::<EditorAssets>()
        .selected
        .as_ref()
        .and_then(|selection| selection.parent_path())
        .and_then(|path| project_relative_path(project_root, Path::new(path)))
    {
        paths.insert(path);
    }
    if let Some(path) = world
        .get_resource::<settings::ProjectSettingsIo>()
        .and_then(|io| project_relative_path(project_root, Path::new(&io.path)))
    {
        paths.insert(path);
    }
    let prefab_sources: Vec<String> = world
        .query::<&PrefabInstance>()
        .iter(world)
        .map(|instance| instance.source_path.clone())
        .collect();
    for path in prefab_sources {
        if let Some(path) = project_relative_path(project_root, Path::new(&path)) {
            paths.insert(path);
        }
    }
    let mut paths: Vec<_> = paths.into_iter().collect();
    paths.sort();
    paths
}

fn spawn_collaboration_scan(
    project_root: PathBuf,
    tracked_paths: Vec<PathBuf>,
    providers: Vec<Arc<dyn FileOwnershipProvider>>,
) -> CollaborationScanJob {
    let result = Arc::new(Mutex::new(None));
    let worker_result = Arc::clone(&result);
    let worker = std::thread::spawn(move || {
        let mut scan = scan_git_status(&project_root, &tracked_paths);
        scan.observed_files = tracked_paths
            .iter()
            .filter_map(|path| {
                let metadata = fs::metadata(project_root.join(path)).ok()?;
                Some((
                    path.clone(),
                    ObservedFileMetadata {
                        readonly: metadata.permissions().readonly(),
                    },
                ))
            })
            .collect();
        let (ownership, provider_errors) =
            query_ownership_providers(&providers, &project_root, &tracked_paths);
        scan.ownership = ownership;
        scan.provider_errors = provider_errors;
        if let Ok(mut slot) = worker_result.lock() {
            *slot = Some(scan);
        }
    });
    CollaborationScanJob {
        result,
        worker: Some(worker),
    }
}

fn query_ownership_providers(
    providers: &[Arc<dyn FileOwnershipProvider>],
    project_root: &Path,
    tracked_paths: &[PathBuf],
) -> (HashMap<PathBuf, Vec<ProviderFileState>>, Vec<String>) {
    let mut statuses: HashMap<PathBuf, Vec<ProviderFileState>> = HashMap::new();
    let mut errors = Vec::new();
    for provider in providers {
        let name = provider.name().to_string();
        match provider.query(project_root, tracked_paths) {
            Ok(results) => {
                for result in results {
                    let Some(path) = project_relative_path(project_root, &result.path) else {
                        errors.push(format!(
                            "{name} returned a path outside the project: {}",
                            result.path.display()
                        ));
                        continue;
                    };
                    statuses.entry(path).or_default().push(ProviderFileState {
                        provider: name.clone(),
                        owner: result.owner,
                        locked_by_other: result.locked_by_other,
                        detail: result.detail,
                    });
                }
            }
            Err(error) => errors.push(format!("{name}: {error}")),
        }
    }
    (statuses, errors)
}

fn scan_git_status(project_root: &Path, tracked_paths: &[PathBuf]) -> CollaborationScan {
    let mut scan = CollaborationScan::default();
    let root_output = match Command::new("git")
        .env("GIT_OPTIONAL_LOCKS", "0")
        .arg("-C")
        .arg(project_root)
        .args(["rev-parse", "--show-toplevel"])
        .output()
    {
        Ok(output) => output,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            scan.git_availability = GitAvailability::Unavailable;
            return scan;
        }
        Err(error) => {
            scan.git_availability =
                GitAvailability::Error(format!("Git status unavailable: {error}"));
            return scan;
        }
    };
    if !root_output.status.success() {
        scan.git_availability = GitAvailability::NotRepository;
        return scan;
    }
    let git_root = PathBuf::from(String::from_utf8_lossy(&root_output.stdout).trim());
    let git_root = git_root.canonicalize().unwrap_or(git_root);
    let project_prefix = project_root
        .strip_prefix(&git_root)
        .unwrap_or_else(|_| Path::new("."));
    let project_prefix = if project_prefix.as_os_str().is_empty() {
        Path::new(".")
    } else {
        project_prefix
    };
    let mut command = Command::new("git");
    command
        .env("GIT_OPTIONAL_LOCKS", "0")
        .arg("-C")
        .arg(&git_root)
        .args([
            "status",
            "--porcelain=v1",
            "-z",
            "--untracked-files=all",
            "--ignored=no",
            "--",
        ]);
    if tracked_paths.is_empty() {
        command.arg(project_prefix);
    } else {
        command.args(tracked_paths.iter().map(|path| project_prefix.join(path)));
    }
    let status_output = match command.output() {
        Ok(output) if output.status.success() => output,
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            scan.git_availability = GitAvailability::Error(if stderr.is_empty() {
                "Git status failed".into()
            } else {
                format!("Git status failed: {stderr}")
            });
            return scan;
        }
        Err(error) => {
            scan.git_availability =
                GitAvailability::Error(format!("Git status unavailable: {error}"));
            return scan;
        }
    };

    scan.git_availability = GitAvailability::Available;
    for (git_path, state) in parse_porcelain_v1_z(&status_output.stdout) {
        let absolute = git_root.join(git_path);
        let Some(relative) = project_relative_path(project_root, &absolute) else {
            continue;
        };
        scan.git_files
            .entry(relative)
            .and_modify(|current| {
                if state.priority() > current.priority() {
                    *current = state;
                }
            })
            .or_insert(state);
    }
    scan
}

fn parse_porcelain_v1_z(bytes: &[u8]) -> Vec<(PathBuf, GitFileState)> {
    let mut records = bytes
        .split(|byte| *byte == 0)
        .filter(|record| !record.is_empty());
    let mut statuses = Vec::new();
    while let Some(record) = records.next() {
        if record.len() < 4 || record[2] != b' ' {
            continue;
        }
        let x = record[0];
        let y = record[1];
        let state = git_file_state(x, y);
        statuses.push((path_from_git_bytes(&record[3..]), state));
        if matches!(x, b'R' | b'C') || matches!(y, b'R' | b'C') {
            let _ = records.next();
        }
    }
    statuses
}

fn git_file_state(x: u8, y: u8) -> GitFileState {
    if x == b'?' && y == b'?' {
        return GitFileState::Untracked;
    }
    if x == b'U'
        || y == b'U'
        || matches!(
            (x, y),
            (b'A', b'A') | (b'D', b'D') | (b'A', b'U') | (b'U', b'A') | (b'D', b'U') | (b'U', b'D')
        )
    {
        return GitFileState::Conflicted;
    }
    GitFileState::Modified
}

#[cfg(unix)]
fn path_from_git_bytes(bytes: &[u8]) -> PathBuf {
    use std::ffi::OsString;
    use std::os::unix::ffi::OsStringExt;
    PathBuf::from(OsString::from_vec(bytes.to_vec()))
}

#[cfg(not(unix))]
fn path_from_git_bytes(bytes: &[u8]) -> PathBuf {
    PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
}

fn project_relative_path(project_root: &Path, path: &Path) -> Option<PathBuf> {
    if project_root.as_os_str().is_empty() || path.as_os_str().is_empty() {
        return None;
    }
    let root = lexical_normalize(project_root);
    let absolute = if path.is_absolute() {
        lexical_normalize(path)
    } else {
        lexical_normalize(&root.join(path))
    };
    let relative = absolute.strip_prefix(&root).ok()?;
    if relative.as_os_str().is_empty() {
        None
    } else {
        Some(relative.to_path_buf())
    }
}

fn lexical_normalize(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                normalized.pop();
            }
            other => normalized.push(other.as_os_str()),
        }
    }
    normalized
}

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

    fn temp_directory(label: &str) -> PathBuf {
        std::env::temp_dir().join(format!("blacksite-{label}-{}", uuid::Uuid::new_v4()))
    }

    #[test]
    fn guarded_write_detects_external_change_before_publish() {
        let root = temp_directory("guarded-external-change");
        let path = root.join("scene.scn.ron");
        fs::create_dir_all(&root).unwrap();
        fs::write(&path, b"loaded").unwrap();
        let expected = FileSnapshot::capture(&path).unwrap();

        let result = guarded_atomic_write_inner(&path, b"editor", &expected, || {
            fs::write(&path, b"external").unwrap();
        });

        assert!(matches!(result, Err(GuardedWriteError::Changed { .. })));
        assert_eq!(fs::read(&path).unwrap(), b"external");
        assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn guarded_create_does_not_replace_a_file_that_appears_during_save() {
        let root = temp_directory("guarded-create-race");
        let path = root.join("new-material.ron");
        fs::create_dir_all(&root).unwrap();
        let expected = FileSnapshot::missing();

        let result = guarded_atomic_write_inner(&path, b"editor", &expected, || {
            fs::write(&path, b"external").unwrap();
        });

        assert!(matches!(result, Err(GuardedWriteError::Changed { .. })));
        assert_eq!(fs::read(&path).unwrap(), b"external");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn identical_content_revision_ignores_metadata_drift() {
        let root = temp_directory("guarded-content-identity");
        let path = root.join("material.ron");
        fs::create_dir_all(&root).unwrap();
        fs::write(&path, b"same").unwrap();
        let expected = FileSnapshot::capture(&path).unwrap();
        fs::write(&path, b"same").unwrap();

        guarded_atomic_write(&path, b"replacement", &expected).unwrap();

        assert_eq!(fs::read(&path).unwrap(), b"replacement");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn read_only_target_is_blocked_without_replacement() {
        let root = temp_directory("guarded-read-only");
        let path = root.join("material.ron");
        fs::create_dir_all(&root).unwrap();
        fs::write(&path, b"loaded").unwrap();
        let expected = FileSnapshot::capture(&path).unwrap();
        let original_permissions = fs::metadata(&path).unwrap().permissions();
        let mut read_only_permissions = original_permissions.clone();
        read_only_permissions.set_readonly(true);
        fs::set_permissions(&path, read_only_permissions).unwrap();

        let result = guarded_atomic_write(&path, b"editor", &expected);

        assert!(matches!(result, Err(GuardedWriteError::ReadOnly { .. })));
        assert_eq!(fs::read(&path).unwrap(), b"loaded");
        fs::set_permissions(&path, original_permissions).unwrap();
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn provider_lock_queues_recovery_without_writing() {
        let root = temp_directory("provider-lock");
        let path = root.join("assets/levels/locked.scn.ron");
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(&path, b"loaded").unwrap();
        let expected = FileSnapshot::capture(&path).unwrap();
        let mut state = CollaborationState {
            project_root: root.clone(),
            ..Default::default()
        };
        state.ownership.insert(
            PathBuf::from("assets/levels/locked.scn.ron"),
            vec![ProviderFileState {
                provider: "test-provider".into(),
                owner: Some("teammate".into()),
                locked_by_other: true,
                detail: Some("exclusive edit".into()),
            }],
        );
        let mut world = World::new();
        world.insert_resource(state);

        let result = publish_authored_file(
            &mut world,
            &path,
            b"editor",
            &expected,
            FileWriteIntent::Standalone {
                description: "test document".into(),
            },
        );

        assert!(result.unwrap_err().contains("locked by another owner"));
        assert_eq!(fs::read(&path).unwrap(), b"loaded");
        assert!(matches!(
            world
                .resource::<CollaborationState>()
                .pending_conflict
                .as_ref()
                .map(|conflict| &conflict.reason),
            Some(FileConflictReason::ProviderLocked { .. })
        ));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn porcelain_parser_handles_spaces_conflicts_and_rename_records() {
        let bytes = b" M assets/levels/main scene.scn.ron\0?? assets/new.ron\0UU assets/conflict.ron\0R  assets/new-name.ron\0assets/old-name.ron\0";
        let parsed = parse_porcelain_v1_z(bytes);

        assert_eq!(
            parsed,
            vec![
                (
                    PathBuf::from("assets/levels/main scene.scn.ron"),
                    GitFileState::Modified
                ),
                (PathBuf::from("assets/new.ron"), GitFileState::Untracked),
                (
                    PathBuf::from("assets/conflict.ron"),
                    GitFileState::Conflicted
                ),
                (PathBuf::from("assets/new-name.ron"), GitFileState::Modified),
            ]
        );
    }

    #[test]
    fn git_scan_reports_untracked_files_without_creating_an_index() {
        if Command::new("git").arg("--version").output().is_err() {
            return;
        }
        let root = temp_directory("git-observation");
        fs::create_dir_all(root.join("assets")).unwrap();
        let initialized = Command::new("git")
            .arg("init")
            .arg("--quiet")
            .arg(&root)
            .status()
            .unwrap();
        assert!(initialized.success());
        fs::write(root.join("assets/untracked.ron"), b"()").unwrap();
        assert!(!root.join(".git/index").exists());

        fs::write(root.join("assets/unrelated.ron"), b"()").unwrap();
        let scan = scan_git_status(&root, &[PathBuf::from("assets/untracked.ron")]);

        assert_eq!(scan.git_availability, GitAvailability::Available);
        assert_eq!(
            scan.git_files.get(Path::new("assets/untracked.ron")),
            Some(&GitFileState::Untracked)
        );
        assert!(!scan
            .git_files
            .contains_key(Path::new("assets/unrelated.ron")));
        assert!(!root.join(".git/index").exists());
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn non_repository_scan_is_a_quiet_normal_state() {
        if Command::new("git").arg("--version").output().is_err() {
            return;
        }
        let root = temp_directory("git-absent-repository");
        fs::create_dir_all(&root).unwrap();

        let scan = scan_git_status(&root, &[]);

        assert_eq!(scan.git_availability, GitAvailability::NotRepository);
        assert!(scan.git_files.is_empty());
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn collaboration_tick_publishes_status_for_a_tracked_scene() {
        if Command::new("git").arg("--version").output().is_err() {
            return;
        }
        let root = temp_directory("git-worker-integration");
        let scene_path = root.join("assets/levels/tracked.scn.ron");
        fs::create_dir_all(scene_path.parent().unwrap()).unwrap();
        fs::write(&scene_path, b"()").unwrap();
        let initialized = Command::new("git")
            .arg("init")
            .arg("--quiet")
            .arg(&root)
            .status()
            .unwrap();
        assert!(initialized.success());

        let workspace = ProjectWorkspace {
            root: root.to_string_lossy().into_owned(),
            ..Default::default()
        };
        let mut scene_io = SceneIo::default();
        scene_io.tabs[0].path = Some(scene_path.clone());
        let editor_assets = EditorAssets {
            folders: Vec::new(),
            assets: Vec::new(),
            current_folder: "assets".into(),
            selected: None,
            dragging: None,
            status: String::new(),
        };
        let mut world = World::new();
        world.insert_resource(workspace);
        world.insert_resource(scene_io);
        world.insert_resource(editor_assets);
        world.insert_resource(FileOwnershipProviders::default());
        world.insert_resource(CollaborationState::default());

        tick_collaboration_status(&mut world);
        for _ in 0..250 {
            finish_scan_if_ready(&mut world);
            if world.resource::<CollaborationState>().scan_job.is_none() {
                break;
            }
            std::thread::sleep(Duration::from_millis(2));
        }

        let status = world
            .resource::<CollaborationState>()
            .file_status(&scene_path);
        assert_eq!(status.git, Some(GitFileState::Untracked));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn empty_provider_registry_is_a_quiet_noop() {
        let (statuses, errors) = query_ownership_providers(&[], Path::new("/project"), &[]);
        assert!(statuses.is_empty());
        assert!(errors.is_empty());
    }

    #[test]
    fn failed_status_worker_is_retired_and_scheduled_for_retry() {
        let result = Arc::new(Mutex::new(None));
        let worker = std::thread::spawn(|| {});
        while !worker.is_finished() {
            std::thread::yield_now();
        }
        let state = CollaborationState {
            scan_job: Some(CollaborationScanJob {
                result,
                worker: Some(worker),
            }),
            last_scan_started: Some(Instant::now()),
            ..Default::default()
        };
        let mut world = World::new();
        world.insert_resource(state);

        finish_scan_if_ready(&mut world);

        let state = world.resource::<CollaborationState>();
        assert!(state.scan_job.is_none());
        assert!(state.last_scan_started.is_some());
        assert!(matches!(state.git_availability, GitAvailability::Error(_)));
    }

    #[test]
    fn status_indicator_prioritizes_blocking_state() {
        let status = FileCollaborationStatus {
            git: Some(GitFileState::Modified),
            readonly: true,
            ..Default::default()
        };
        assert_eq!(
            status.indicator(),
            Some(FileStatusIndicator {
                label: "READ ONLY",
                tone: FileStatusTone::Error,
            })
        );
    }

    #[test]
    fn conflict_modal_renders_without_mutating_the_pending_write() {
        let path = PathBuf::from("assets/levels/main.scn.ron");
        let state = CollaborationState {
            pending_conflict: Some(PendingFileConflict {
                path: path.clone(),
                expected: FileSnapshot::missing(),
                current: FileSnapshot {
                    revision: FileRevision::Present([7; 32]),
                    metadata: FileComparisonMetadata {
                        byte_len: Some(42),
                        modified_unix_millis: Some(1_000),
                        readonly: false,
                    },
                },
                reason: FileConflictReason::ExternalChange,
                intent: FileWriteIntent::Standalone {
                    description: "test scene".into(),
                },
                proposed_bytes: b"editor".to_vec(),
                show_metadata: true,
            }),
            ..Default::default()
        };
        let mut world = World::new();
        world.insert_resource(state);
        world.insert_resource(SceneIo::default());
        let context = egui::Context::default();
        let mut fonts = egui::FontDefinitions::default();
        egui_phosphor_icons::add_fonts(&mut fonts);
        context.set_fonts(fonts);
        context.begin_pass(egui::RawInput::default());

        file_conflict_modal(&mut world, &context);
        let output = context.end_pass();

        assert!(!output.shapes.is_empty());
        assert!(world
            .resource::<CollaborationState>()
            .pending_conflict
            .is_some());
    }

    #[test]
    fn project_relative_paths_reject_escape_and_accept_absolute_project_files() {
        let root = Path::new("/project");
        assert_eq!(
            project_relative_path(root, Path::new("assets/a.ron")),
            Some(PathBuf::from("assets/a.ron"))
        );
        assert_eq!(
            project_relative_path(root, Path::new("/project/assets/a.ron")),
            Some(PathBuf::from("assets/a.ron"))
        );
        assert_eq!(
            project_relative_path(root, Path::new("../outside.ron")),
            None
        );
    }
}
