use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use application::{
    InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions,
    ListPlugins, UninstallPlugin, UninstallPluginInput,
};
use infrastructure::{
    ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore, FsPluginStorageStore,
    TokioBroadcastEventBus,
};

static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

fn temp_dir(label: &str) -> PathBuf {
    let n = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
    let path = std::env::temp_dir().join(format!(
        "idea-plugin-functional-{label}-{}-{n}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&path);
    fs::create_dir_all(&path).unwrap();
    path
}

fn fixture_path(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("plugins")
        .join(name)
}

fn sdk_hello_plugin_path() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../..")
        .join("sdk/IdeaSDK/examples/hello-plugin")
}

fn materialize_sdk_hello_plugin() -> PathBuf {
    let source = sdk_hello_plugin_path();
    let root = temp_dir("hello-plugin-built");
    fs::create_dir_all(root.join("dist")).unwrap();
    fs::write(
        root.join("idea-plugin.json"),
        fs::read(source.join("idea-plugin.json")).unwrap(),
    )
    .unwrap();
    fs::write(
        root.join("dist/index.js"),
        "export function activate() { return 'hello-plugin'; }",
    )
    .unwrap();
    root
}

fn write_multifile_plugin(root: &Path) {
    fs::create_dir_all(root.join("dist/core")).unwrap();
    fs::write(
        root.join("idea-plugin.json"),
        r#"{
          "ideaPluginManifestVersion": 1,
          "id": "dev.acme.multifile",
          "displayName": "Multifile Plugin",
          "version": "0.1.0",
          "main": "dist/index.js",
          "trustLevel": "full",
          "capabilities": ["ui"],
          "contributes": {}
        }"#,
    )
    .unwrap();
    fs::write(
        root.join("dist/index.js"),
        "import { answer } from './constants.js'; export default answer;",
    )
    .unwrap();
    fs::write(root.join("dist/constants.js"), "export const answer = 42;").unwrap();
    fs::write(root.join("dist/core/util.js"), "export const util = true;").unwrap();
}

#[tokio::test]
async fn installs_reference_fixture_and_loads_runtime_catalog() {
    let app_data = temp_dir("app-data");
    let fixture = fixture_path("reference-minimal");
    let packages = Arc::new(FsPluginPackageStore::new(&app_data));
    let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
    let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
    let events = Arc::new(TokioBroadcastEventBus::new());
    let mcp = Arc::new(ExternalMcpPluginSupervisor::new());

    let install = InstallPluginFromDirectory::new(
        packages.clone(),
        registry.clone(),
        validator.clone(),
        events,
        mcp.clone(),
    );
    let result = install
        .execute(fixture.to_string_lossy().into_owned())
        .await
        .unwrap();

    assert_eq!(result.plugin.id, "dev.idea.fixtures.reference-minimal");
    assert_eq!(result.plugin.display_name, "Reference Minimal Plugin");
    assert_eq!(result.review.contribution_summary.top_level_menus, 1);
    assert_eq!(result.review.contribution_summary.menu_items, 1);
    assert!(result.restart_required);
    assert!(app_data
        .join("plugins/installed/dev.idea.fixtures.reference-minimal/idea-plugin.json")
        .is_file());

    let admin = ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
        .execute()
        .await
        .unwrap();
    assert_eq!(admin.len(), 1);
    assert_eq!(admin[0].id, "dev.idea.fixtures.reference-minimal");
    assert_eq!(
        admin[0].lifecycle_state,
        domain::PluginLifecycleState::Enabled
    );

    let catalog = ListPluginRuntimeContributions::new(packages, registry, validator)
        .execute()
        .await
        .unwrap();
    assert_eq!(catalog.plugins.len(), 1);
    let plugin = &catalog.plugins[0];
    assert_eq!(plugin.id, "dev.idea.fixtures.reference-minimal");
    assert!(plugin
        .bundle_url
        .starts_with("idea-plugin://dev.idea.fixtures.reference-minimal/0.1.0/"));
    assert_eq!(plugin.contributes.menus.len(), 1);
    assert_eq!(plugin.contributes.menu_items.len(), 1);

    let _ = fs::remove_dir_all(app_data);
}

