#![allow(clippy::items_after_test_module)]
//! Shared test fixtures for es-fluent-cli tests.
//!
//! This module provides common file-based fixtures that can be reused
//! across different test modules.
#![allow(dead_code)]

#[cfg(test)]
use crate::generation::cache::RunnerCache;
#[cfg(test)]
use fs_err as fs;
#[cfg(test)]
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::process::Command;
#[cfg(test)]
use std::sync::OnceLock;
#[cfg(test)]
use std::time::SystemTime;

pub const CARGO_TOML: &str = include_str!("../../tests/fixtures/base/Cargo.toml");
pub const I18N_TOML: &str = include_str!("../../tests/fixtures/base/i18n.toml");
pub const LIB_RS: &str = include_str!("../../tests/fixtures/base/lib.rs");
pub const HELLO_FTL: &str = include_str!("../../tests/fixtures/base/ftl/hello.ftl");
pub const HELLO_ES_FTL: &str = include_str!("../../tests/fixtures/base/ftl/hello_es.ftl");
pub const HELLO_FR_FTL: &str = include_str!("../../tests/fixtures/base/ftl/hello_fr.ftl");
pub const HELLO_WORLD_FTL: &str = include_str!("../../tests/fixtures/base/ftl/hello_world.ftl");
pub const RUNNER_SCRIPT: &str = include_str!("../../tests/fixtures/runner/runner.sh");
pub const RUNNER_OUTPUT_SCRIPT: &str = include_str!("../../tests/fixtures/runner/runner_output.sh");
pub const RUNNER_FAILING_SCRIPT: &str =
    include_str!("../../tests/fixtures/runner/runner_failing.sh");
pub const INVALID_FTL: &str = include_str!("../../tests/fixtures/runner/invalid.ftl");

// Check command specific fixtures
pub const INVENTORY_WITH_HELLO: &str =
    include_str!("../../tests/fixtures/check/inventory_with_hello.json");
pub const INVENTORY_WITH_MISSING_KEY: &str =
    include_str!("../../tests/fixtures/check/inventory_with_missing_key.json");

// Format command specific fixtures
pub const UI_UNSORTED_FTL: &str = include_str!("../../tests/fixtures/format/ui_unsorted.ftl");

// Utils specific fixtures
pub const WORKSPACE_CARGO_TOML: &str = include_str!("../../tests/fixtures/workspace/Cargo.toml");

#[cfg(test)]
#[derive(Clone, Debug, Default)]
pub struct FakeRunnerBehavior {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
    pub echo_args: bool,
    pub record_args_path: Option<PathBuf>,
}

#[cfg(test)]
impl FakeRunnerBehavior {
    pub fn silent_success() -> Self {
        Self::default()
    }

    pub fn stdout(stdout: impl Into<String>) -> Self {
        Self {
            stdout: stdout.into(),
            ..Self::default()
        }
    }

    pub fn failing(stderr: impl Into<String>) -> Self {
        Self {
            stderr: stderr.into(),
            exit_code: 1,
            ..Self::default()
        }
    }

    pub fn echo_args() -> Self {
        Self {
            echo_args: true,
            ..Self::default()
        }
    }

    pub fn record_args(path: impl Into<PathBuf>) -> Self {
        Self {
            record_args_path: Some(path.into()),
            ..Self::default()
        }
    }
}

#[cfg(test)]
pub fn create_test_crate_workspace() -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    write_basic_workspace(temp.path(), true);
    temp
}

#[cfg(test)]
pub fn create_test_crate_workspace_without_ftl() -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    write_basic_workspace(temp.path(), false);
    temp
}

#[cfg(test)]
pub fn create_binary_only_i18n_workspace() -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(temp.path().join("src")).expect("create src");
    fs::create_dir_all(temp.path().join("i18n/en")).expect("create i18n/en");
    fs::write(
        temp.path().join("Cargo.toml"),
        "[package]\nname = \"bin-app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )
    .expect("write Cargo.toml");
    fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").expect("write main.rs");
    fs::write(temp.path().join("i18n.toml"), I18N_TOML).expect("write i18n.toml");
    fs::write(temp.path().join("i18n/en/bin-app.ftl"), HELLO_FTL).expect("write ftl");
    temp
}

