//! soldr#1543 — integration coverage for the dependency-prefetch
//! overlap on the blessed `soldr build --target <T>` surface.
//!
//! Contract pinned here (all through a fake recording cargo, so no
//! live network is ever touched):
//!
//! * `soldr build --target <T>` with a `Cargo.lock` spawns exactly one
//!   `cargo fetch --target <T>` prefetch, and it completes
//!   before the main `cargo build` spawns.
//! * `--offline`, a missing lockfile, and the `SOLDR_FETCH_OVERLAP`
//!   kill switch each suppress the prefetch.
//! * A failing prefetch does not fail the build (best-effort).
//! * NO other surface spawns a fetch: `soldr cargo build --target <T>`
//!   (the explicit legacy passthrough) and `soldr cargo check` run
//!   zero `cargo fetch` subprocesses.

#![allow(unused_imports)]

mod common;

use common::*;
use std::path::{Path, PathBuf};
use std::{fs, process::Command};

/// The target used across these tests. It is pre-seeded with a fake
/// catalogue-backed GNU bundle so the overlap coverage stays hermetic and
/// cannot mask a new normal-path Zig fetch.
const TARGET: &str = "x86_64-unknown-linux-gnu";

/// Fake cargo that answers `cargo metadata` with an empty JSON object
/// and records every other invocation's argv (unit-separator-joined)
/// to `log_path`. Exit code is `SOLDR_TEST_FAKE_CARGO_EXIT` for
/// `fetch` invocations when set (used by the best-effort test) and 0
/// otherwise.
fn fake_recording_cargo_script(log_path: &Path) -> String {
    if matches!(
        soldr_platform::host::facts::os(),
        soldr_platform::host::facts::HostOs::Windows
    ) {
        format!(
            "@echo off\n\
             if \"%~1\"==\"metadata\" (\n\
               echo {{}}\n\
               exit /b 0\n\
             )\n\
             setlocal enabledelayedexpansion\n\
             set \"line=\"\n\
             :loop\n\
             if \"%~1\"==\"\" goto done\n\
             if defined line (set \"line=!line!\u{1f}%~1\") else (set \"line=%~1\")\n\
             shift\n\
             goto loop\n\
             :done\n\
             echo !line!>>\"{0}\"\n\
             if \"!line:~0,5!\"==\"fetch\" (\n\
               if defined SOLDR_TEST_FAKE_CARGO_EXIT exit /b %SOLDR_TEST_FAKE_CARGO_EXIT%\n\
             )\n\
             exit /b 0\n",
            log_path.display()
        )
    } else {
        format!(
            "#!/bin/sh\n\
             if [ \"$1\" = \"metadata\" ]; then\n\
               echo '{{}}'\n\
               exit 0\n\
             fi\n\
             verb=\"$1\"\n\
             sep=$(printf '\\037')\n\
             out=\"\"\n\
             first=1\n\
             for arg in \"$@\"; do\n\
               if [ $first -eq 1 ]; then\n\
                 out=\"$arg\"\n\
                 first=0\n\
               else\n\
                 out=\"$out${{sep}}$arg\"\n\
               fi\n\
             done\n\
             printf '%s\\n' \"$out\" >> \"{0}\"\n\
             if [ \"$verb\" = \"fetch\" ] && [ -n \"${{SOLDR_TEST_FAKE_CARGO_EXIT:-}}\" ]; then\n\
               exit \"$SOLDR_TEST_FAKE_CARGO_EXIT\"\n\
             fi\n\
             exit 0\n",
            log_path.display()
        )
    }
}

struct Harness {
    project: PathBuf,
    cache_root: PathBuf,
    log_path: PathBuf,
    cargo: PathBuf,
    rustc: PathBuf,
    rustup: PathBuf,
}