#[tokio::test]
async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() {
    let app_data = temp_dir("hello-app-data");
    let hello_plugin = materialize_sdk_hello_plugin();
    let packages = Arc::new(FsPluginPackageStore::new(&app_data));
    let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
    let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
    let events = Arc::new(TokioBroadcastEventBus::new());
    let mcp = Arc::new(ExternalMcpPluginSupervisor::new());

    let install = InstallPluginFromDirectory::new(
        packages.clone(),
        registry.clone(),
        validator.clone(),
        events,
        mcp,
    );
    let result = install
        .execute(hello_plugin.to_string_lossy().into_owned())
        .await
        .unwrap();

    assert_eq!(result.plugin.id, "com.example.hello-plugin");
    assert_eq!(result.plugin.display_name, "Hello Plugin");
    assert_eq!(result.review.contribution_summary.top_level_menus, 1);
    assert_eq!(result.review.contribution_summary.menu_items, 1);
    assert_eq!(result.review.contribution_summary.layouts, 1);
    assert!(app_data
        .join("plugins/installed/com.example.hello-plugin/dist/index.js")
        .is_file());

    let catalog = ListPluginRuntimeContributions::new(packages, registry, validator)
        .execute()
        .await
        .unwrap();
    assert_eq!(catalog.plugins.len(), 1);
    let plugin = &catalog.plugins[0];
    assert_eq!(plugin.id, "com.example.hello-plugin");
    assert!(plugin
        .bundle_url
        .starts_with("idea-plugin://com.example.hello-plugin/0.1.0/"));
    assert!(plugin.bundle_url.ends_with("/dist/index.js"));
    assert_eq!(plugin.contributes.menus[0].id, "hello-plugin.menu");
    assert_eq!(plugin.contributes.menus[0].label.as_str(), "Hello Plugin");
    assert_eq!(
        plugin.contributes.menu_items[0].command.as_str(),
        "hello-plugin"
    );
    assert_eq!(
        plugin.contributes.layouts[0].layout_type.as_str(),
        "hello-plugin.hello-world"
    );
    assert_eq!(plugin.contributes.layouts[0].label.as_str(), "hello-world");

    let _ = fs::remove_dir_all(app_data);
    let _ = fs::remove_dir_all(hello_plugin);
}

