//! Test script detection — generates `script/test` content for new projects.

use std::path::Path;

use super::super::templates::STORY_KIT_SCRIPT_TEST;
use super::detect_node_test_cmd;

/// Generate `script/test` content for a new project at `root`.
///
/// Inspects well-known marker files to identify which tech stacks are present
/// and emits the appropriate test commands. Multi-stack projects get combined
/// commands run sequentially. Falls back to the generic stub when no markers
/// are found so the scaffold is always valid.
///
/// For projects with a frontend in a known subdirectory (`frontend/`, `client/`),
/// the test runner is detected from the `package.json` (vitest, jest, npm, pnpm).
pub(crate) fn detect_script_test(root: &Path) -> String {
    let mut commands: Vec<String> = Vec::new();

    if root.join("Cargo.toml").exists() {
        commands.push("cargo test".to_string());
    }

    if root.join("package.json").exists() {
        if root.join("pnpm-lock.yaml").exists() {
            commands.push("pnpm test".to_string());
        } else {
            commands.push("npm test".to_string());
        }
    }

    // Detect frontend in known subdirectories (e.g. frontend/, client/)
    for subdir in &["frontend", "client"] {
        let sub_path = root.join(subdir);
        if sub_path.join("package.json").exists() {
            let cmd = detect_node_test_cmd(&sub_path);
            commands.push(format!("(cd {} && {})", subdir, cmd));
        }
    }

    if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
        commands.push("pytest".to_string());
    }

    if root.join("go.mod").exists() {
        commands.push("go test ./...".to_string());
    }

    if commands.is_empty() {
        return STORY_KIT_SCRIPT_TEST.to_string();
    }

    let mut script = "#!/usr/bin/env bash\nset -euo pipefail\n\n".to_string();
    for cmd in commands {
        script.push_str(&cmd);
        script.push('\n');
    }
    script
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn detect_script_test_no_markers_returns_stub() {
        let dir = tempdir().unwrap();
        let script = detect_script_test(dir.path());
        assert!(
            script.contains("No tests configured"),
            "fallback should contain the generic stub message"
        );
        assert!(script.starts_with("#!/usr/bin/env bash"));
    }

    #[test]
    fn detect_script_test_cargo_toml_adds_cargo_test() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("cargo test"),
            "Rust project should run cargo test"
        );
        assert!(!script.contains("No tests configured"));
    }

    #[test]
    fn detect_script_test_package_json_npm_adds_npm_test() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("npm test"),
            "Node project without pnpm-lock should run npm test"
        );
        assert!(!script.contains("No tests configured"));
    }

    #[test]
    fn detect_script_test_package_json_pnpm_adds_pnpm_test() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();
        fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("pnpm test"),
            "Node project with pnpm-lock should run pnpm test"
        );
        // "pnpm test" is a substring of itself; verify there's no bare "npm test" line
        assert!(
            !script.lines().any(|l| l.trim() == "npm test"),
            "should not use npm when pnpm-lock.yaml is present"
        );
    }

    #[test]
    fn detect_script_test_pyproject_toml_adds_pytest() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("pyproject.toml"),
            "[project]\nname = \"x\"\n",
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("pytest"),
            "Python project should run pytest"
        );
        assert!(!script.contains("No tests configured"));
    }

    #[test]
    fn detect_script_test_requirements_txt_adds_pytest() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("requirements.txt"), "flask\n").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("pytest"),
            "Python project (requirements.txt) should run pytest"
        );
    }

    #[test]
    fn detect_script_test_go_mod_adds_go_test() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("go test ./..."),
            "Go project should run go test ./..."
        );
        assert!(!script.contains("No tests configured"));
    }

    #[test]
    fn detect_script_test_multi_stack_combines_commands() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("go test ./..."),
            "multi-stack should include Go test command"
        );
        assert!(
            script.contains("npm test"),
            "multi-stack should include Node test command"
        );
    }

    #[test]
    fn detect_script_test_frontend_subdir_with_vitest_uses_npx_vitest() {
        let dir = tempdir().unwrap();
        let frontend = dir.path().join("frontend");
        fs::create_dir_all(&frontend).unwrap();
        fs::write(
            frontend.join("package.json"),
            r#"{"devDependencies":{"vitest":"^1.0.0"},"scripts":{"test":"vitest run"}}"#,
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("vitest run"),
            "frontend with vitest should emit vitest run"
        );
        assert!(
            script.contains("cd frontend"),
            "should cd into the frontend directory"
        );
        assert!(
            !script.contains("No tests configured"),
            "should not use stub when frontend is detected"
        );
    }

    #[test]
    fn detect_script_test_frontend_subdir_with_jest_uses_npx_jest() {
        let dir = tempdir().unwrap();
        let frontend = dir.path().join("frontend");
        fs::create_dir_all(&frontend).unwrap();
        fs::write(
            frontend.join("package.json"),
            r#"{"devDependencies":{"jest":"^29.0.0"},"scripts":{"test":"jest"}}"#,
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("jest"),
            "frontend with jest should emit jest"
        );
        assert!(
            script.contains("cd frontend"),
            "should cd into the frontend directory"
        );
    }

    #[test]
    fn detect_script_test_frontend_subdir_no_known_runner_uses_npm_test() {
        let dir = tempdir().unwrap();
        let frontend = dir.path().join("frontend");
        fs::create_dir_all(&frontend).unwrap();
        fs::write(
            frontend.join("package.json"),
            r#"{"scripts":{"test":"mocha"}}"#,
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("npm test"),
            "frontend without known runner should fall back to npm test"
        );
        assert!(script.contains("cd frontend"));
    }

    #[test]
    fn detect_script_test_frontend_subdir_pnpm_uses_pnpm_vitest() {
        let dir = tempdir().unwrap();
        let frontend = dir.path().join("frontend");
        fs::create_dir_all(&frontend).unwrap();
        fs::write(
            frontend.join("package.json"),
            r#"{"devDependencies":{"vitest":"^1.0.0"}}"#,
        )
        .unwrap();
        fs::write(frontend.join("pnpm-lock.yaml"), "").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("pnpm vitest run"),
            "pnpm frontend with vitest should use pnpm vitest run"
        );
    }

    #[test]
    fn detect_script_test_rust_plus_frontend_subdir_both_included() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("Cargo.toml"),
            "[package]\nname = \"server\"\n",
        )
        .unwrap();
        let frontend = dir.path().join("frontend");
        fs::create_dir_all(&frontend).unwrap();
        fs::write(
            frontend.join("package.json"),
            r#"{"devDependencies":{"vitest":"^1.0.0"}}"#,
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("cargo test"),
            "Rust + frontend should include cargo test"
        );
        assert!(
            script.contains("vitest run"),
            "Rust + frontend should include vitest run"
        );
        assert!(
            script.contains("cd frontend"),
            "Rust + frontend should cd into frontend"
        );
    }

    #[test]
    fn detect_script_test_client_subdir_detected() {
        let dir = tempdir().unwrap();
        let client = dir.path().join("client");
        fs::create_dir_all(&client).unwrap();
        fs::write(
            client.join("package.json"),
            r#"{"scripts":{"test":"jest"}}"#,
        )
        .unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.contains("cd client"),
            "client/ subdir should also be detected"
        );
    }

    #[test]
    fn detect_script_test_output_starts_with_shebang() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();

        let script = detect_script_test(dir.path());
        assert!(
            script.starts_with("#!/usr/bin/env bash\nset -euo pipefail\n"),
            "generated script should start with bash shebang and set -euo pipefail"
        );
    }
}
