//! E2E Tests for install.sh - Multi-Platform Binary Installer
//!
//! Tests the br installer script for platform detection, version resolution,
//! checksum verification, and various installation scenarios.
//!
//! Related bead: beads_rust-1g0q (Installer script: multi-platform binary downloader)
//!
//! Test Categories:
//! - Platform detection
//! - Version resolution (GitHub API and redirect fallback)
//! - Checksum verification (success and failure)
//! - Idempotent installation
//! - Error handling
//! - Proxy support
//!
//! Note: Many tests require network access and are skipped in offline CI environments.

mod common;

use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use tempfile::TempDir;

/// Check if we have network access (for tests that need GitHub API)
fn has_network() -> bool {
    Command::new("curl")
        .args(["-fsSL", "--connect-timeout", "5", "https://api.github.com"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .is_ok_and(|s| s.success())
}

/// Check if bash is available
fn has_bash() -> bool {
    Command::new("bash")
        .arg("--version")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .is_ok_and(|s| s.success())
}

/// Run install.sh with given arguments and environment variables
fn run_installer(temp_dir: &TempDir, args: &[&str], env_vars: HashMap<&str, &str>) -> Output {
    let install_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("install.sh");

    let dest_dir = temp_dir.path().join("bin");
    fs::create_dir_all(&dest_dir).expect("Failed to create dest dir");

    let mut cmd = Command::new("bash");
    cmd.arg(&install_script);
    cmd.args(args);

    // Set default environment
    cmd.env("HOME", temp_dir.path());
    cmd.env("DEST", &dest_dir);
    cmd.env("NO_GUM", "1"); // Disable fancy output for test parsing
    cmd.current_dir(temp_dir.path());

    // Clear potentially interfering variables
    cmd.env_remove("BR_INSTALL_DIR");
    cmd.env_remove("VERSION");

    // Add custom environment variables
    for (key, value) in env_vars {
        cmd.env(key, value);
    }

    cmd.output().expect("Failed to run installer")
}

/// Run a bash function from install.sh and capture output
fn run_installer_function(temp_dir: &TempDir, _function_name: &str, function_call: &str) -> Output {
    let install_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("install.sh");

    // Source the script and call the function
    let script = format!(
        r#"
        set +e
        source "{}" 2>/dev/null || true
        {}
        "#,
        install_script.display(),
        function_call
    );

    Command::new("bash")
        .args(["-c", &script])
        .env("HOME", temp_dir.path())
        .env("NO_GUM", "1")
        .env("QUIET", "1")
        .current_dir(temp_dir.path())
        .output()
        .expect("Failed to run installer function")
}

fn install_script_contents() -> String {
    let install_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("install.sh");
    fs::read_to_string(install_script).expect("read install.sh")
}

fn shell_function_section<'a>(script: &'a str, function_name: &str) -> &'a str {
    let marker = format!("{function_name}() {{");
    let start = script.find(&marker).expect("function marker") + marker.len();
    let tail = &script[start..];
    let end = tail
        .find("\n# ============================================================================")
        .unwrap_or(tail.len());
    &tail[..end]
}

// ============================================================================
// Platform Detection Tests
// ============================================================================

