//! MCP tool implementations for the interactive setup wizard.
//!
//! These tools allow Claude Code (and other MCP clients) to drive the setup
//! wizard entirely from the terminal without requiring the web UI or chat bot.
//!
//! Typical flow:
//! 1. `wizard_status`  — inspect current state
//! 2. `wizard_generate` — read the codebase and call again with `content` to
//!    stage generated text for review
//! 3. `wizard_confirm`  — write staged content to disk and advance the wizard
//! 4. `wizard_skip`     — skip a step that does not apply
//! 5. `wizard_retry`    — discard staged content and regenerate from scratch

use crate::http::context::AppContext;
use crate::io::wizard::WizardStep;
use crate::service::wizard::state_machine;
use crate::service::wizard::{self as svc};
use serde_json::Value;
use std::path::Path;

// ── Thin adapters (kept for callers in chat/commands/setup.rs) ────────────────

/// Return the filesystem path for a step's output file.
///
/// Pure path concatenation — delegates to `service::wizard::state_machine`.
pub(crate) fn step_output_path(
    project_root: &Path,
    step: WizardStep,
) -> Option<std::path::PathBuf> {
    state_machine::step_output_path(project_root, step)
}

/// Return true when `step` produces an executable script file.
pub(crate) fn is_script_step(step: WizardStep) -> bool {
    state_machine::is_script_step(step)
}

/// Write `content` to `path`, skipping if the file already has real content.
///
/// Delegates to `service::wizard::write_step_file`.
pub(crate) fn write_if_missing(
    path: &Path,
    content: &str,
    executable: bool,
) -> Result<bool, String> {
    svc::write_step_file(path, content, executable).map_err(|e| e.to_string())
}

/// Return true when the project directory has no meaningful source files.
///
/// Delegates to `service::wizard::state_machine::is_bare_project` after
/// reading directory entries via `service::wizard::io`.
#[cfg(test)]
fn is_bare_project(project_root: &Path) -> bool {
    use crate::service::wizard::io as wio;
    let names = wio::list_dir_names(project_root);
    state_machine::is_bare_project(&names)
}

/// Return a generation hint for `step` based on the project at `project_root`.
///
/// Reads filesystem state then delegates pure logic to `state_machine`.
pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
    use crate::service::wizard::io as wio;
    let names = wio::list_dir_names(project_root);
    let tools = wio::detect_project_tools(project_root);
    let is_bare = state_machine::is_bare_project(&names);
    state_machine::generation_hint(step, is_bare, &tools)
}

// ── MCP tool handlers ─────────────────────────────────────────────────────────

/// `wizard_status` — return current wizard state as a human-readable summary.
pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    svc::status(&root).map_err(|e| e.to_string())
}

/// `wizard_generate` — mark the current step as generating or stage content.
///
/// Call with no `content` argument to mark the step as `Generating` and
/// receive a hint describing what to generate.  Call again with a `content`
/// argument (the generated file body) to stage it for review; the step will
/// transition to `AwaitingConfirmation`.  Content is **not** written to disk
/// until `wizard_confirm` is called.
pub(super) fn tool_wizard_generate(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let content = args.get("content").and_then(|v| v.as_str());
    svc::generate(&root, content).map_err(|e| e.to_string())
}

/// `wizard_confirm` — confirm the current step and write its content to disk.
///
/// If the step has staged content, the content is written to its target file
/// (only if that file does not already exist — existing files are never
/// overwritten).  The step is then marked as `Confirmed` and the wizard
/// advances to the next pending step.
pub(super) fn tool_wizard_confirm(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    svc::confirm(&root).map_err(|e| e.to_string())
}

/// `wizard_skip` — skip the current step without writing any file.
pub(super) fn tool_wizard_skip(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    svc::skip(&root).map_err(|e| e.to_string())
}

/// `wizard_retry` — discard staged content and reset the current step to
/// `Pending` so it can be regenerated.
pub(super) fn tool_wizard_retry(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    svc::retry(&root).map_err(|e| e.to_string())
}

// ── tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use crate::io::wizard::{StepStatus, WizardState, format_wizard_state};
    use tempfile::TempDir;

    fn setup(dir: &TempDir) -> AppContext {
        let root = dir.path().to_path_buf();
        std::fs::create_dir_all(root.join(".huskies")).unwrap();
        WizardState::init_if_missing(&root);
        AppContext::new_test(root)
    }

    #[test]
    fn wizard_status_returns_state() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_status(&ctx).unwrap();
        assert!(result.contains("Setup wizard"));
        assert!(result.contains("context"));
    }

    #[test]
    fn wizard_status_no_wizard_returns_error() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let ctx = AppContext::new_test(dir.path().to_path_buf());
        assert!(tool_wizard_status(&ctx).is_err());
    }

    #[test]
    fn wizard_generate_marks_generating() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_generate(&serde_json::json!({}), &ctx).unwrap();
        assert!(result.contains("generating"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Generating);
    }

    #[test]
    fn wizard_generate_rejects_boilerplate_user_story_content() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_generate(
            &serde_json::json!({"content": "As a user, I want X, so that Y."}),
            &ctx,
        );
        let err = result.unwrap_err();
        assert!(err.contains("Invalid content"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Pending);
    }

    #[test]
    fn wizard_generate_with_content_stages_content() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result =
            tool_wizard_generate(&serde_json::json!({"content": "# My Project"}), &ctx).unwrap();
        assert!(result.contains("staged"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
        assert_eq!(state.steps[1].content.as_deref(), Some("# My Project"));
    }

    #[test]
    fn wizard_confirm_writes_file_and_advances() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Stage content for Context step.
        tool_wizard_generate(&serde_json::json!({"content": "# Context content"}), &ctx).unwrap();
        let result = tool_wizard_confirm(&ctx).unwrap();
        assert!(result.contains("confirmed"));
        // File should now exist.
        let context_path = dir
            .path()
            .join(".huskies")
            .join("specs")
            .join("00_CONTEXT.md");
        assert!(context_path.exists());
        assert_eq!(
            std::fs::read_to_string(&context_path).unwrap(),
            "# Context content"
        );
        // Wizard should have advanced.
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Confirmed);
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn wizard_confirm_does_not_overwrite_existing_file() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Pre-create the specs directory and file with real (non-template) content.
        let specs_dir = dir.path().join(".huskies").join("specs");
        std::fs::create_dir_all(&specs_dir).unwrap();
        let context_path = specs_dir.join("00_CONTEXT.md");
        std::fs::write(&context_path, "original content").unwrap();

        // Stage and confirm — existing real file should NOT be overwritten.
        tool_wizard_generate(&serde_json::json!({"content": "new content"}), &ctx).unwrap();
        let result = tool_wizard_confirm(&ctx).unwrap();
        assert!(result.contains("already exists"));
        assert_eq!(
            std::fs::read_to_string(&context_path).unwrap(),
            "original content"
        );
    }

    #[test]
    fn wizard_confirm_overwrites_scaffold_template_file() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Pre-create the file with scaffold template placeholder content.
        let specs_dir = dir.path().join(".huskies").join("specs");
        std::fs::create_dir_all(&specs_dir).unwrap();
        let context_path = specs_dir.join("00_CONTEXT.md");
        std::fs::write(
            &context_path,
            "<!-- huskies:scaffold-template -->\n# Project Context\n\nTODO: Describe...",
        )
        .unwrap();

        // Stage and confirm — template placeholder should be overwritten with generated content.
        tool_wizard_generate(
            &serde_json::json!({"content": "# My Real Project\n\nThis is a real project."}),
            &ctx,
        )
        .unwrap();
        let result = tool_wizard_confirm(&ctx).unwrap();
        assert!(result.contains("confirmed"));
        assert_eq!(
            std::fs::read_to_string(&context_path).unwrap(),
            "# My Real Project\n\nThis is a real project."
        );
    }

    #[test]
    fn wizard_skip_advances_wizard() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_skip(&ctx).unwrap();
        assert!(result.contains("skipped"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Skipped);
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn wizard_retry_resets_to_pending() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Stage content first.
        tool_wizard_generate(&serde_json::json!({"content": "some content"}), &ctx).unwrap();
        let result = tool_wizard_retry(&ctx).unwrap();
        assert!(result.contains("reset"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Pending);
        assert!(state.steps[1].content.is_none());
    }

    #[test]
    fn wizard_complete_returns_done_message() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Skip all remaining steps (scaffold is pre-confirmed, so 7 remaining).
        for _ in 0..7 {
            tool_wizard_skip(&ctx).unwrap();
        }
        let result = tool_wizard_status(&ctx).unwrap();
        assert!(result.contains("complete"));
    }

    #[test]
    fn format_wizard_state_shows_all_steps() {
        let mut state = WizardState::default();
        state.steps[0].status = StepStatus::Confirmed;
        let output = format_wizard_state(&state);
        assert!(output.contains("✓"));
        assert!(output.contains("Scaffold"));
        assert!(output.contains("← current"));
    }

    #[test]
    fn is_bare_project_detects_empty_dir() {
        let dir = TempDir::new().unwrap();
        assert!(is_bare_project(dir.path()));
    }

    #[test]
    fn is_bare_project_detects_scaffold_only_dir() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        std::fs::write(dir.path().join("CLAUDE.md"), "# Claude").unwrap();
        std::fs::write(dir.path().join("README.md"), "# Readme").unwrap();
        std::fs::create_dir_all(dir.path().join("script")).unwrap();
        assert!(is_bare_project(dir.path()));
    }

    #[test]
    fn is_bare_project_false_when_source_files_exist() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
        assert!(!is_bare_project(dir.path()));
    }

    #[test]
    fn is_bare_project_false_with_src_directory() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join("src")).unwrap();
        assert!(!is_bare_project(dir.path()));
    }

    #[test]
    fn generation_hint_bare_context_asks_user() {
        let dir = TempDir::new().unwrap();
        // Bare project — only scaffolding
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::Context, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("Ask the user"));
    }

    #[test]
    fn generation_hint_bare_stack_asks_user() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::Stack, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("Ask the user"));
    }

    #[test]
    fn generation_hint_bare_test_script_references_stack() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::TestScript, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("STACK.md"));
    }

    #[test]
    fn generation_hint_bare_release_script_references_stack() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::ReleaseScript, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("STACK.md"));
    }

    #[test]
    fn generation_hint_bare_test_coverage_references_stack() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::TestCoverage, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("STACK.md"));
    }

    #[test]
    fn generation_hint_existing_project_reads_code() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
        let hint = generation_hint(WizardStep::Context, dir.path());
        assert!(hint.contains("Read the project"));
        assert!(!hint.contains("bare project"));
    }

    #[test]
    fn generation_hint_existing_project_test_script_detects_cargo() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
        let hint = generation_hint(WizardStep::TestScript, dir.path());
        assert!(hint.contains("cargo nextest"));
        assert!(!hint.contains("bare project"));
    }

    #[test]
    fn generation_hint_bare_build_script_references_stack() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::BuildScript, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("STACK.md"));
    }

    #[test]
    fn generation_hint_bare_lint_script_references_stack() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        let hint = generation_hint(WizardStep::LintScript, dir.path());
        assert!(hint.contains("bare project"));
        assert!(hint.contains("STACK.md"));
    }

    #[test]
    fn generation_hint_existing_project_build_script_detects_cargo() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
        let hint = generation_hint(WizardStep::BuildScript, dir.path());
        assert!(hint.contains("cargo build --release"));
        assert!(!hint.contains("bare project"));
    }

    #[test]
    fn generation_hint_existing_project_lint_script_detects_cargo() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
        let hint = generation_hint(WizardStep::LintScript, dir.path());
        assert!(hint.contains("cargo fmt --all --check"));
        assert!(hint.contains("cargo clippy -- -D warnings"));
        assert!(!hint.contains("bare project"));
    }

    #[test]
    fn step_output_path_build_script_returns_script_build() {
        let dir = TempDir::new().unwrap();
        let path = step_output_path(dir.path(), WizardStep::BuildScript).unwrap();
        assert!(path.ends_with("script/build"));
    }

    #[test]
    fn step_output_path_lint_script_returns_script_lint() {
        let dir = TempDir::new().unwrap();
        let path = step_output_path(dir.path(), WizardStep::LintScript).unwrap();
        assert!(path.ends_with("script/lint"));
    }

    #[test]
    fn is_script_step_includes_build_and_lint() {
        assert!(is_script_step(WizardStep::BuildScript));
        assert!(is_script_step(WizardStep::LintScript));
    }
}
