use std::path::Path;
use std::process::Command;
use std::time::Duration;

use anyhow::Context;
use anyhow::Result;
use app_test_support::TestAppServer;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::MarketplaceUpgradeParams;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::RequestId;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;

#[cfg(windows)]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25);
#[cfg(not(windows))]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";

fn run_git(cwd: &Path, args: &[&str]) -> Result<String> {
    let output = Command::new("git").current_dir(cwd).args(args).output()?;
    if !output.status.success() {
        anyhow::bail!(
            "git {} failed in {}: {}",
            args.join(" "),
            cwd.display(),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

fn write_marketplace_files(root: &Path, marketplace_name: &str, marker: &str) -> Result<()> {
    std::fs::create_dir_all(root.join(".agents/plugins"))?;
    std::fs::write(
        root.join(".agents/plugins/marketplace.json"),
        format!(r#"{{"name":"{marketplace_name}","plugins":[]}}"#),
    )?;
    std::fs::write(root.join("marker.txt"), marker)?;
    Ok(())
}

fn init_marketplace_repo(root: &Path, marketplace_name: &str, marker: &str) -> Result<String> {
    run_git(root, &["init"])?;
    run_git(root, &["config", "user.email", "codex@example.com"])?;
    run_git(root, &["config", "user.name", "Codex Tests"])?;
    write_marketplace_files(root, marketplace_name, marker)?;
    run_git(root, &["add", "."])?;
    run_git(root, &["commit", "-m", "initial marketplace"])?;
    run_git(root, &["rev-parse", "HEAD"])
}

fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String> {
    std::fs::write(root.join("marker.txt"), marker)?;
    run_git(root, &["add", "marker.txt"])?;
    run_git(root, &["commit", "-m", "update marker"])?;
    run_git(root, &["rev-parse", "HEAD"])
}

fn configured_git_marketplace_update<'a>(
    source: &'a str,
    last_revision: Option<&'a str>,
    ref_name: Option<&'a str>,
) -> MarketplaceConfigUpdate<'a> {
    MarketplaceConfigUpdate {
        last_updated: "2026-04-13T00:00:00Z",
        last_revision,
        source_type: "git",
        source,
        ref_name,
        sparse_paths: &[],
    }
}

fn configured_local_marketplace_update(source: &str) -> MarketplaceConfigUpdate<'_> {
    MarketplaceConfigUpdate {
        last_updated: "2026-04-13T00:00:00Z",
        last_revision: None,
        source_type: "local",
        source,
        ref_name: None,
        sparse_paths: &[],
    }
}

fn record_git_marketplace(
    codex_home: &Path,
    marketplace_name: &str,
    source: &Path,
    last_revision: &str,
    ref_name: Option<&str>,
) -> Result<()> {
    let source = source.display().to_string();
    record_user_marketplace(
        codex_home,
        marketplace_name,
        &configured_git_marketplace_update(&source, Some(last_revision), ref_name),
    )?;
    Ok(())
}

fn disable_plugin_startup_tasks(codex_home: &Path) -> Result<()> {
    let config_path = codex_home.join("config.toml");
    let config = std::fs::read_to_string(&config_path)?;
    std::fs::write(
        config_path,
        format!("{config}\n[features]\nplugins = false\n"),
    )?;
    Ok(())
}

fn marketplace_install_root(codex_home: &Path) -> std::path::PathBuf {
    codex_home.join(INSTALLED_MARKETPLACES_DIR)
}

fn expected_installed_root(codex_home: &Path, marketplace_name: &str) -> Result<AbsolutePathBuf> {
    AbsolutePathBuf::try_from(
        marketplace_install_root(&codex_home.canonicalize()?).join(marketplace_name),
    )
    .context("expected installed root should be absolute")
}

async fn send_marketplace_upgrade(
    mcp: &mut TestAppServer,
    marketplace_name: Option<&str>,
) -> Result<MarketplaceUpgradeResponse> {
    let request_id = mcp
        .send_marketplace_upgrade_request(MarketplaceUpgradeParams {
            marketplace_name: marketplace_name.map(str::to_string),
        })
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    to_response(response)
}

#[tokio::test]
async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> {
    let codex_home = TempDir::new()?;
    let debug_source = TempDir::new()?;
    let tools_source = TempDir::new()?;
    let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?;
    let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?;
    let debug_new_revision = commit_marketplace_marker(debug_source.path(), "debug new")?;
    let tools_new_revision = commit_marketplace_marker(tools_source.path(), "tools new")?;
    record_git_marketplace(
        codex_home.path(),
        "debug",
        debug_source.path(),
        &debug_old_revision,
        Some(&debug_new_revision),
    )?;
    record_git_marketplace(
        codex_home.path(),
        "tools",
        tools_source.path(),
        &tools_old_revision,
        Some(&tools_new_revision),
    )?;
    disable_plugin_startup_tasks(codex_home.path())?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    let debug_root = expected_installed_root(codex_home.path(), "debug")?;
    let tools_root = expected_installed_root(codex_home.path(), "tools")?;
    let response = send_marketplace_upgrade(&mut mcp, /*marketplace_name*/ None).await?;

    assert_eq!(
        response,
        MarketplaceUpgradeResponse {
            selected_marketplaces: vec!["debug".to_string(), "tools".to_string()],
            upgraded_roots: vec![debug_root.clone(), tools_root.clone()],
            errors: Vec::new(),
        }
    );
    assert_eq!(
        std::fs::read_to_string(debug_root.as_path().join("marker.txt"))?,
        "debug new"
    );
    assert_eq!(
        std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?,
        "tools new"
    );
    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
    assert!(config.contains(&debug_new_revision));
    assert!(config.contains(&tools_new_revision));
    Ok(())
}

#[tokio::test]
async fn marketplace_upgrade_named_marketplace_only() -> Result<()> {
    let codex_home = TempDir::new()?;
    let debug_source = TempDir::new()?;
    let tools_source = TempDir::new()?;
    let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?;
    let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?;
    commit_marketplace_marker(debug_source.path(), "debug new")?;
    commit_marketplace_marker(tools_source.path(), "tools new")?;
    record_git_marketplace(
        codex_home.path(),
        "debug",
        debug_source.path(),
        &debug_old_revision,
        /*ref_name*/ None,
    )?;
    record_git_marketplace(
        codex_home.path(),
        "tools",
        tools_source.path(),
        &tools_old_revision,
        /*ref_name*/ None,
    )?;
    disable_plugin_startup_tasks(codex_home.path())?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    let tools_root = expected_installed_root(codex_home.path(), "tools")?;
    let response = send_marketplace_upgrade(&mut mcp, Some("tools")).await?;

    assert_eq!(
        response,
        MarketplaceUpgradeResponse {
            selected_marketplaces: vec!["tools".to_string()],
            upgraded_roots: vec![tools_root.clone()],
            errors: Vec::new(),
        }
    );
    assert_eq!(
        std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?,
        "tools new"
    );
    assert!(
        !marketplace_install_root(codex_home.path())
            .join("debug")
            .exists()
    );
    Ok(())
}

#[tokio::test]
async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Result<()> {
    let codex_home = TempDir::new()?;
    let source = TempDir::new()?;
    let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?;
    commit_marketplace_marker(source.path(), "debug new")?;
    record_git_marketplace(
        codex_home.path(),
        "debug",
        source.path(),
        &old_revision,
        /*ref_name*/ None,
    )?;
    disable_plugin_startup_tasks(codex_home.path())?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let first_response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?;
    assert!(first_response.errors.is_empty());

    let response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?;

    assert_eq!(
        response,
        MarketplaceUpgradeResponse {
            selected_marketplaces: vec!["debug".to_string()],
            upgraded_roots: Vec::new(),
            errors: Vec::new(),
        }
    );
    Ok(())
}

#[tokio::test]
async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result<()> {
    let codex_home = TempDir::new()?;
    let local_source = TempDir::new()?;
    record_user_marketplace(
        codex_home.path(),
        "local-only",
        &configured_local_marketplace_update(&local_source.path().display().to_string()),
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    for marketplace_name in ["missing", "local-only"] {
        let request_id = mcp
            .send_marketplace_upgrade_request(MarketplaceUpgradeParams {
                marketplace_name: Some(marketplace_name.to_string()),
            })
            .await?;

        let err = timeout(
            DEFAULT_TIMEOUT,
            mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
        )
        .await??;

        assert_eq!(err.error.code, -32600);
        assert_eq!(
            err.error.message,
            format!("marketplace `{marketplace_name}` is not configured as a Git marketplace"),
        );
    }
    Ok(())
}