#[test]
fn e2e_installer_platform_detection_linux_x64() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Test that detect_platform works on current system
    let output = run_installer_function(&temp, "detect_platform", "detect_platform");

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let platform = stdout.trim();

        // Verify platform format: os_arch or os_libc_arch (the libc segment
        // is only emitted on Linux when a non-glibc libc is detected — see
        // detect_platform in install.sh and #284).
        assert!(
            platform.contains('_'),
            "Platform should be os[_libc]_arch format, got: {platform}"
        );

        let parts: Vec<&str> = platform.split('_').collect();
        assert!(
            parts.len() == 2 || parts.len() == 3,
            "Platform should have 2 or 3 parts, got {parts:?}"
        );

        let valid_os = ["linux", "darwin", "windows"];
        let valid_libc = ["musl"];
        let valid_arch = ["amd64", "arm64", "armv7"];

        assert!(
            valid_os.contains(&parts[0]),
            "Invalid OS: {}, expected one of {:?}",
            parts[0],
            valid_os
        );

        if parts.len() == 3 {
            assert_eq!(parts[0], "linux", "libc segment is only valid on Linux");
            assert!(
                valid_libc.contains(&parts[1]),
                "Invalid libc: {}, expected one of {:?}",
                parts[1],
                valid_libc
            );
            assert!(
                valid_arch.contains(&parts[2]),
                "Invalid arch: {}, expected one of {:?}",
                parts[2],
                valid_arch
            );
        } else {
            assert!(
                valid_arch.contains(&parts[1]),
                "Invalid arch: {}, expected one of {:?}",
                parts[1],
                valid_arch
            );
        }
    }
    // Note: Function might fail if sourcing install.sh has issues, that's OK for this test
}

#[test]
fn e2e_installer_detects_system_platform() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Run uname commands to verify platform detection matches system
    let os_output = Command::new("uname").arg("-s").output().expect("uname -s");
    let arch_output = Command::new("uname").arg("-m").output().expect("uname -m");

    let os_raw = String::from_utf8_lossy(&os_output.stdout)
        .trim()
        .to_lowercase();
    let arch_raw = String::from_utf8_lossy(&arch_output.stdout)
        .trim()
        .to_lowercase();

    // Map expected values
    let expected_os = match os_raw.as_str() {
        s if s.starts_with("linux") => "linux",
        s if s.starts_with("darwin") => "darwin",
        s if s.contains("mingw") || s.contains("msys") || s.contains("cygwin") => "windows",
        _ => &os_raw,
    };

    let expected_arch = match arch_raw.as_str() {
        "x86_64" | "amd64" => "amd64",
        "aarch64" | "arm64" => "arm64",
        s if s.starts_with("armv7") => "armv7",
        _ => &arch_raw,
    };

    let output = run_installer_function(&temp, "detect_platform", "detect_platform");
    if output.status.success() {
        let detected = String::from_utf8_lossy(&output.stdout).trim().to_string();
        // Accept either the 2-part (glibc / non-Linux) or 3-part (musl) form.
        // The exact libc segment depends on the host's libc — we don't try to
        // probe it here, only verify that the os/arch alignment is correct.
        let two_part = format!("{expected_os}_{expected_arch}");
        let three_part_musl = format!("{expected_os}_musl_{expected_arch}");

        assert!(
            detected == two_part || detected == three_part_musl,
            "Platform detection mismatch: got {detected}, expected {two_part} or {three_part_musl}"
        );
    }
}

// ============================================================================
// Version Resolution Tests
// ============================================================================

#[test]
fn e2e_installer_version_resolution_explicit() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Test with explicit version - should not call API
    let output = run_installer(&temp, &["--version", "v0.1.0", "--help"], HashMap::new());

    // --help should print usage regardless of version
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Help output should mention br installer
    assert!(
        stdout.contains("br installer") || stdout.contains("install") || stderr.contains("br"),
        "Help output should mention br installer"
    );
}

#[test]
#[ignore = "requires network access"]
fn e2e_installer_version_resolution_github_api() {
    if !has_bash() || !has_network() {
        eprintln!("Skipping test: bash or network not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Test version resolution from GitHub API
    let script = r#"
        QUIET=1
        NO_GUM=1
        MAX_RETRIES=1
        resolve_version
        echo "VERSION=$VERSION"
    "#;

    let install_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("install.sh");
    let full_script = format!(
        r#"source "{}" 2>/dev/null; {}"#,
        install_script.display(),
        script
    );

    let output = Command::new("bash")
        .args(["-c", &full_script])
        .env("HOME", temp.path())
        .env("NO_GUM", "1")
        .output()
        .expect("Failed to run version resolution");

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);

        // Should resolve to a version like v0.1.x
        if stdout.contains("VERSION=v") {
            let version = stdout
                .lines()
                .find(|l| l.starts_with("VERSION="))
                .and_then(|l| l.strip_prefix("VERSION="))
                .unwrap_or("");

            assert!(
                version.starts_with('v'),
                "Version should start with 'v', got: {version}"
            );
        }
    }
}