#[cfg(test)]
pub fn create_mixed_library_and_binary_i18n_workspace() -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::write(
        temp.path().join("Cargo.toml"),
        "[workspace]\nmembers = [\"valid-app\", \"bin-app\"]\nresolver = \"3\"\n",
    )
    .expect("write workspace Cargo.toml");

    let valid = temp.path().join("valid-app");
    fs::create_dir_all(valid.join("src")).expect("create valid src");
    fs::create_dir_all(valid.join("i18n/en")).expect("create valid i18n");
    fs::write(
        valid.join("Cargo.toml"),
        "[package]\nname = \"valid-app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )
    .expect("write valid Cargo.toml");
    fs::write(valid.join("src/lib.rs"), LIB_RS).expect("write valid lib.rs");
    fs::write(valid.join("i18n.toml"), I18N_TOML).expect("write valid i18n.toml");
    fs::write(valid.join("i18n/en/valid-app.ftl"), HELLO_FTL).expect("write valid ftl");

    let binary = temp.path().join("bin-app");
    fs::create_dir_all(binary.join("src")).expect("create binary src");
    fs::create_dir_all(binary.join("i18n/en")).expect("create binary i18n");
    fs::write(
        binary.join("Cargo.toml"),
        "[package]\nname = \"bin-app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )
    .expect("write binary Cargo.toml");
    fs::write(binary.join("src/main.rs"), "fn main() {}\n").expect("write binary main.rs");
    fs::write(binary.join("i18n.toml"), I18N_TOML).expect("write binary i18n.toml");
    fs::write(binary.join("i18n/en/bin-app.ftl"), HELLO_FTL).expect("write binary ftl");

    temp
}

#[cfg(test)]
pub fn write_basic_workspace(base: &Path, include_ftl: bool) {
    fs::create_dir_all(base.join("src")).expect("create src");
    fs::create_dir_all(base.join("i18n/en")).expect("create i18n/en");
    fs::write(base.join("Cargo.toml"), CARGO_TOML).expect("write Cargo.toml");
    fs::write(base.join("src/lib.rs"), LIB_RS).expect("write lib.rs");
    fs::write(base.join("i18n.toml"), I18N_TOML).expect("write i18n.toml");
    if include_ftl {
        fs::write(base.join("i18n/en/test-app.ftl"), HELLO_FTL).expect("write ftl");
    }
}

#[cfg(test)]
pub fn create_workspace_with_locales(locales: &[(&str, &str)]) -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(temp.path().join("src")).expect("create src");
    fs::write(temp.path().join("Cargo.toml"), CARGO_TOML).expect("write Cargo.toml");
    fs::write(temp.path().join("src/lib.rs"), LIB_RS).expect("write lib.rs");
    fs::write(temp.path().join("i18n.toml"), I18N_TOML).expect("write i18n.toml");

    for (locale, content) in locales {
        let locale_dir = temp.path().join("i18n").join(locale);
        fs::create_dir_all(&locale_dir).expect("create locale dir");
        fs::write(locale_dir.join("test-app.ftl"), content).expect("write locale ftl");
    }

    temp
}

#[cfg(test)]
pub fn write_file(path: &Path, contents: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent directory");
    }
    fs::write(path, contents).expect("write file");
}

#[cfg(test)]
pub mod toml_helpers {
    use std::path::Path;
    use toml::Value;

    pub fn string_value(value: &str) -> Value {
        Value::String(value.to_string())
    }

