//! Integration tests for the scaffold module — verifies that `scaffold_story_kit`
//! creates the correct directory structure, files, and git state.

use super::*;
use std::fs;
use tempfile::tempdir;

#[test]
fn scaffold_story_kit_creates_structure() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert!(dir.path().join(".huskies/README.md").exists());
    assert!(dir.path().join(".huskies/project.toml").exists());
    assert!(dir.path().join(".huskies/agents.toml").exists());
    assert!(dir.path().join(".huskies/specs/00_CONTEXT.md").exists());
    assert!(dir.path().join(".huskies/specs/tech/STACK.md").exists());
    // Old stories/ dirs should NOT be created
    assert!(!dir.path().join(".huskies/stories").exists());
    assert!(dir.path().join("script/test").exists());
}

#[test]
fn scaffold_story_kit_does_not_create_work_pipeline_dirs() {
    // After 763 the `.huskies/work/` tree is no longer scaffolded; CRDT is
    // the canonical pipeline state store.
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let work_root = dir.path().join(".huskies/work");
    assert!(
        !work_root.exists(),
        ".huskies/work/ should not be created at scaffold time"
    );
}

#[test]
fn scaffold_story_kit_agents_toml_has_coder_qa_mergemaster() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    // Agent definitions go into agents.toml, not project.toml.
    let agents = fs::read_to_string(dir.path().join(".huskies/agents.toml")).unwrap();
    assert!(agents.contains("[[agent]]"));
    assert!(agents.contains("stage = \"coder\""));
    assert!(agents.contains("stage = \"qa\""));
    assert!(agents.contains("stage = \"mergemaster\""));
    assert!(agents.contains("model = \"sonnet\""));

    // project.toml should NOT contain [[agent]] blocks.
    let project = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(!project.contains("[[agent]]"));
}

#[test]
fn scaffold_project_toml_contains_rate_limit_and_timezone_comments() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("rate_limit_notifications"),
        "project.toml scaffold should document rate_limit_notifications"
    );
    assert!(
        content.contains("timezone"),
        "project.toml scaffold should document timezone"
    );
}

#[test]
fn scaffold_project_toml_contains_max_retries_with_default_value() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("max_retries = 2"),
        "project.toml scaffold should include max_retries with default value 2"
    );
    assert!(
        content.contains("Maximum number of retries"),
        "project.toml scaffold should include a comment explaining max_retries"
    );
}

#[test]
fn scaffold_project_toml_contains_commented_out_optional_fields() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("# default_coder_model"),
        "project.toml scaffold should include commented-out default_coder_model"
    );
    assert!(
        content.contains("# max_coders"),
        "project.toml scaffold should include commented-out max_coders"
    );
    assert!(
        content.contains("# base_branch"),
        "project.toml scaffold should include commented-out base_branch"
    );
}

#[test]
fn scaffold_project_toml_round_trips_through_project_config_load() {
    use crate::config::ProjectConfig;

    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    // The generated project.toml must parse without error.
    let config =
        ProjectConfig::load(dir.path()).expect("Generated project.toml should parse without error");

    // Key defaults must survive the round-trip.
    assert_eq!(config.default_qa, "server");
    assert_eq!(config.max_retries, 2);
    assert!(
        config.rate_limit_notifications,
        "rate_limit_notifications should default to true"
    );
    assert!(
        config.default_coder_model.is_none(),
        "default_coder_model should be None when commented out"
    );
    assert!(
        config.max_coders.is_none(),
        "max_coders should be None when commented out"
    );
    assert!(
        config.base_branch.is_none(),
        "base_branch should be None when commented out"
    );
    assert!(
        config.timezone.is_none(),
        "timezone should be None when commented out"
    );
}

#[test]
fn scaffold_project_toml_uncommenting_base_branch_takes_effect() {
    use crate::config::ProjectConfig;

    // Regression for story 1177: the scaffold template used to emit
    // `[[component]]` headers before the commented top-level settings, so
    // uncommenting a setting like `base_branch` attached it to the last
    // `[[component]]` table instead of the top level, silently no-opping.
    let dir = tempdir().unwrap();
    fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"myapp\"\n",
    )
    .unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("# base_branch = \"main\""),
        "scaffold should emit a commented-out base_branch line to uncomment"
    );
    let uncommented = content.replace("# base_branch = \"main\"", "base_branch = \"main\"");

    let config =
        ProjectConfig::parse(&uncommented).expect("uncommented project.toml should still parse");
    assert_eq!(
        config.base_branch,
        Some("main".to_string()),
        "uncommenting base_branch in the scaffolded project.toml must take effect"
    );
}