// ============================================================================
// Checksum Verification Tests
// ============================================================================

#[test]
fn e2e_installer_checksum_verification_sha256sum() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Create a test file
    let test_file = temp.path().join("test_file.txt");
    fs::write(&test_file, "hello world\n").expect("write test file");

    // Calculate expected checksum
    let sha_output = Command::new("sha256sum").arg(&test_file).output();

    if sha_output.is_err() {
        // Try shasum (macOS)
        let sha_output = Command::new("shasum")
            .args(["-a", "256"])
            .arg(&test_file)
            .output();

        if sha_output.is_err() {
            eprintln!("Skipping test: no sha256sum or shasum available");
            return;
        }
    }

    let sha_output = Command::new("sh")
        .args([
            "-c",
            &format!(
                "sha256sum '{}' 2>/dev/null || shasum -a 256 '{}'",
                test_file.display(),
                test_file.display()
            ),
        ])
        .output()
        .expect("sha256sum");

    let expected_hash = String::from_utf8_lossy(&sha_output.stdout)
        .split_whitespace()
        .next()
        .unwrap_or("")
        .to_string();

    assert!(!expected_hash.is_empty(), "Should get a hash");
    assert_eq!(expected_hash.len(), 64, "SHA256 should be 64 chars");
}

#[test]
fn e2e_installer_checksum_mismatch_fails() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let archive = temp.path().join("br-v0.0.0-linux_amd64.tar.gz");
    fs::write(&archive, b"not a release archive").expect("write fake archive");

    // Provide a bad checksum - installer should fail
    let bad_checksum = "0000000000000000000000000000000000000000000000000000000000000000";
    let artifact_url = format!("file://{}", archive.display());

    let output = run_installer(
        &temp,
        &[
            "--version",
            "v0.0.0",
            "--artifact-url",
            &artifact_url,
            "--checksum",
            bad_checksum,
            "--quiet",
            "--skip-skills",
        ],
        HashMap::new(),
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        !output.status.success(),
        "installer must fail closed on checksum mismatch; stdout={stdout}, stderr={stderr}"
    );
    assert!(
        stderr.contains("Checksum mismatch"),
        "stderr should explain the checksum mismatch; stdout={stdout}, stderr={stderr}"
    );
    assert!(
        stderr.contains("Release artifact verification failed"),
        "stderr should not fall back to source build after verification failure; stdout={stdout}, stderr={stderr}"
    );

    let binary_path = temp.path().join("bin").join("br");
    assert!(
        !binary_path.exists(),
        "checksum mismatch must not install a binary"
    );
}

// ============================================================================
// Idempotent Installation Tests
// ============================================================================

#[test]
fn e2e_installer_idempotent_runs_twice() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let dest = temp.path().join("bin");
    fs::create_dir_all(&dest).expect("create dest");

    // Create a fake "br" binary to simulate existing installation
    let fake_binary = dest.join("br");
    fs::write(&fake_binary, "#!/bin/sh\necho 'br 0.0.1'").expect("write fake");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&fake_binary, fs::Permissions::from_mode(0o755)).expect("chmod");
    }

    // First run
    let output1 = run_installer(&temp, &["--quiet"], HashMap::new());
    let stderr1 = String::from_utf8_lossy(&output1.stderr);

    // Second run - should not error out
    let output2 = run_installer(&temp, &["--quiet"], HashMap::new());
    let stderr2 = String::from_utf8_lossy(&output2.stderr);

    // Both runs should complete without critical errors
    // (Note: they may fail to download if no network, but shouldn't crash)
    assert!(
        !stderr1.contains("panic") && !stderr2.contains("panic"),
        "Installer should not panic on repeated runs: stderr1={stderr1}, stderr2={stderr2}"
    );
}