    pub fn table(
        entries: impl IntoIterator<Item = (&'static str, Value)>,
    ) -> toml::map::Map<String, Value> {
        entries
            .into_iter()
            .map(|(key, value)| (key.to_string(), value))
            .collect()
    }

    pub fn write_toml(path: &Path, value: &Value) {
        super::write_file(
            path,
            &toml::to_string(value).expect("serialize TOML fixture"),
        );
    }

    pub fn insert_section(document: &mut Value, key: &str, value: Value) {
        document
            .as_table_mut()
            .expect("TOML document table")
            .insert(key.to_string(), value);
    }

    pub fn package_manifest(name: &str, version: &str) -> Value {
        Value::Table(table([(
            "package",
            Value::Table(table([
                ("name", string_value(name)),
                ("version", string_value(version)),
                ("edition", string_value("2024")),
            ])),
        )]))
    }

    pub fn workspace_manifest(members: &[&str]) -> Value {
        Value::Table(table([(
            "workspace",
            Value::Table(table([
                (
                    "members",
                    Value::Array(members.iter().copied().map(string_value).collect()),
                ),
                ("resolver", string_value("2")),
            ])),
        )]))
    }

    pub fn i18n_config(fallback_language: &str, assets_dir: &str) -> Value {
        Value::Table(table([
            ("fallback_language", string_value(fallback_language)),
            ("assets_dir", string_value(assets_dir)),
        ]))
    }
}

#[cfg(test)]
pub fn fake_runner_binary_name() -> String {
    format!("es-fluent-runner{}", std::env::consts::EXE_SUFFIX)
}

#[cfg(test)]
pub fn fake_runner_binary_path(target_dir: &Path) -> PathBuf {
    target_dir.join("debug").join(fake_runner_binary_name())
}

#[cfg(test)]
fn compiled_fake_runner_binary() -> &'static PathBuf {
    static BINARY: OnceLock<PathBuf> = OnceLock::new();

    BINARY.get_or_init(|| {
        let cache_dir =
            std::env::temp_dir().join(format!("es-fluent-cli-test-runner-{}", std::process::id()));
        fs::create_dir_all(&cache_dir).expect("create fake runner cache dir");

        let source_path = cache_dir.join("fake_runner.rs");
        let binary_path = cache_dir.join(fake_runner_binary_name());

        fs::write(
            &source_path,
            r#"use std::{env, fs, process};

fn read_sidecar(exe: &std::path::Path, ext: &str) -> Option<String> {
    fs::read_to_string(exe.with_extension(ext)).ok()
}

fn main() {
    let exe = env::current_exe().expect("current_exe");
    let args: Vec<String> = env::args().skip(1).collect();
    let mode = read_sidecar(&exe, "mode").unwrap_or_default();

    if mode.trim() == "echo_args" {
        print!("{}", args.join(" "));
    }

    if let Some(path) = read_sidecar(&exe, "recordargs") {
        fs::write(path.trim(), args.join(" ")).expect("record args");
    }

    if let Some(stdout) = read_sidecar(&exe, "stdout") {
        print!("{stdout}");
    }

    if let Some(stderr) = read_sidecar(&exe, "stderr") {
        eprint!("{stderr}");
    }

    let exit_code = read_sidecar(&exe, "exitcode")
        .and_then(|raw| raw.trim().parse::<i32>().ok())
        .unwrap_or(0);
    process::exit(exit_code);
}
"#,
        )
        .expect("write fake runner source");

        let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
        let output = Command::new(rustc)
            .arg("--edition=2021")
            .arg(&source_path)
            .arg("-o")
            .arg(&binary_path)
            .output()
            .expect("spawn rustc for fake runner");

        assert!(
            output.status.success(),
            "failed to compile fake runner: stdout={}; stderr={}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        binary_path
    })
}

#[cfg(test)]
pub fn install_fake_runner(binary_path: &Path, behavior: &FakeRunnerBehavior) {
    fs::create_dir_all(binary_path.parent().expect("binary parent")).expect("create target/debug");
    let _ = fs::remove_file(binary_path);
    if fs::hard_link(compiled_fake_runner_binary(), binary_path).is_err() {
        let staged_binary_path = binary_path.with_extension("installing");
        let _ = fs::remove_file(&staged_binary_path);
        fs::copy(compiled_fake_runner_binary(), &staged_binary_path)
            .expect("copy fake runner binary");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt as _;

            let mut perms = fs::metadata(&staged_binary_path)
                .expect("fake runner metadata")
                .permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&staged_binary_path, perms).expect("set fake runner executable");
        }
        fs::rename(&staged_binary_path, binary_path).expect("install fake runner binary");
    }
    fs::write(
        binary_path.with_extension("mode"),
        if behavior.echo_args {
            "echo_args"
        } else {
            "static"
        },
    )
    .expect("write fake runner mode");
    fs::write(binary_path.with_extension("stdout"), &behavior.stdout)
        .expect("write fake runner stdout");
    fs::write(binary_path.with_extension("stderr"), &behavior.stderr)
        .expect("write fake runner stderr");
    if let Some(path) = &behavior.record_args_path {
        fs::write(
            binary_path.with_extension("recordargs"),
            path.display().to_string(),
        )
        .expect("write fake runner arg recorder path");
    } else {
        let _ = fs::remove_file(binary_path.with_extension("recordargs"));
    }
    fs::write(
        binary_path.with_extension("exitcode"),
        behavior.exit_code.to_string(),
    )
    .expect("write fake runner exit code");
}