impl Harness {
    fn new(label: &str, with_lockfile: bool) -> Self {
        let project = unique_temp_dir(&format!("fetch-overlap-{label}"));
        let cache_root = unique_temp_dir(&format!("fetch-overlap-{label}-cache"));
        let tool_dir = unique_temp_dir(&format!("fetch-overlap-{label}-bin"));
        let log_path = cache_root.join("cargo-invocations.log");

        fs::create_dir_all(project.join("src")).expect("create src dir");
        fs::write(
            project.join("Cargo.toml"),
            "[package]\n\
             name = \"fetch-overlap-demo\"\n\
             version = \"0.1.0\"\n\
             edition = \"2021\"\n\
             \n\
             [workspace]\n",
        )
        .expect("write Cargo.toml");
        fs::write(project.join("src").join("main.rs"), "fn main() {}\n").expect("write main.rs");
        if with_lockfile {
            fs::write(
                project.join("Cargo.lock"),
                "# This file is automatically @generated by Cargo.\n\
                 version = 3\n\
                 \n\
                 [[package]]\n\
                 name = \"fetch-overlap-demo\"\n\
                 version = \"0.1.0\"\n",
            )
            .expect("write Cargo.lock");
        }

        let cargo = fake_script_path(&tool_dir, "cargo");
        let rustc = fake_script_path(&tool_dir, "rustc");
        let rustup = install_logging_fake_rustup(&cache_root.join("rustup-invocations.log"));
        write_fake_script(&cargo, &fake_recording_cargo_script(&log_path));
        write_fake_script(&rustc, &fake_rustc_script(&log_path));
        Harness {
            project,
            cache_root,
            log_path,
            cargo,
            rustc,
            rustup,
        }
    }

    fn run(&self, args: &[&str], extra_env: &[(&str, &str)]) -> std::process::Output {
        let bundle = self
            .cache_root
            .join("bin")
            .join("syslib")
            .join("gnu-linux-toolchain")
            .join(soldr_cli::fetch::gnu_linux_toolchain::GNU_LINUX_TOOLCHAIN_VERSION)
            .join("linux-x64-gnu");
        let package = bundle.join("package");
        let compiler_prefix = "x86_64-conda-linux-gnu";
        let bin = package.join("bin");
        let sysroot = package.join(compiler_prefix).join("sysroot");
        fs::create_dir_all(&bin).expect("create GNU bundle bin");
        fs::create_dir_all(sysroot.join("usr/include")).expect("create GNU sysroot includes");
        fs::create_dir_all(sysroot.join("usr/lib")).expect("create GNU sysroot libraries");
        for tool in ["gcc", "g++", "ar", "ranlib", "ld", "readelf"] {
            write_fake_script(
                &bin.join(format!("{compiler_prefix}-{tool}")),
                &fake_rustc_script(&self.log_path),
            );
        }
        fs::write(bundle.join(".complete"), "test bundle").expect("write GNU bundle stamp");

        let mut cmd = common::isolated_soldr_command();
        cmd.args(args)
            .current_dir(&self.project)
            .env("SOLDR_CACHE_DIR", &self.cache_root)
            .env("SOLDR_TEST_CARGO_BIN", &self.cargo)
            .env("SOLDR_TEST_RUSTC_BIN", &self.rustc)
            // These tests cover Cargo prefetch ordering, not rustup. Avoid
            // depending on the target runner having a default toolchain.
            .env("SOLDR_TEST_RUSTUP_BIN", &self.rustup)
            // Keep `blessed_build::prepare` fully offline/hermetic:
            // no *-sys catalogue probes, no managed cmake/ninja fetch.
            .env("SOLDR_USE_LEGACY_VENDORED_SYS", "1")
            .env("SOLDR_USE_SYSTEM_CMAKE", "1")
            // ...and the toolchain catalogue, which those two do not cover.
            // The GNU catalogue bundle is pre-seeded below so the build stays
            // hermetic while exercising only Cargo prefetch ordering.
            .env("SOLDR_MANIFEST_DISABLE", "1")
            // soldr#2159: these tests hit the 120s nextest budget
            // on CI while finishing in ~1.4s locally, and the child produces no
            // output at all, so three rounds of hypotheses have been guesswork.
            // Every `command_output_with_timeout` call defaults to 60s, so two
            // of them exhaust the budget in silence. Capping it here converts
            // any such stall into `<context> timed out after 20 seconds`, which
            // names the caller instead of hanging.
            .env("SOLDR_COMMAND_OUTPUT_TIMEOUT_SECS", "20")
            // soldr#2159: and the invariant itself, rather than one more
            // opt-out. Four separate fetch paths have opened up under this
            // suite; each fix was locally right and globally incomplete
            // because nothing made a *new* one fail. This turns a fifth
            // into an immediate error naming the fetch, instead of a slow
            // suite that aborts at the 120s watchdog saying nothing.
            .env("SOLDR_TEST_NO_NETWORK", "1")
            // soldr#2437: these fixtures seed a FAKE Linux toolchain and run
            // on Windows lanes too; disable the host-cross guard so the
            // machinery under test stays reachable.
            .env("SOLDR_WINDOWS_LINUX_CROSS_GUARD", "off")
            // Isolate from any outer environment that would change the
            // overlap decision.
            .env_remove("CARGO_NET_OFFLINE")
            .env_remove("SOLDR_FETCH_OVERLAP")
            .env_remove("SOLDR_TEST_BUILD_PREP_DELAY_MS")
            .env_remove("SOLDR_TEST_FAKE_CARGO_EXIT");
        for (name, value) in extra_env {
            cmd.env(name, value);
        }
        // soldr#2159: mark each invocation as it starts and finishes, flushed
        // immediately. A watchdog abort kills the process, so anything printed
        // only at the end of the test is lost -- these markers are what tells
        // the next CI failure *which* of the two runs stalled, and for how
        // long, rather than leaving a 120s silence.
        eprintln!("[fetch-overlap] -> soldr {args:?}");
        let started = std::time::Instant::now();
        let output = cmd.output().expect("failed to run soldr");
        eprintln!(
            "[fetch-overlap] <- soldr {args:?} in {:?} (status {:?})",
            started.elapsed(),
            output.status.code()
        );
        output
    }

