mod modal;
mod options;
mod schema;
mod suggestions;

pub(crate) use modal::execute_request_user_input_tool;
pub(crate) use schema::normalize_request_user_input_fallback_args;

#[cfg(test)]
mod tests {
    use super::modal::build_question_items;
    use super::options::{resolve_question_options, sanitize_provided_options};
    use super::schema::{
        RequestUserInputOption, RequestUserInputQuestion, normalize_request_user_input_args,
        normalize_request_user_input_fallback_args,
    };
    use super::suggestions::generate_suggested_options;
    use serde_json::json;
    use vtcode_tui::app::{InlineListSelection, WizardModalMode};

    fn prompt_question_with_hints() -> RequestUserInputQuestion {
        RequestUserInputQuestion {
            id: "system_prompt_plan".to_string(),
            header: "Direction".to_string(),
            question: "Which area should we prioritize to improve plan mode behavior?".to_string(),
            options: None,
            focus_area: Some("system prompt".to_string()),
            analysis_hints: vec!["navigation loop".to_string(), "stream timeout".to_string()],
        }
    }

    #[test]
    fn generates_prompt_specific_suggestions() {
        let question = prompt_question_with_hints();
        let options = generate_suggested_options(&question).expect("expected generated options");

        assert!((1..=3).contains(&options.len()));
        assert!(options[0].label.contains("(Recommended)"));
        assert!(
            options
                .iter()
                .any(|option| option.label.contains("fallback") || option.label.contains("Loop"))
        );
    }

    #[test]
    fn generates_weakness_aware_prompt_options() {
        let question = RequestUserInputQuestion {
            id: "prompt_improvement".to_string(),
            header: "Direction".to_string(),
            question: "Which system prompt improvement should we prioritize?".to_string(),
            options: None,
            focus_area: Some("system_prompt".to_string()),
            analysis_hints: vec![
                "Redundancy exists between prompt variants".to_string(),
                "Missing explicit guidance for failure patterns".to_string(),
            ],
        };

        let options = generate_suggested_options(&question).expect("expected generated options");
        assert!((1..=3).contains(&options.len()));
        assert!(options.iter().any(|opt| {
            opt.label.contains("redundancy")
                || opt.label.contains("Failure pattern")
                || opt.label.contains("Prompt")
        }));
    }

    #[test]
    fn generates_planning_options_for_goal_constraints_questions() {
        let question = RequestUserInputQuestion {
            id: "constraints".to_string(),
            header: "Plan".to_string(),
            question: "Break the work into 3-7 composable steps. For each step include target file(s) and a concrete expected outcome.".to_string(),
            options: None,
            focus_area: None,
            analysis_hints: Vec::new(),
        };

        let options = generate_suggested_options(&question).expect("expected planning options");
        assert!((1..=3).contains(&options.len()));
        assert!(options[0].label.contains("(Recommended)"));
    }