#[test]
fn scaffold_context_is_blank_template_not_story_kit_content() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/specs/00_CONTEXT.md")).unwrap();
    assert!(content.contains("<!-- huskies:scaffold-template -->"));
    assert!(content.contains("## High-Level Goal"));
    assert!(content.contains("## Core Features"));
    assert!(content.contains("## Domain Definition"));
    assert!(content.contains("## Glossary"));
    // Must NOT contain Story Kit-specific content
    assert!(!content.contains("Agentic AI Code Assistant"));
    assert!(!content.contains("Poem HTTP server"));
}

#[test]
fn scaffold_stack_is_blank_template_not_story_kit_content() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/specs/tech/STACK.md")).unwrap();
    assert!(content.contains("<!-- huskies:scaffold-template -->"));
    assert!(content.contains("## Core Stack"));
    assert!(content.contains("## Coding Standards"));
    assert!(content.contains("## Quality Gates"));
    assert!(content.contains("## Libraries"));
    // Must NOT contain Story Kit-specific content
    assert!(!content.contains("Poem HTTP server"));
    assert!(!content.contains("TypeScript + React"));
}

#[cfg(unix)]
#[test]
fn scaffold_story_kit_creates_executable_script_test() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let script_test = dir.path().join("script/test");
    assert!(script_test.exists(), "script/test should be created");
    let perms = fs::metadata(&script_test).unwrap().permissions();
    assert!(
        perms.mode() & 0o111 != 0,
        "script/test should be executable"
    );
}

#[test]
fn scaffold_story_kit_does_not_overwrite_existing() {
    let dir = tempdir().unwrap();
    let readme = dir.path().join(".huskies/README.md");
    fs::create_dir_all(readme.parent().unwrap()).unwrap();
    fs::write(&readme, "custom content").unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert_eq!(fs::read_to_string(&readme).unwrap(), "custom content");
}

#[test]
fn scaffold_story_kit_is_idempotent() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let readme_content = fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap();
    let toml_content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();

    // Run again — must not change content or add duplicate .gitignore entries
    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert_eq!(
        fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap(),
        readme_content
    );
    assert_eq!(
        fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap(),
        toml_content
    );

    let story_kit_gitignore = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
    let count = story_kit_gitignore
        .lines()
        .filter(|l| l.trim() == "worktrees/")
        .count();
    assert_eq!(
        count, 1,
        ".huskies/.gitignore should not have duplicate entries"
    );
}

#[test]
fn scaffold_story_kit_existing_git_repo_no_commit() {
    let dir = tempdir().unwrap();

    // Initialize a git repo before scaffold
    std::process::Command::new("git")
        .args(["init"])
        .current_dir(dir.path())
        .status()
        .unwrap();
    std::process::Command::new("git")
        .args([
            "-c",
            "user.email=test@test.com",
            "-c",
            "user.name=Test",
            "commit",
            "--allow-empty",
            "-m",
            "pre-scaffold",
        ])
        .current_dir(dir.path())
        .status()
        .unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    // Only 1 commit should exist — scaffold must not commit into an existing repo
    let log_output = std::process::Command::new("git")
        .args(["log", "--oneline"])
        .current_dir(dir.path())
        .output()
        .unwrap();
    let log = String::from_utf8_lossy(&log_output.stdout);
    let commit_count = log.lines().count();
    assert_eq!(
        commit_count, 1,
        "scaffold should not create a commit in an existing git repo"
    );
}

#[test]
fn scaffold_creates_story_kit_gitignore_with_relative_entries() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    // .huskies/.gitignore must contain relative patterns for files under .huskies/
    let sk_content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
    assert!(sk_content.contains("worktrees/"));
    assert!(sk_content.contains("merge_workspace/"));
    assert!(sk_content.contains("coverage/"));
    assert!(sk_content.contains("matrix_history.json"));
    assert!(sk_content.contains("timers.json"));
    // Must NOT contain absolute .huskies/ prefixed paths
    assert!(!sk_content.contains(".huskies/"));

    // Root .gitignore must contain root-level huskies entries
    let root_content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
    assert!(root_content.contains(".huskies_port"));
    // store.json now lives inside .huskies/ and must NOT appear in root .gitignore
    assert!(!root_content.contains("store.json"));
    // Root .gitignore must NOT contain .huskies/ sub-directory patterns
    assert!(!root_content.contains(".huskies/worktrees/"));
    assert!(!root_content.contains(".huskies/merge_workspace/"));
    assert!(!root_content.contains(".huskies/coverage/"));
    // store.json must be in .huskies/.gitignore instead
    assert!(sk_content.contains("store.json"));
    // Database files must be ignored so novice users don't accidentally commit them
    assert!(sk_content.contains("pipeline.db"));
    assert!(sk_content.contains("*.db"));
}