#[cfg(test)]
pub fn runner_binary_mtime(binary_path: &Path) -> u64 {
    fs::metadata(binary_path)
        .and_then(|metadata| metadata.modified())
        .expect("runner mtime")
        .duration_since(SystemTime::UNIX_EPOCH)
        .expect("mtime duration")
        .as_secs()
}

#[cfg(test)]
pub fn save_runner_cache(
    temp_store: &es_fluent_runner::RunnerMetadataStore,
    workspace_root: &Path,
    runner_mtime: u64,
    cli_version: &str,
    crate_hashes: indexmap::IndexMap<es_fluent_runner::PackageName, String>,
) {
    fs::create_dir_all(temp_store.base_dir()).expect("create temp dir");
    RunnerCache {
        crate_hashes,
        runner_mtime,
        cli_version: cli_version.to_string(),
        workspace_inputs_hash: crate::generation::cache::compute_workspace_inputs_hash(
            workspace_root,
        ),
    }
    .save(temp_store.base_dir())
    .expect("save runner cache");
}

#[cfg(test)]
pub fn install_fake_runner_with_cache(
    binary_path: &Path,
    temp_store: &es_fluent_runner::RunnerMetadataStore,
    workspace_root: &Path,
    behavior: &FakeRunnerBehavior,
    cli_version: &str,
    crate_hashes: indexmap::IndexMap<es_fluent_runner::PackageName, String>,
) -> u64 {
    install_fake_runner(binary_path, behavior);
    let runner_mtime = runner_binary_mtime(binary_path);
    save_runner_cache(
        temp_store,
        workspace_root,
        runner_mtime,
        cli_version,
        crate_hashes,
    );
    runner_mtime
}

#[cfg(test)]
pub fn setup_fake_runner_and_cache(temp: &tempfile::TempDir, behavior: FakeRunnerBehavior) {
    let binary_path = fake_runner_binary_path(&temp.path().join("target"));
    let src_dir = temp.path().join("src");
    let i18n_toml = temp.path().join("i18n.toml");
    let hash = crate::generation::cache::compute_crate_inputs_hash(
        temp.path(),
        &src_dir,
        Some(&i18n_toml),
    );
    let temp_store = es_fluent_runner::RunnerMetadataStore::temp_for_workspace(temp.path());
    let mut crate_hashes = indexmap::IndexMap::new();
    crate_hashes.insert(
        es_fluent_runner::PackageName::try_new("test-app").expect("valid package name"),
        hash,
    );
    install_fake_runner_with_cache(
        &binary_path,
        &temp_store,
        temp.path(),
        &behavior,
        env!("CARGO_PKG_VERSION"),
        crate_hashes,
    );
}