#[tokio::test]
async fn uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() {
    let app_data = temp_dir("hello-reinstall-app-data");
    let hello_plugin = materialize_sdk_hello_plugin();
    let packages = Arc::new(FsPluginPackageStore::new(&app_data));
    let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
    let storage = Arc::new(FsPluginStorageStore::new(&app_data));
    let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
    let events = Arc::new(TokioBroadcastEventBus::new());
    let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
    let install = InstallPluginFromDirectory::new(
        packages.clone(),
        registry.clone(),
        validator.clone(),
        events.clone(),
        mcp.clone(),
    );
    let uninstall = UninstallPlugin::new(packages.clone(), storage, registry.clone(), events, mcp);

    install
        .execute(hello_plugin.to_string_lossy().into_owned())
        .await
        .unwrap();
    let uninstall_result = uninstall
        .execute(UninstallPluginInput {
            plugin_id: "com.example.hello-plugin".to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(
        uninstall_result.removal_outcome,
        domain::RemovalOutcome::Removed
    );
    assert!(!app_data
        .join("plugins/installed/com.example.hello-plugin")
        .exists());
    assert!(
        ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
            .execute()
            .await
            .unwrap()
            .is_empty()
    );
    assert!(ListPluginRuntimeContributions::new(
        packages.clone(),
        registry.clone(),
        validator.clone()
    )
    .execute()
    .await
    .unwrap()
    .plugins
    .is_empty());

    let reinstall = install
        .execute(hello_plugin.to_string_lossy().into_owned())
        .await
        .unwrap();
    assert_eq!(reinstall.plugin.id, "com.example.hello-plugin");
    assert_eq!(
        reinstall.plugin.lifecycle_state,
        domain::PluginLifecycleState::Enabled
    );
    let catalog = ListPluginRuntimeContributions::new(packages, registry, validator)
        .execute()
        .await
        .unwrap();
    assert_eq!(catalog.plugins.len(), 1);
    assert_eq!(catalog.plugins[0].id, "com.example.hello-plugin");
    assert!(catalog.plugins[0].bundle_url.ends_with("/dist/index.js"));

    let _ = fs::remove_dir_all(app_data);
    let _ = fs::remove_dir_all(hello_plugin);
}

#[tokio::test]
async fn uninstall_multifile_plugin_removes_package_registry_and_runtime_residue() {
    let app_data = temp_dir("multifile-uninstall-app-data");
    let source = temp_dir("multifile-source");
    write_multifile_plugin(&source);
    let packages = Arc::new(FsPluginPackageStore::new(&app_data));
    let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
    let storage = Arc::new(FsPluginStorageStore::new(&app_data));
    let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
    let events = Arc::new(TokioBroadcastEventBus::new());
    let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
    let install = InstallPluginFromDirectory::new(
        packages.clone(),
        registry.clone(),
        validator.clone(),
        events.clone(),
        mcp.clone(),
    );
    let uninstall = UninstallPlugin::new(packages.clone(), storage, registry.clone(), events, mcp);

    install
        .execute(source.to_string_lossy().into_owned())
        .await
        .unwrap();
    assert!(app_data
        .join("plugins/installed/dev.acme.multifile/dist/constants.js")
        .is_file());
    assert!(app_data
        .join("plugins/installed/dev.acme.multifile/dist/core/util.js")
        .is_file());
    let plugin_data = app_data.join("plugins/data/dev.acme.multifile");
    fs::create_dir_all(&plugin_data).unwrap();
    fs::write(plugin_data.join("state.json"), r#"{"launches":1}"#).unwrap();

    let uninstall_result = uninstall
        .execute(UninstallPluginInput {
            plugin_id: "dev.acme.multifile".to_owned(),
        })
        .await
        .unwrap();

    assert_eq!(
        uninstall_result.removal_outcome,
        domain::RemovalOutcome::Removed
    );
    assert!(!app_data
        .join("plugins/installed/dev.acme.multifile")
        .exists());
    assert!(!app_data.join("plugins/data/dev.acme.multifile").exists());
    assert!(
        ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
            .execute()
            .await
            .unwrap()
            .is_empty()
    );
    assert!(
        ListPluginRuntimeContributions::new(packages, registry, validator)
            .execute()
            .await
            .unwrap()
            .plugins
            .is_empty()
    );

    let _ = fs::remove_dir_all(app_data);
    let _ = fs::remove_dir_all(source);
}

#[tokio::test]
async fn installed_plugin_with_missing_main_is_isolated_from_runtime_catalog() {
    let app_data = temp_dir("missing-main-app-data");
    let source = temp_dir("missing-main-source");
    fs::write(
        source.join("idea-plugin.json"),
        r#"{
          "ideaPluginManifestVersion": 1,
          "id": "dev.idea.fixtures.missing-main",
          "displayName": "Missing Main Plugin",
          "version": "0.1.0",
          "main": "dist/index.js",
          "trustLevel": "full",
          "capabilities": ["ui"],
          "contributes": {}
        }"#,
    )
    .unwrap();
    let packages = Arc::new(FsPluginPackageStore::new(&app_data));
    let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
    let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
    let events = Arc::new(TokioBroadcastEventBus::new());
    let mcp = Arc::new(ExternalMcpPluginSupervisor::new());

    let install = InstallPluginFromDirectory::new(
        packages.clone(),
        registry.clone(),
        validator.clone(),
        events,
        mcp,
    );
    let result = install
        .execute(source.to_string_lossy().into_owned())
        .await
        .unwrap();
    assert_eq!(result.plugin.id, "dev.idea.fixtures.missing-main");
    assert_eq!(
        result.plugin.lifecycle_state,
        domain::PluginLifecycleState::Invalid
    );
    assert!(result
        .plugin
        .error
        .as_deref()
        .unwrap_or_default()
        .contains("not servable"));

    let catalog = ListPluginRuntimeContributions::new(packages, registry.clone(), validator)
        .execute()
        .await
        .unwrap();
    assert!(catalog.plugins.is_empty());
    let admin = ListPlugins::new(
        Arc::new(FsPluginPackageStore::new(&app_data)),
        registry,
        Arc::new(JsonPluginManifestValidator::new("0.3.0")),
    )
    .execute()
    .await
    .unwrap();
    assert_eq!(
        admin[0].lifecycle_state,
        domain::PluginLifecycleState::Invalid
    );
    assert!(admin[0]
        .error
        .as_deref()
        .unwrap_or_default()
        .contains("not servable"));

    let _ = fs::remove_dir_all(source);
    let _ = fs::remove_dir_all(app_data);
}