#[test]
fn e2e_installer_creates_dest_directory() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let nested_dest = temp.path().join("deeply").join("nested").join("bin");

    let mut env = HashMap::new();
    env.insert("DEST", nested_dest.to_str().unwrap());

    let _output = run_installer(&temp, &["--quiet"], env);

    // Installer should create the directory (even if download fails)
    // The mkdir -p happens early in the script
    // Note: Directory might not be created if script exits early due to lock
}

// ============================================================================
// Lock Mechanism Tests
// ============================================================================

#[test]
fn e2e_installer_lock_prevents_concurrent() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Create a stale lock directory
    let lock_dir = PathBuf::from("/tmp/br-install.lock.d");

    // Clean up any existing lock first
    let _ = fs::remove_dir_all(&lock_dir);

    // Create lock with a PID that doesn't exist
    fs::create_dir_all(&lock_dir).expect("create lock dir");
    fs::write(lock_dir.join("pid"), "999999999").expect("write stale pid");

    // Installer should detect stale lock and recover
    let output = run_installer(&temp, &["--help"], HashMap::new());

    // Clean up
    let _ = fs::remove_dir_all(&lock_dir);

    // Should have printed help (lock was stale and recovered)
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("br installer") || stdout.contains("--version"),
        "Should print help after recovering stale lock"
    );
}

// ============================================================================
// Uninstall Tests
// ============================================================================

#[test]
fn e2e_installer_uninstall_removes_binary() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let dest = temp.path().join("bin");
    fs::create_dir_all(&dest).expect("create dest");

    // Create a fake binary
    let binary_path = dest.join("br");
    fs::write(&binary_path, "#!/bin/sh\necho 'br 0.0.1'").expect("write fake");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&binary_path, fs::Permissions::from_mode(0o755)).expect("chmod");
    }

    assert!(binary_path.exists(), "Binary should exist before uninstall");

    // Run uninstall
    let mut env = HashMap::new();
    env.insert("DEST", dest.to_str().unwrap());

    let output = run_installer(&temp, &["--uninstall"], env);

    let stderr = String::from_utf8_lossy(&output.stderr);

    // Binary should be removed
    assert!(
        !binary_path.exists(),
        "Binary should be removed after uninstall"
    );

    // Should report success
    assert!(
        stderr.contains("uninstalled") || stderr.contains("Removed"),
        "Should report successful uninstall"
    );
}

// ============================================================================
// Environment Variable Tests
// ============================================================================

#[test]
fn e2e_installer_respects_br_install_dir() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let custom_dir = temp.path().join("custom_install_location");

    let mut env = HashMap::new();
    env.insert("BR_INSTALL_DIR", custom_dir.to_str().unwrap());

    let _output = run_installer(&temp, &["--quiet"], env);

    // The directory should be created (mkdir -p in script)
    // Even if download fails, the setup should prepare the directory
}

#[test]
fn e2e_installer_no_gum_disables_fancy_output() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    let mut env = HashMap::new();
    env.insert("NO_GUM", "1");

    let output = run_installer(&temp, &["--help"], env);
    let stdout = String::from_utf8_lossy(&output.stdout);

    // With NO_GUM, should use plain text output (no gum style boxes)
    // The fallback output uses simpler formatting
    assert!(
        stdout.contains("br installer") || stdout.contains("Usage"),
        "Should print help in plain format"
    );
}

// ============================================================================
// Source Build Tests
// ============================================================================

#[test]
fn e2e_installer_from_source_flag_accepted() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Just verify the flag is accepted (actual build would take too long for a test)
    // We use --help to verify flag parsing
    let output = run_installer(&temp, &["--from-source", "--help"], HashMap::new());

    // Should print help (flag was accepted)
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("br installer") || stdout.contains("--from-source"),
        "Should accept --from-source flag"
    );
}