#[test]
fn scaffold_story_kit_gitignore_does_not_duplicate_existing_entries() {
    let dir = tempdir().unwrap();
    // Pre-create .huskies dir and .gitignore with some entries already present
    fs::create_dir_all(dir.path().join(".huskies")).unwrap();
    fs::write(
        dir.path().join(".huskies/.gitignore"),
        "worktrees/\ncoverage/\n",
    )
    .unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
    let worktrees_count = content.lines().filter(|l| l.trim() == "worktrees/").count();
    assert_eq!(worktrees_count, 1, "worktrees/ should not be duplicated");
    let coverage_count = content.lines().filter(|l| l.trim() == "coverage/").count();
    assert_eq!(coverage_count, 1, "coverage/ should not be duplicated");
    // The missing entry must have been added
    assert!(content.contains("merge_workspace/"));
}

#[test]
fn scaffold_creates_claude_md_at_project_root() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let claude_md = dir.path().join("CLAUDE.md");
    assert!(
        claude_md.exists(),
        "CLAUDE.md should be created at project root"
    );

    let content = fs::read_to_string(&claude_md).unwrap();
    assert!(
        content.contains("<!-- huskies:scaffold-template -->"),
        "CLAUDE.md should contain the scaffold sentinel"
    );
    assert!(
        content.contains("Read .huskies/README.md"),
        "CLAUDE.md should include directive to read .huskies/README.md"
    );
    assert!(
        content.contains("Never chain shell commands"),
        "CLAUDE.md should include command chaining rule"
    );
    assert!(
        content.contains("wizard_status"),
        "CLAUDE.md should instruct Claude to call wizard_status on first conversation"
    );
}

#[test]
fn scaffold_does_not_overwrite_existing_claude_md() {
    let dir = tempdir().unwrap();
    let claude_md = dir.path().join("CLAUDE.md");
    fs::write(&claude_md, "custom CLAUDE.md content").unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert_eq!(
        fs::read_to_string(&claude_md).unwrap(),
        "custom CLAUDE.md content",
        "scaffold should not overwrite an existing CLAUDE.md"
    );
}

#[test]
fn scaffold_story_kit_writes_mcp_json_with_port() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 4242).unwrap();

    let mcp_path = dir.path().join(".mcp.json");
    assert!(mcp_path.exists(), ".mcp.json should be created by scaffold");
    let content = fs::read_to_string(&mcp_path).unwrap();
    assert!(
        content.contains("4242"),
        ".mcp.json should reference the given port"
    );
    assert!(
        content.contains("localhost"),
        ".mcp.json should reference localhost"
    );
    assert!(
        content.contains("huskies"),
        ".mcp.json should name the huskies server"
    );
}

#[test]
fn scaffold_story_kit_does_not_overwrite_existing_mcp_json() {
    let dir = tempdir().unwrap();
    let mcp_path = dir.path().join(".mcp.json");
    fs::write(&mcp_path, "{\"custom\": true}").unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert_eq!(
        fs::read_to_string(&mcp_path).unwrap(),
        "{\"custom\": true}",
        "scaffold should not overwrite an existing .mcp.json"
    );
}

#[test]
fn scaffold_gitignore_includes_mcp_json() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let root_gitignore = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
    assert!(
        root_gitignore.contains(".mcp.json"),
        "root .gitignore should include .mcp.json (port is environment-specific)"
    );
}

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

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join("script/test")).unwrap();
    assert!(
        content.contains("cargo test"),
        "Rust project scaffold should set cargo test in script/test"
    );
    assert!(
        !content.contains("No tests configured"),
        "should not use stub when stack is detected"
    );
}

#[test]
fn scaffold_script_test_fallback_stub_when_no_stack() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join("script/test")).unwrap();
    assert!(
        content.contains("No tests configured"),
        "unknown stack should use the generic stub"
    );
}