    #[test]
    fn generates_distinct_options_for_outcome_steps_and_verification_questions() {
        let outcome_question = RequestUserInputQuestion {
            id: "q1".to_string(),
            header: "Q1".to_string(),
            question: "What user-visible outcome should this change deliver, and what constraints or non-goals must be respected?".to_string(),
            options: None,
            focus_area: Some("system_prompt".to_string()),
            analysis_hints: vec![
                "Redundancy exists between prompt variants".to_string(),
                "Missing explicit guidance for failure patterns".to_string(),
            ],
        };
        let steps_question = RequestUserInputQuestion {
            id: "q2".to_string(),
            header: "Q2".to_string(),
            question: "Break the work into 3-7 composable steps. For each step include target file(s) and a concrete expected outcome.".to_string(),
            options: None,
            focus_area: Some("system_prompt".to_string()),
            analysis_hints: vec![
                "Redundancy exists between prompt variants".to_string(),
                "Missing explicit guidance for failure patterns".to_string(),
            ],
        };
        let verification_question = RequestUserInputQuestion {
            id: "q3".to_string(),
            header: "Q3".to_string(),
            question: "For each step, what exact command or manual check proves it is complete?"
                .to_string(),
            options: None,
            focus_area: Some("system_prompt".to_string()),
            analysis_hints: vec![
                "Redundancy exists between prompt variants".to_string(),
                "Missing explicit guidance for failure patterns".to_string(),
            ],
        };

        let outcome = generate_suggested_options(&outcome_question).expect("outcome options");
        let steps = generate_suggested_options(&steps_question).expect("step options");
        let verification =
            generate_suggested_options(&verification_question).expect("verification options");

        let outcome_labels = outcome
            .iter()
            .map(|opt| opt.label.clone())
            .collect::<Vec<_>>();
        let step_labels = steps
            .iter()
            .map(|opt| opt.label.clone())
            .collect::<Vec<_>>();
        let verification_labels = verification
            .iter()
            .map(|opt| opt.label.clone())
            .collect::<Vec<_>>();

        assert_ne!(outcome_labels, step_labels);
        assert_ne!(step_labels, verification_labels);
        assert_ne!(outcome_labels, verification_labels);

        assert!(outcome[0].label.contains("Recommended"));
        assert!(steps[0].label.contains("Recommended"));
        assert!(verification[0].label.contains("Recommended"));
    }

    #[test]
    fn provided_duplicate_options_are_regenerated_per_question() {
        let duplicate_options = vec![
            RequestUserInputOption {
                label: "Minimal implementation slice (Recommended)".to_string(),
                description: "Ship only the smallest possible scope.".to_string(),
            },
            RequestUserInputOption {
                label: "Balanced implementation".to_string(),
                description: "Ship medium scope with moderate risk.".to_string(),
            },
            RequestUserInputOption {
                label: "Comprehensive implementation".to_string(),
                description: "Ship full scope with deeper validation.".to_string(),
            },
        ];

        let questions = vec![
            RequestUserInputQuestion {
                id: "goal".to_string(),
                header: "Goal".to_string(),
                question: "What user-visible outcome should this change deliver, and what constraints or non-goals must be respected?".to_string(),
                options: Some(duplicate_options.clone()),
                focus_area: None,
                analysis_hints: Vec::new(),
            },
            RequestUserInputQuestion {
                id: "constraints".to_string(),
                header: "Plan".to_string(),
                question: "Break the work into 3-7 composable steps. For each step include target file(s) and a concrete expected outcome.".to_string(),
                options: Some(duplicate_options.clone()),
                focus_area: None,
                analysis_hints: Vec::new(),
            },
            RequestUserInputQuestion {
                id: "verification".to_string(),
                header: "Verification".to_string(),
                question: "For each step, what exact command or manual check proves it is complete?"
                    .to_string(),
                options: Some(duplicate_options),
                focus_area: None,
                analysis_hints: Vec::new(),
            },
        ];

        let resolved = resolve_question_options(&questions);
        assert_eq!(resolved.len(), 3);

        let goal_labels = resolved[0]
            .as_ref()
            .expect("goal options should resolve")
            .iter()
            .map(|option| option.label.clone())
            .collect::<Vec<_>>();
        let step_labels = resolved[1]
            .as_ref()
            .expect("step options should resolve")
            .iter()
            .map(|option| option.label.clone())
            .collect::<Vec<_>>();
        let verification_labels = resolved[2]
            .as_ref()
            .expect("verification options should resolve")
            .iter()
            .map(|option| option.label.clone())
            .collect::<Vec<_>>();

        assert_ne!(goal_labels, step_labels);
        assert_ne!(step_labels, verification_labels);
        assert_ne!(goal_labels, verification_labels);
    }