#[test]
fn e2e_installer_source_build_preflight_does_not_touch_shared_cargo_state() {
    let script = install_script_contents();
    let prepare_for_build = shell_function_section(&script, "prepare_for_build");

    assert!(
        !prepare_for_build.contains("pkill"),
        "source-build preflight must not kill unrelated cargo processes"
    );
    for forbidden_path in [
        "~/.cargo/.package-cache",
        "~/.cargo/registry/.crate-cache.lock",
        "~/.cargo/registry/cache",
        "/tmp/cargo-target",
    ] {
        assert!(
            !prepare_for_build.contains(forbidden_path),
            "source-build preflight must not delete shared Cargo state: {forbidden_path}"
        );
    }
}

// ============================================================================
// Integration Test: Full Install (Network Required)
// ============================================================================

#[test]
#[ignore = "requires network access and takes time"]
fn e2e_installer_full_install_and_verify() {
    if !has_bash() || !has_network() {
        eprintln!("Skipping test: bash or network not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");
    let dest = temp.path().join("bin");

    let mut env = HashMap::new();
    env.insert("DEST", dest.to_str().unwrap());

    let output = run_installer(&temp, &["--verify"], env);

    let _stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if output.status.success() {
        // Verify binary exists and works
        let binary_path = dest.join("br");
        assert!(binary_path.exists(), "Binary should exist after install");

        // Run the installed binary
        let version_output = Command::new(&binary_path)
            .arg("--version")
            .output()
            .expect("run installed br");

        assert!(
            version_output.status.success(),
            "Installed binary should run: {}",
            String::from_utf8_lossy(&version_output.stderr)
        );

        let version_str = String::from_utf8_lossy(&version_output.stdout);
        assert!(
            version_str.contains("br") || version_str.contains("0."),
            "Should report version: {version_str}"
        );
    } else {
        eprintln!("Install failed (may be network issue):\n{stderr}");
    }
}

// ============================================================================
// Proxy Support Tests
// ============================================================================

#[test]
fn e2e_installer_proxy_env_vars_accepted() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    // Test that proxy environment variables are accepted
    // (We can't actually test proxy functionality without a proxy server)
    let mut env = HashMap::new();
    env.insert("HTTPS_PROXY", "http://localhost:8888");
    env.insert("HTTP_PROXY", "http://localhost:8888");

    // Just run help to verify env vars don't cause errors
    let output = run_installer(&temp, &["--help"], env);

    let stdout = String::from_utf8_lossy(&output.stdout);
    let _stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stdout.contains("br installer") || stdout.contains("HTTPS_PROXY"),
        "Should mention HTTPS_PROXY in help. stdout={stdout}"
    );
}

// ============================================================================
// Help Output Tests
// ============================================================================

#[test]
fn e2e_installer_help_shows_all_options() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    let output = run_installer(&temp, &["--help"], HashMap::new());
    let stdout = String::from_utf8_lossy(&output.stdout);

    // Verify key options are documented
    let expected_options = [
        "--version",
        "--dest",
        "--system",
        "--easy-mode",
        "--verify",
        "--from-source",
        "--quiet",
        "--uninstall",
    ];

    for option in expected_options {
        assert!(stdout.contains(option), "Help should document {option}");
    }
}

#[test]
fn e2e_installer_shows_supported_platforms() {
    if !has_bash() {
        eprintln!("Skipping test: bash not available");
        return;
    }

    let temp = TempDir::new().expect("temp dir");

    let output = run_installer(&temp, &["--help"], HashMap::new());
    let stdout = String::from_utf8_lossy(&output.stdout);

    // Verify platforms are mentioned
    assert!(
        stdout.contains("Linux") || stdout.contains("linux"),
        "Help should mention Linux support"
    );
    assert!(
        stdout.contains("macOS") || stdout.contains("darwin") || stdout.contains("Mac"),
        "Help should mention macOS support"
    );
}