    /// Recorded cargo invocations (metadata calls are not logged),
    /// each as its own argv vector.
    fn cargo_invocations(&self) -> Vec<Vec<String>> {
        let text = fs::read_to_string(&self.log_path).unwrap_or_default();
        text.lines()
            .filter(|line| !line.trim().is_empty())
            .map(|line| line.split('\u{1f}').map(str::to_string).collect())
            .collect()
    }
}

fn assert_success(output: &std::process::Output, context: &str) {
    assert!(
        output.status.success(),
        "{context} failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn fetch_invocations(invocations: &[Vec<String>]) -> Vec<&Vec<String>> {
    invocations
        .iter()
        .filter(|argv| argv.first().map(String::as_str) == Some("fetch"))
        .collect()
}

#[test]
fn blessed_build_prefetches_dependencies_before_cargo_build() {
    let harness = Harness::new("basic", true);
    let output = harness.run(&["--no-cache", "build", "--target", TARGET], &[]);
    assert_success(&output, "soldr build --target");

    let invocations = harness.cargo_invocations();
    let fetches = fetch_invocations(&invocations);
    assert_eq!(
        fetches.len(),
        1,
        "expected exactly one prefetch invocation; got {invocations:?}"
    );
    assert_eq!(
        fetches[0],
        &vec![
            "fetch".to_string(),
            "--target".to_string(),
            TARGET.to_string(),
        ],
        "prefetch must be locked and target-scoped"
    );

    let fetch_idx = invocations
        .iter()
        .position(|argv| argv.first().map(String::as_str) == Some("fetch"))
        .expect("fetch invocation present");
    let build_idx = invocations
        .iter()
        .position(|argv| argv.first().map(String::as_str) == Some("build"))
        .unwrap_or_else(|| panic!("cargo build invocation missing: {invocations:?}"));
    assert!(
        fetch_idx < build_idx,
        "prefetch must be joined before the main cargo build spawns: {invocations:?}"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("prefetching dependencies"),
        "expected the prefetch announcement on stderr:\n{stderr}"
    );
    assert!(
        stderr.contains("dependency prefetch completed"),
        "expected the prefetch completion line on stderr:\n{stderr}"
    );
}

#[test]
fn offline_build_skips_prefetch() {
    let harness = Harness::new("offline", true);
    let output = harness.run(
        &["--no-cache", "build", "--target", TARGET, "--offline"],
        &[],
    );
    assert_success(&output, "soldr build --target --offline");

    let invocations = harness.cargo_invocations();
    assert!(
        fetch_invocations(&invocations).is_empty(),
        "--offline must suppress the prefetch: {invocations:?}"
    );
    assert!(
        invocations
            .iter()
            .any(|argv| argv.first().map(String::as_str) == Some("build")),
        "the main cargo build must still run: {invocations:?}"
    );
}

#[test]
fn missing_lockfile_still_prefetches() {
    let harness = Harness::new("nolock", false);
    let output = harness.run(&["--no-cache", "build", "--target", TARGET], &[]);
    assert_success(&output, "soldr build --target (no lockfile)");

    let invocations = harness.cargo_invocations();
    assert!(
        fetch_invocations(&invocations).len() == 1,
        "no Cargo.lock must mean no `cargo fetch` prefetch: {invocations:?}"
    );
}

#[test]
fn kill_switch_skips_prefetch() {
    let harness = Harness::new("killswitch", true);
    let output = harness.run(
        &["--no-cache", "build", "--target", TARGET],
        &[("SOLDR_FETCH_OVERLAP", "0")],
    );
    assert_success(&output, "soldr build --target with SOLDR_FETCH_OVERLAP=0");

    let invocations = harness.cargo_invocations();
    assert!(
        fetch_invocations(&invocations).is_empty(),
        "SOLDR_FETCH_OVERLAP=0 must suppress the prefetch: {invocations:?}"
    );
}

#[test]
fn failing_prefetch_does_not_fail_the_build() {
    let harness = Harness::new("besteffort", true);
    let output = harness.run(
        &["--no-cache", "build", "--target", TARGET],
        &[("SOLDR_TEST_FAKE_CARGO_EXIT", "101")],
    );
    assert_success(
        &output,
        "soldr build --target with a failing prefetch (must still succeed)",
    );

    let invocations = harness.cargo_invocations();
    assert_eq!(
        fetch_invocations(&invocations).len(),
        1,
        "the prefetch ran (and failed): {invocations:?}"
    );
    assert!(
        invocations
            .iter()
            .any(|argv| argv.first().map(String::as_str) == Some("build")),
        "the main cargo build must still run after a prefetch failure: {invocations:?}"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("dependency prefetch exited with"),
        "expected the best-effort failure log line on stderr:\n{stderr}"
    );
}

#[test]
fn legacy_and_non_build_commands_spawn_no_prefetch() {
    // The explicit legacy passthrough surface: `soldr cargo build`.
    let harness = Harness::new("legacy", true);
    let output = harness.run(&["--no-cache", "cargo", "build", "--target", TARGET], &[]);
    assert_success(&output, "soldr cargo build --target");
    let invocations = harness.cargo_invocations();
    assert!(
        fetch_invocations(&invocations).is_empty(),
        "legacy `soldr cargo build` must spawn no prefetch: {invocations:?}"
    );

    // A non-build command through the front door: `soldr cargo check`.
    let harness = Harness::new("nonbuild", true);
    let output = harness.run(&["--no-cache", "cargo", "check", "--target", TARGET], &[]);
    assert_success(&output, "soldr cargo check --target");
    let invocations = harness.cargo_invocations();
    assert!(
        fetch_invocations(&invocations).is_empty(),
        "`soldr cargo check` must spawn no prefetch: {invocations:?}"
    );
}