    #[test]
    fn valid_provided_options_are_preserved() {
        let provided_options = vec![
            RequestUserInputOption {
                label: "Outcome KPI (Recommended)".to_string(),
                description: "Define one measurable user-visible result.".to_string(),
            },
            RequestUserInputOption {
                label: "Constraint checklist".to_string(),
                description: "Lock boundaries before implementation.".to_string(),
            },
            RequestUserInputOption {
                label: "MVP boundary".to_string(),
                description: "Limit scope to the smallest deliverable.".to_string(),
            },
        ];

        let questions = vec![RequestUserInputQuestion {
            id: "goal".to_string(),
            header: "Goal".to_string(),
            question: "What user-visible outcome should this change deliver, and what constraints or non-goals must be respected?".to_string(),
            options: Some(provided_options.clone()),
            focus_area: None,
            analysis_hints: Vec::new(),
        }];

        let resolved = resolve_question_options(&questions);
        let resolved_options = resolved[0]
            .as_ref()
            .expect("provided options should be preserved");

        assert_eq!(resolved_options.len(), provided_options.len());
        for (resolved_option, provided_option) in resolved_options.iter().zip(provided_options) {
            assert_eq!(resolved_option.label, provided_option.label);
            assert_eq!(resolved_option.description, provided_option.description);
        }
    }

    #[test]
    fn id_keyword_does_not_override_question_text_intent() {
        let question = RequestUserInputQuestion {
            id: "constraints".to_string(),
            header: "Plan".to_string(),
            question: "For each step, what exact command or manual check proves it is complete?"
                .to_string(),
            options: None,
            focus_area: None,
            analysis_hints: Vec::new(),
        };

        let options = generate_suggested_options(&question).expect("verification options");
        let labels = options
            .iter()
            .map(|option| option.label.to_lowercase())
            .collect::<Vec<_>>();

        assert!(
            labels
                .iter()
                .any(|label| label.contains("command-based proof"))
        );
        assert!(
            !labels
                .iter()
                .any(|label| label.contains("dependency-first slices"))
        );
    }

    #[test]
    fn option_questions_add_explicit_custom_note_choice() {
        let question = RequestUserInputQuestion {
            id: "scope".to_string(),
            header: "Scope".to_string(),
            question: "Pick direction".to_string(),
            options: Some(vec![
                RequestUserInputOption {
                    label: "Option A".to_string(),
                    description: "A".to_string(),
                },
                RequestUserInputOption {
                    label: "Option B".to_string(),
                    description: "B".to_string(),
                },
            ]),
            focus_area: None,
            analysis_hints: Vec::new(),
        };

        let items = build_question_items(&question);
        assert_eq!(items.len(), 3);
        assert!(items[2].title.contains("Custom note"));

        let selection = items[2]
            .selection
            .clone()
            .expect("expected selection for other choice");
        match selection {
            InlineListSelection::RequestUserInputAnswer {
                selected, other, ..
            } => {
                assert!(selected.is_empty());
                assert_eq!(other, Some(String::new()));
            }
            _ => panic!("expected request_user_input selection"),
        }
    }

    #[test]
    fn falls_back_to_generic_options_when_no_suggestions_apply() {
        let question = RequestUserInputQuestion {
            id: "env".to_string(),
            header: "Env".to_string(),
            question: "What environment are you using?".to_string(),
            options: None,
            focus_area: None,
            analysis_hints: Vec::new(),
        };

        let items = build_question_items(&question);
        assert_eq!(items.len(), 4);
        assert!(items[0].title.contains("(Recommended)"));
        assert!(items[3].title.contains("Custom note"));
    }

    #[test]
    fn structured_payload_normalizes_to_multi_step_mode() {
        let args = json!({
            "questions": [
                {
                    "id": "scope",
                    "header": "Scope",
                    "question": "Which direction should we take?",
                    "options": [
                        {"label": "Minimal (Recommended)", "description": "Smallest viable slice"},
                        {"label": "Full", "description": "Complete implementation"}
                    ]
                }
            ]
        });

        let normalized = normalize_request_user_input_args(&args).expect("normalize structured");
        assert_eq!(normalized.args.questions.len(), 1);
        assert_eq!(normalized.wizard_mode, WizardModalMode::MultiStep);
        assert_eq!(normalized.current_step, 0);
        assert_eq!(normalized.title_override, None);
        assert_eq!(normalized.freeform_label.as_deref(), Some("Custom note"));
        assert_eq!(
            normalized.freeform_placeholder.as_deref(),
            Some("Type your response...")
        );
    }