#[test]
fn scaffold_story_kit_creates_script_build_and_lint() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    assert!(
        dir.path().join("script/build").exists(),
        "script/build should be created by scaffold"
    );
    assert!(
        dir.path().join("script/lint").exists(),
        "script/lint should be created by scaffold"
    );
}

#[cfg(unix)]
#[test]
fn scaffold_story_kit_creates_executable_script_build_and_lint() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    for name in &["build", "lint"] {
        let path = dir.path().join("script").join(name);
        assert!(path.exists(), "script/{name} should be created");
        let perms = fs::metadata(&path).unwrap().permissions();
        assert!(
            perms.mode() & 0o111 != 0,
            "script/{name} should be executable"
        );
    }
}

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

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join("script/build")).unwrap();
    assert!(
        content.contains("cargo build --release"),
        "Rust project scaffold should set cargo build --release in script/build"
    );
}

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

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join("script/lint")).unwrap();
    assert!(
        content.contains("cargo fmt --all --check"),
        "Rust project scaffold should include fmt check in script/lint"
    );
    assert!(
        content.contains("cargo clippy -- -D warnings"),
        "Rust project scaffold should include clippy in script/lint"
    );
}

#[test]
fn scaffold_project_toml_contains_detected_components() {
    let dir = tempdir().unwrap();
    // Place a Cargo.toml in the project root before scaffolding
    fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"myapp\"\n",
    )
    .unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("[[component]]"),
        "project.toml should contain a component entry"
    );
    assert!(
        content.contains("name = \"server\""),
        "Rust project should have a 'server' component"
    );
    assert!(
        content.contains("cargo check"),
        "Rust component should have cargo check setup"
    );
}

#[test]
fn scaffold_project_toml_fallback_when_no_stack_detected() {
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
    assert!(
        content.contains("[[component]]"),
        "project.toml should always have at least one component"
    );
    // Fallback uses generic app component with empty setup — no Rust-specific commands
    assert!(
        content.contains("name = \"app\""),
        "fallback should use generic 'app' component name"
    );
    assert!(
        !content.contains("cargo"),
        "fallback must not contain Rust-specific commands for non-Rust projects"
    );
}

#[test]
fn scaffold_does_not_overwrite_existing_project_toml_with_components() {
    let dir = tempdir().unwrap();
    let sk_dir = dir.path().join(".huskies");
    fs::create_dir_all(&sk_dir).unwrap();
    let existing = "[[component]]\nname = \"custom\"\npath = \".\"\nsetup = [\"make build\"]\n";
    fs::write(sk_dir.join("project.toml"), existing).unwrap();

    scaffold_story_kit(dir.path(), 3001).unwrap();

    let content = fs::read_to_string(sk_dir.join("project.toml")).unwrap();
    assert_eq!(
        content, existing,
        "scaffold should not overwrite existing project.toml"
    );
}

#[test]
fn scaffold_story_kit_claude_settings_uses_canonical_bash_syntax() {
    // Regression for bug 883. Claude Code 2.1.123+ honours wildcard Bash
    // allowlist patterns only in the canonical form `Bash(cmd:*)`. The space
    // form `Bash(cmd *)` falls through to prompt_permission and gets
    // auto-denied in agent mode, breaking spawned coders.
    let dir = tempdir().unwrap();
    scaffold_story_kit(dir.path(), 3001).unwrap();

    let settings = fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap();

    // No `Bash(... *)` (space before asterisk) patterns may survive.
    let space_form = regex::Regex::new(r#""Bash\([^"]* \*\)""#).unwrap();
    if let Some(m) = space_form.find(&settings) {
        panic!(
            "settings.json contains non-canonical Bash pattern: {} \
             (use `Bash(cmd:*)` form instead)",
            m.as_str()
        );
    }

    // The unconstrained `Bash` rule must be present — covers all bash commands.
    // (Previously this asserted a curated per-command list; replaced with the
    // tool-only form since coders kept hitting auto-deny on patterns the list
    // missed, and the per-command gate offers no real safety in this trusted
    // single-user deployment. The earlier `Bash(:*)` form was tried and
    // rejected by Claude Code — empty prefix before `:*` is invalid and
    // silently skipped.)
    assert!(
        settings.contains(r#""Bash""#),
        "settings.json missing unconstrained Bash allowlist: {settings}"
    );
}