    #[test]
    fn legacy_payload_is_rejected() {
        let legacy_args = json!({
            "question": "Choose one",
            "tabs": [
                {
                    "id": "scope",
                    "title": "Scope",
                    "items": [
                        {"id": "minimal", "title": "Minimal scope"},
                        {"id": "full", "title": "Full scope"}
                    ]
                }
            ]
        });
        let result = normalize_request_user_input_args(&legacy_args);
        assert!(result.is_err());
    }

    #[test]
    fn fallback_normalizes_legacy_tabbed_payload() {
        let legacy_args = json!({
            "question": "Choose one",
            "tabs": [
                {
                    "id": "scope",
                    "title": "Scope",
                    "items": [
                        {"id": "minimal", "title": "Minimal scope"},
                        {"id": "full", "title": "Full scope"}
                    ]
                }
            ]
        });

        let normalized = normalize_request_user_input_fallback_args(&legacy_args)
            .expect("fallback should normalize legacy shape");
        assert_eq!(normalized["questions"][0]["id"], "scope");
        assert_eq!(normalized["questions"][0]["header"], "Scope");
        assert_eq!(normalized["questions"][0]["question"], "Choose one");
        assert_eq!(
            normalized["questions"][0]["options"]
                .as_array()
                .map(|options| options.len()),
            Some(2)
        );
    }

    #[test]
    fn fallback_normalizes_single_question_shorthand() {
        let args = json!({
            "question": "Which direction should we take?",
            "header": "Scope",
            "options": ["Minimal", "Full", "Minimal"]
        });

        let normalized = normalize_request_user_input_fallback_args(&args)
            .expect("fallback should normalize shorthand");
        assert_eq!(normalized["questions"][0]["id"], "question_1");
        assert_eq!(normalized["questions"][0]["header"], "Scope");
        assert_eq!(
            normalized["questions"][0]["question"],
            "Which direction should we take?"
        );
        assert_eq!(
            normalized["questions"][0]["options"]
                .as_array()
                .map(|options| options.len()),
            Some(2)
        );
    }

    #[test]
    fn normalize_rejects_non_snake_case_ids() {
        let args = json!({
            "questions": [
                {
                    "id": "GoalQuestion",
                    "header": "Goal",
                    "question": "What outcome matters most?"
                }
            ]
        });

        let result = normalize_request_user_input_args(&args);
        assert!(result.is_err());
    }

    #[test]
    fn normalize_rejects_headers_over_twelve_chars() {
        let args = json!({
            "questions": [
                {
                    "id": "goal",
                    "header": "HeaderTooLong",
                    "question": "What outcome matters most?"
                }
            ]
        });

        let result = normalize_request_user_input_args(&args);
        assert!(result.is_err());
    }

    #[test]
    fn sanitize_provided_options_drops_other_and_duplicates() {
        let options = vec![
            RequestUserInputOption {
                label: "A (Recommended)".to_string(),
                description: "Choice A".to_string(),
            },
            RequestUserInputOption {
                label: "Other".to_string(),
                description: "Custom response".to_string(),
            },
            RequestUserInputOption {
                label: "A".to_string(),
                description: "Duplicate A".to_string(),
            },
            RequestUserInputOption {
                label: "B".to_string(),
                description: "Choice B".to_string(),
            },
        ];

        let sanitized = sanitize_provided_options(&options);
        let labels = sanitized
            .iter()
            .map(|option| option.label.as_str())
            .collect::<Vec<_>>();
        assert_eq!(labels, vec!["A (Recommended)", "B"]);
    }
}
